From 89048bae15505d8feb8f259bae2daf55ca5f9d81 Mon Sep 17 00:00:00 2001 From: Riccardo Giorato Date: Fri, 26 Dec 2025 19:43:28 +0100 Subject: [PATCH] Add redraw page functionality with pageId support and UI updates --- app/api/add-page/route.ts | 61 +++++++++++++--- app/editor/[storySlug]/page.tsx | 89 ++++++++++++++++++++--- app/stories/page.tsx | 6 +- components/editor/comic-canvas.tsx | 5 +- components/editor/generate-page-modal.tsx | 22 ++++-- components/editor/page-sidebar.tsx | 6 +- 6 files changed, 151 insertions(+), 38 deletions(-) diff --git a/app/api/add-page/route.ts b/app/api/add-page/route.ts index 029fc2f..6bdcc21 100644 --- a/app/api/add-page/route.ts +++ b/app/api/add-page/route.ts @@ -1,6 +1,9 @@ import { type NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; import Together from "together-ai"; +import { db } from "@/lib/db"; +import { pages } from "@/lib/schema"; +import { eq } from "drizzle-orm"; import { updatePage, createPage, @@ -34,7 +37,7 @@ export async function POST(request: NextRequest) { ); } - const { storyId, prompt, characterImages = [] } = await request.json(); + const { storyId, pageId, prompt, characterImages = [] } = await request.json(); if (!storyId || !prompt) { return NextResponse.json( @@ -76,13 +79,35 @@ export async function POST(request: NextRequest) { } } - const nextPageNumber = await getNextPageNumber(story.id); - const page = await createPage({ - storyId: story.id, - pageNumber: nextPageNumber, - prompt, - characterImageUrls: characterImages, - }); + let page; + let pageNumber; + let isRedraw = false; + + if (pageId) { + // Redraw mode: update existing page + isRedraw = true; + const storyData = await getStoryWithPagesBySlug(storyId); + if (!storyData) { + return NextResponse.json({ error: "Story not found" }, { status: 404 }); + } + + const existingPage = storyData.pages.find(p => p.id === pageId); + if (!existingPage) { + return NextResponse.json({ error: "Page not found" }, { status: 404 }); + } + + page = existingPage; + pageNumber = existingPage.pageNumber; + } else { + // Add new page mode + pageNumber = await getNextPageNumber(story.id); + page = await createPage({ + storyId: story.id, + pageNumber, + prompt, + characterImageUrls: characterImages, + }); + } const dimensions = FIXED_DIMENSIONS; @@ -90,10 +115,22 @@ export async function POST(request: NextRequest) { let referenceImages: string[] = []; // Get previous page image for style consistency (unless it's page 1) - if (nextPageNumber > 1) { - const lastPageImage = await getLastPageImage(story.id); - if (lastPageImage) { - referenceImages.push(lastPageImage); + if (pageNumber > 1) { + if (isRedraw) { + // For redraw, get all pages and find the previous page's image + const storyData = await getStoryWithPagesBySlug(storyId); + if (storyData) { + const previousPage = storyData.pages.find(p => p.pageNumber === pageNumber - 1); + if (previousPage?.generatedImageUrl) { + referenceImages.push(previousPage.generatedImageUrl); + } + } + } else { + // For new page, use the last page image + const lastPageImage = await getLastPageImage(story.id); + if (lastPageImage) { + referenceImages.push(lastPageImage); + } } } diff --git a/app/editor/[storySlug]/page.tsx b/app/editor/[storySlug]/page.tsx index 877bf6e..d5eb91e 100644 --- a/app/editor/[storySlug]/page.tsx +++ b/app/editor/[storySlug]/page.tsx @@ -9,6 +9,7 @@ import { ComicCanvas } from "@/components/editor/comic-canvas"; import { ApiKeyModal } from "@/components/api-key-modal"; import { PageInfoSheet } from "@/components/editor/page-info-sheet"; import { GeneratePageModal } from "@/components/editor/generate-page-modal"; +import { StoryLoader } from "@/components/ui/story-loader"; interface PageData { id: number; // pageNumber for component compatibility @@ -119,6 +120,67 @@ export default function StoryEditorPage() { setShowGenerateModal(true); }; + const handleRedrawPage = async () => { + const storedKey = localStorage.getItem("together_api_key"); + if (!storedKey) { + setShowApiModal(true); + return; + } + + const currentPageData = pages[currentPage]; + if (!currentPageData) return; + + setLoadingPageId(currentPage); + + try { + const response = await fetch("/api/add-page", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": storedKey, + }, + body: JSON.stringify({ + storyId: story?.slug, + pageId: currentPageData.dbId, // Add pageId to override existing page + prompt: currentPageData.prompt, + characterImages: currentPageData.characterUploads || [], + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to redraw page"); + } + + const result = await response.json(); + + // Update the current page with the new image + setPages((prevPages) => + prevPages.map((page, index) => + index === currentPage + ? { ...page, image: result.imageUrl } + : page + ) + ); + + toast({ + title: "Page redrawn successfully", + description: "The page has been regenerated with a fresh image.", + duration: 3000, + }); + } catch (error) { + console.error("Error redrawing page:", error); + toast({ + title: "Failed to redraw page", + description: error instanceof Error ? error.message : "Failed to redraw page", + variant: "destructive", + duration: 4000, + }); + } finally { + setLoadingPageId(null); + } + }; + const handleApiKeyClick = () => { setShowApiModal(true); }; @@ -132,8 +194,6 @@ export default function StoryEditorPage() { } }; - - const handleGeneratePage = async (data: { prompt: string; characterFiles?: File[]; @@ -146,6 +206,7 @@ export default function StoryEditorPage() { return; } + // Add new page mode const response = await fetch("/api/add-page", { method: "POST", headers: { @@ -175,16 +236,20 @@ export default function StoryEditorPage() { prompt: data.prompt, characterUploads: data.characterUrls || [], style: story?.style || "noir", + dbId: result.pageId, }, ]); setCurrentPage(pages.length); + setShowGenerateModal(false); } catch (error) { console.error("Error generating page:", error); toast({ title: "Failed to generate page", description: - error instanceof Error ? error.message : "Failed to generate page", + error instanceof Error + ? error.message + : "Failed to generate page", variant: "destructive", duration: 4000, }); @@ -194,7 +259,7 @@ export default function StoryEditorPage() { if (isLoading) { return (
-
Loading story...
+
); } @@ -209,10 +274,7 @@ export default function StoryEditorPage() { return (
- +
setShowInfoSheet(true)} - onNextPage={() => setCurrentPage((prev) => (prev < pages.length - 1 ? prev + 1 : prev))} - onPrevPage={() => setCurrentPage((prev) => (prev > 0 ? prev - 1 : prev))} + onRedrawClick={handleRedrawPage} + onNextPage={() => + setCurrentPage((prev) => + prev < pages.length - 1 ? prev + 1 : prev + ) + } + onPrevPage={() => + setCurrentPage((prev) => (prev > 0 ? prev - 1 : prev)) + } />
diff --git a/app/stories/page.tsx b/app/stories/page.tsx index afcffa5..18ecabb 100644 --- a/app/stories/page.tsx +++ b/app/stories/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Plus, Loader2 } from "lucide-react"; import { Navbar } from "@/components/landing/navbar"; +import { StoryLoader } from "@/components/ui/story-loader"; interface Story { id: string; @@ -51,10 +52,7 @@ export default function StoriesPage() {
-
- -

Loading your comic library...

-
+
); diff --git a/components/editor/comic-canvas.tsx b/components/editor/comic-canvas.tsx index 8cc63c1..9a8be28 100644 --- a/components/editor/comic-canvas.tsx +++ b/components/editor/comic-canvas.tsx @@ -15,11 +15,12 @@ interface PageData { interface ComicCanvasProps { page: PageData; onInfoClick?: () => void; + onRedrawClick?: () => void; onNextPage?: () => void; onPrevPage?: () => void; } -export function ComicCanvas({ page, onInfoClick, onNextPage, onPrevPage }: ComicCanvasProps) { +export function ComicCanvas({ page, onInfoClick, onRedrawClick, onNextPage, onPrevPage }: ComicCanvasProps) { return (
{/* Dot grid background */} @@ -76,6 +77,7 @@ export function ComicCanvas({ page, onInfoClick, onNextPage, onPrevPage }: Comic