Merge pull request #98 from presenton/feat/nextjs_api_implement
chore: presentation image,icon updates & api implementation
This commit is contained in:
commit
9cf5ba3906
15 changed files with 83 additions and 61 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,4 +10,5 @@ user_data
|
|||
app_data
|
||||
tmp
|
||||
debug
|
||||
.fastembed_cache
|
||||
.fastembed_cache
|
||||
my-doc.txt
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue