Merge pull request #98 from presenton/feat/nextjs_api_implement

chore: presentation image,icon updates & api implementation
This commit is contained in:
Shiva Raj Badu 2025-07-19 18:47:41 +05:45 committed by GitHub
commit 9cf5ba3906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 83 additions and 61 deletions

3
.gitignore vendored
View file

@ -10,4 +10,5 @@ user_data
app_data
tmp
debug
.fastembed_cache
.fastembed_cache
my-doc.txt

View file

@ -298,7 +298,7 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
onClose={handleEditorClose}
onImageChange={handleImageChange}
>
<div />
</ImageEditor>
)}
@ -311,7 +311,7 @@ const EditableLayoutWrapper: React.FC<EditableLayoutWrapperProps> = ({
onClose={handleEditorClose}
onIconChange={handleIconChange}
>
<div />
</IconsEditor>
)}
</div>

View file

@ -31,23 +31,20 @@ const IconsEditor = ({
}: IconsEditorProps) => {
// State management
const [icon, setIcon] = useState(initialIcon);
const [icons, setIcons] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState<string>(
icon_prompt?.[0] || ""
);
const [loading, setLoading] = useState(true);
const searchParams = useSearchParams();
// Update local state when initial icon changes
useEffect(() => {
setIcon(initialIcon);
}, [initialIcon]);
// Search for icons when component opens
useEffect(() => {
handleIconSearch();
if (icon_prompt && icon_prompt.length > 0 && icons.length === 0) {
handleIconSearch();
}
}, []);
/**
@ -55,17 +52,15 @@ const IconsEditor = ({
*/
const handleIconSearch = async () => {
setLoading(true);
const presentation_id = searchParams.get("id");
const query = searchQuery.length > 0 ? searchQuery : icon_prompt?.[0] || "";
try {
const data = await PresentationGenerationApi.searchIcons({
presentation_id: presentation_id!,
query,
page: 1,
limit: 40,
});
setIcons(data.paths);
console.log("icons search data", data);
setIcons(data);
} catch (error) {
console.error("Error fetching icons:", error);
setIcons([]);
@ -78,7 +73,6 @@ const IconsEditor = ({
* Handles icon selection and calls the parent callback
*/
const handleIconChange = (newIcon: string) => {
setIcon(newIcon);
if (onIconChange) {
onIconChange(newIcon, searchQuery || icon_prompt?.[0] || '');
@ -137,7 +131,7 @@ const IconsEditor = ({
<Skeleton key={index} className="w-16 h-16 rounded-lg" />
))}
</div>
) : icons.length > 0 ? (
) : icons && icons.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{icons.map((iconSrc, idx) => (
<div

View file

@ -44,8 +44,6 @@ const ImageEditor = ({
onImageChange,
}: ImageEditorProps) => {
const { currentTheme } = useSelector((state: RootState) => state.theme);
const searchParams = useSearchParams();
// State management
const [image, setImage] = useState(initialImage);
@ -186,23 +184,21 @@ const ImageEditor = ({
* Generates new images using AI
*/
const handleGenerateImage = async () => {
if (!prompt) {
setError("Please enter a prompt");
return;
}
console.log("prompt", prompt);
try {
setIsGenerating(true);
setError(null);
const presentation_id = searchParams.get("id");
const response = await PresentationGenerationApi.generateImage({
presentation_id: presentation_id!,
prompt: {
theme_prompt: ThemeImagePrompt[currentTheme],
image_prompt: prompt,
aspect_ratio: "4:5",
},
prompt: prompt,
});
setPreviewImages(response.paths);
} catch (err) {
console.error("Error in image generation", err);
setError("Failed to generate image. Please try again.");
} finally {
setIsGenerating(false);

View file

@ -1,9 +1,10 @@
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "@/hooks/use-toast";
import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { StreamState } from "../types/index";
import { RootState } from "@/store/store";
const DEFAULT_STREAM_STATE: StreamState = {
isStreaming: false,
@ -12,10 +13,11 @@ const DEFAULT_STREAM_STATE: StreamState = {
export const useOutlineStreaming = (presentationId: string | null) => {
const dispatch = useDispatch();
const {outlines} = useSelector((state: RootState) => state.presentationGeneration);
const [streamState, setStreamState] = useState<StreamState>(DEFAULT_STREAM_STATE);
useEffect(() => {
if (!presentationId) return;
if (!presentationId || outlines.length > 0) return;
let eventSource: EventSource;
let accumulatedChunks = "";

View file

@ -2,7 +2,7 @@ import { useState, useCallback } from "react";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration";
import { clearPresentationData, setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { useLayout } from "../../context/LayoutContext";
import { LayoutGroup, LoadingState } from "../types/index";
@ -69,8 +69,11 @@ export const usePresentationGeneration = (
}, [selectedLayoutGroup, getLayoutById]);
const handleSubmit = useCallback(async () => {
if (!validateInputs()) return;
setLoadingState({
message: "Generating presentation data...",
isLoading: true,
@ -89,8 +92,8 @@ export const usePresentationGeneration = (
});
if (response) {
dispatch(setPresentationData(response));
router.push(`/presentation?id=${presentationId}&stream=true`);
dispatch(clearPresentationData());
router.push(`/presentation?id=${presentationId}&stream=true`);
}
} catch (error) {
console.error("Error in data generation", error);

View file

@ -6,18 +6,14 @@ export interface ImageSearch {
}
export interface ImageGenerate {
presentation_id: string;
prompt: {
theme_prompt: string;
image_prompt: string;
aspect_ratio: string;
};
prompt: string;
}
export interface IconSearch {
presentation_id: string;
query: string;
category?: string;
page: number;
limit: number;
}

View file

@ -172,11 +172,10 @@ export class PresentationGenerationApi {
static async generateImage(imageGenerate: ImageGenerate) {
try {
const response = await fetch(
`/api/v1/ppt/image/generate`,
`/api/v1/ppt/images/generate?prompt=${imageGenerate.prompt}`,
{
method: "POST",
method: "GET",
headers: getHeader(),
body: JSON.stringify(imageGenerate),
cache: "no-cache",
}
);
@ -195,11 +194,10 @@ export class PresentationGenerationApi {
static async searchIcons(iconSearch: IconSearch) {
try {
const response = await fetch(
`/api/v1/ppt/icon/search`,
`/api/v1/ppt/icons/search?query=${iconSearch.query}&limit=${iconSearch.limit}`,
{
method: "POST",
method: "GET",
headers: getHeader(),
body: JSON.stringify(iconSearch),
cache: "no-cache",
}
);

View file

@ -13,7 +13,7 @@
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { useDispatch } from "react-redux";
import { setPresentationId } from "@/store/slices/presentationGeneration";
import { clearOutlines, setPresentationId } from "@/store/slices/presentationGeneration";
import { ConfigurationSelects } from "./ConfigurationSelects";
import { PromptInput } from "./PromptInput";
import { LanguageType, PresentationConfig } from "../type";
@ -154,8 +154,6 @@ const UploadPage = () => {
});
// Use the first available layout group for direct generation
const createResponse = await PresentationGenerationApi.createPresentation({
prompt: config?.prompt ?? "",
n_slides: config?.slides ? parseInt(config.slides) : null,
@ -164,6 +162,7 @@ const UploadPage = () => {
});
dispatch(setPresentationId(createResponse.id));
dispatch(clearOutlines());
router.push("/outline");
};

View file

@ -22,12 +22,14 @@ export async function POST(request: NextRequest) {
const buffer = Buffer.from(bytes);
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(userDataDir, "uploads");
const uploadsDir = path.join(userDataDir, "images");
fs.mkdirSync(uploadsDir, { recursive: true });
console.log("uploadsDir", uploadsDir);
// Generate unique filename
const filename = `${crypto.randomBytes(16).toString("hex")}.png`;
const filePath = path.join(uploadsDir, filename);
console.log("filePath", filePath);
// Write file to disk
fs.writeFileSync(filePath, buffer);
@ -35,7 +37,7 @@ export async function POST(request: NextRequest) {
// Return the relative path that can be used in the frontend
return NextResponse.json({
success: true,
filePath: `${userDataDir}/uploads/${filename}`
filePath: `${uploadsDir}/${filename}`
});
} catch (error) {
console.error("Error saving image:", error);

View file

@ -67,20 +67,29 @@ export class DashboardApi {
static async deletePresentation(presentation_id: string) {
try {
const response = await fetch(
`/api/v1/ppt/delete?id=${presentation_id}`,
`/api/v1/ppt/presentation/?id=${presentation_id}`,
{
method: "DELETE",
headers: getHeader(),
}
);
const data = await response.json();
if (response.status === 204) {
return true;
return {
success: true,
};
}
return false;
return {
success: false,
message: data.detail || "Failed to delete presentation",
};
} catch (error) {
console.error("Error deleting presentation:", error);
throw error;
return {
success: false,
message: error instanceof Error ? error.message : "Failed to delete presentation",
};
}
}

View file

@ -35,6 +35,11 @@ const DashboardPage: React.FC = () => {
}
};
const removePresentation = (presentationId: string) => {
setPresentations((prev: any) =>
prev ? prev.filter((p: any) => p.id !== presentationId) : []
);
};
return (
<div className="min-h-screen bg-[#E9E8F8]">
@ -50,6 +55,7 @@ const DashboardPage: React.FC = () => {
type="slide"
isLoading={isLoading}
error={error}
onPresentationDeleted={removePresentation}
/>
</section>
</main>

View file

@ -16,12 +16,14 @@ export const PresentationCard = ({
id,
title,
created_at,
slide
slide,
onDeleted
}: {
id: string;
title: string;
created_at: string;
slide: any
slide: any;
onDeleted?: (presentationId: string) => void;
}) => {
const router = useRouter();
const { renderSlideContent } = useGroupLayouts();
@ -50,6 +52,10 @@ export const PresentationCard = ({
description: "The presentation has been deleted successfully",
variant: "default",
});
// Call the onDeleted callback to update the parent state
if (onDeleted) {
onDeleted(id);
}
} else {
toast({
title: "Error",
@ -57,7 +63,7 @@ export const PresentationCard = ({
variant: "destructive",
});
}
window.location.reload();
// Removed window.location.reload() - no longer needed
};
@ -79,7 +85,10 @@ export const PresentationCard = ({
</p>
<Popover>
<PopoverTrigger onClick={(e) => e.stopPropagation()}>
<DotsVerticalIcon className="w-4 h-4 text-gray-500" />
<button className="w-6 h-6 rounded-full flex items-center justify-center text-gray-500 hover:text-gray-700" >
<DotsVerticalIcon className="w-4 h-4 text-gray-500" />
</button>
</PopoverTrigger>
<PopoverContent align="end" className="bg-white w-[200px]">
<button

View file

@ -9,6 +9,7 @@ interface PresentationGridProps {
type: "slide" | "video";
isLoading?: boolean;
error?: string | null;
onPresentationDeleted?: (presentationId: string) => void;
}
export const PresentationGrid = ({
@ -16,6 +17,7 @@ export const PresentationGrid = ({
type,
isLoading = false,
error = null,
onPresentationDeleted,
}: PresentationGridProps) => {
const router = useRouter();
const handleCreateNewPresentation = () => {
@ -105,7 +107,7 @@ export const PresentationGrid = ({
title={presentation.title}
created_at={presentation.created_at}
slide={presentation.slides[0]}
onDeleted={onPresentationDeleted}
/>
))}
</div>

View file

@ -72,6 +72,10 @@ const presentationGenerationSlice = createSlice({
state.presentation_id = null;
state.error = null;
state.isLoading = false;
state.presentationData = null;
},
clearOutlines: (state) => {
state.outlines = [];
},
// Set outlines
setOutlines: (state, action: PayloadAction<SlideOutline[]>) => {
@ -341,6 +345,7 @@ export const {
setSlidesRendered,
setError,
clearPresentationData,
clearOutlines,
deleteSlideOutline,
setPresentationData,
setOutlines,