From 7feea26188bcc42edceaa449b953c326fc046432 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 22 Dec 2025 08:58:22 -0500 Subject: [PATCH] feat: upgrade MycroZine generator to use full standalone API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use /api/outline for AI-generated 8-page outlines via Gemini - Use /api/generate-page for individual page image generation - Use /api/regenerate-page for page regeneration with feedback - Use /api/print-layout for 300 DPI print-ready layout generation - Remove legacy local generation functions - Add proper error handling and API response parsing - Include folding instructions in completion message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/shapes/MycroZineGeneratorShapeUtil.tsx | 244 ++++++++++++--------- 1 file changed, 136 insertions(+), 108 deletions(-) diff --git a/src/shapes/MycroZineGeneratorShapeUtil.tsx b/src/shapes/MycroZineGeneratorShapeUtil.tsx index c030b3d..0b2db09 100644 --- a/src/shapes/MycroZineGeneratorShapeUtil.tsx +++ b/src/shapes/MycroZineGeneratorShapeUtil.tsx @@ -11,7 +11,13 @@ import React, { useState, useRef, useEffect } from "react" import { StandardizedToolWrapper } from "@/components/StandardizedToolWrapper" import { usePinnedToView } from "@/hooks/usePinnedToView" import { useMaximize } from "@/hooks/useMaximize" -// Note: Image generation now uses zine.jeffemmett.com API which proxies through RunPod +// Uses zine.jeffemmett.com API for full zine generation workflow: +// - /api/outline - AI-generated 8-page outlines via Gemini +// - /api/generate-page - Individual page image generation via RunPod +// - /api/regenerate-page - Page regeneration with feedback +// - /api/print-layout - 300 DPI print-ready layout generation + +const ZINE_API_BASE = 'https://zine.jeffemmett.com' // ============================================================================ // Types @@ -262,7 +268,7 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil { if (!shape.props.topic) { updateProps({ error: 'Please enter a topic first' }) @@ -272,26 +278,58 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil ({ - pageNumber: index + 1, - type: template.type as 'cover' | 'content' | 'cta', - title: index === 0 ? shape.props.topic.toUpperCase() : `Page ${index + 1}`, - subtitle: template.description, - keyPoints: [], + // Call the standalone zine API for AI-generated outline + const response = await fetch(`${ZINE_API_BASE}/api/outline`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + topic: shape.props.topic, + style: shape.props.style, + tone: shape.props.tone, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) as { error?: string } + throw new Error(errorData.error || `API error: ${response.status}`) + } + + const data = await response.json() as { + id: string + outline: Array<{ + pageNumber: number + type: string + title: string + keyPoints: string[] + imagePrompt: string + }> + } + + // Convert API response to our outline format + const outline: ZinePageOutline[] = data.outline.map((page) => ({ + pageNumber: page.pageNumber, + type: page.type as 'cover' | 'content' | 'cta', + title: page.title, + subtitle: undefined, + keyPoints: page.keyPoints, hashtags: [], - imagePrompt: `${STYLES[shape.props.style]}. ${TONES[shape.props.tone]}. ${template.description} for ${shape.props.topic}.`, + imagePrompt: page.imagePrompt, })) + // Store the zine ID from the API for later use + const newZineId = data.id + // Add assistant message with outline summary - addMessage('assistant', `Great! I've created an outline for your "${shape.props.topic}" zine:\n\n${outline.map(p => `Page ${p.pageNumber}: ${p.title}`).join('\n')}\n\nClick "Generate Drafts" when you're ready to create the pages!`) + addMessage('assistant', `Great! I've created an AI-generated outline for your "${shape.props.topic}" zine:\n\n${outline.map(p => `Page ${p.pageNumber}: ${p.title}\n • ${p.keyPoints.slice(0, 2).join('\n • ')}`).join('\n\n')}\n\nClick "Generate Drafts" when you're ready to create the pages!`) updateProps({ + zineId: newZineId, contentOutline: outline, - title: shape.props.topic.toUpperCase(), + title: outline[0]?.title || shape.props.topic.toUpperCase(), isLoading: false, }) } catch (err) { + console.error('Outline generation error:', err) updateProps({ isLoading: false, error: `Failed to generate outline: ${err instanceof Error ? err.message : String(err)}`, @@ -323,7 +361,7 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil { if (!shape.props.contentOutline) { await generateOutline() @@ -338,6 +376,7 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil ({})) as { error?: string } + throw new Error(errorData.error || `API error: ${response.status}`) + } + + const data = await response.json() as { pageNumber: number; imageUrl: string; success: boolean } + + // Use the API-returned image URL + const imageUrl = `${ZINE_API_BASE}${data.imageUrl}` generatedPages.push({ pageNumber: i + 1, imageUrl, outline: pageOutline, - generationPrompt: prompt, + generationPrompt: pageOutline.imagePrompt, timestamp: Date.now(), version: 1, }) @@ -374,6 +428,7 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil f.approved || f.feedbackText.length > 0 ) - // Move to finalization + // Move to finalization - regenerate pages with feedback using API const startFinalization = async () => { const pagesNeedingUpdate = shape.props.pageFeedback.filter(f => !f.approved && f.feedbackText) @@ -438,6 +493,7 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil p.pageNumber === feedback.pageNumber) if (!originalPage) continue - // Regenerate with feedback incorporated - const prompt = `${originalPage.generationPrompt}\n\nIMPORTANT REVISIONS: ${feedback.feedbackText}` - const imageUrl = await generatePageImage(prompt, feedback.pageNumber) + // Call the standalone API to regenerate with feedback + const response = await fetch(`${ZINE_API_BASE}/api/regenerate-page`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + zineId: zineId, + pageNumber: feedback.pageNumber, + currentOutline: originalPage.outline, + feedback: feedback.feedbackText, + style: shape.props.style, + tone: shape.props.tone, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) as { error?: string } + throw new Error(errorData.error || `API error: ${response.status}`) + } + + const data = await response.json() as { + imageUrl: string + updatedOutline: ZinePageOutline + } + + const imageUrl = `${ZINE_API_BASE}${data.imageUrl}` finalPages[feedback.pageNumber - 1] = { ...originalPage, imageUrl, - generationPrompt: prompt, + outline: data.updatedOutline || originalPage.outline, + generationPrompt: data.updatedOutline?.imagePrompt || originalPage.generationPrompt, timestamp: Date.now(), version: originalPage.version + 1, } } catch (err) { console.error(`Failed to regenerate page ${feedback.pageNumber}:`, err) + updateProps({ + error: `Failed to regenerate page ${feedback.pageNumber}: ${err instanceof Error ? err.message : String(err)}`, + }) } } @@ -470,23 +552,45 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil { updateProps({ isLoading: true, error: null }) try { - // In actual implementation, this would call the mycro-zine layout generator - // For now, show a success message - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) - const printUrl = `/output/${shape.props.zineId}_print_${timestamp}.png` + const zineId = shape.props.zineId + const zineName = (shape.props.title || shape.props.topic).slice(0, 20).replace(/[^a-zA-Z0-9]/g, '_') + + // Call the standalone API for print layout generation + const response = await fetch(`${ZINE_API_BASE}/api/print-layout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + zineId: zineId, + zineName: zineName, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) as { error?: string } + throw new Error(errorData.error || `API error: ${response.status}`) + } + + const data = await response.json() as { + success: boolean + printLayoutUrl: string + filename: string + } + + const printUrl = `${ZINE_API_BASE}${data.printLayoutUrl}` updateProps({ printLayoutUrl: printUrl, isLoading: false, }) - addMessage('assistant', `Print layout generated! Your zine is ready to download and print.`) + addMessage('assistant', `🖨️ Print layout generated at 300 DPI!\n\nDownload and print on 8.5" × 11" paper (landscape).\n\nFolding instructions:\n1. Fold in half lengthwise (hotdog fold)\n2. Fold in half again\n3. Fold once more to create booklet\n4. Unfold and cut center slit\n5. Refold and push ends together\n6. Pages should now be in order 1-8!`) } catch (err) { + console.error('Print layout error:', err) updateProps({ isLoading: false, error: `Failed to generate print layout: ${err instanceof Error ? err.message : String(err)}`, @@ -1035,83 +1139,7 @@ export class MycroZineGeneratorShape extends BaseBoxShapeUtil 0 ? `Key points: ${pageOutline.keyPoints.join(', ')}` : ''} -${pageOutline.hashtags.length > 0 ? `Hashtags: ${pageOutline.hashtags.join(' ')}` : ''} - -Style: ${styleDesc} -Tone: ${toneDesc} - -High contrast black and white with neon green accent highlights. Xerox texture, DIY cut-and-paste collage aesthetic, rough edges, rebellious feel.` -} - -interface GenerateImageResponse { - success: boolean - imageData?: string - mimeType?: string - error?: string -} - -async function generatePageImage(prompt: string, pageNumber: number): Promise { - console.log(`🍄 Generating page ${pageNumber} via RunPod proxy...`) - console.log(`📝 Prompt preview:`, prompt.substring(0, 100) + '...') - - // Use the mycro-zine API which proxies through RunPod (US-based, bypasses geo-restrictions) - const ZINE_API_URL = 'https://zine.jeffemmett.com/api/generate-image' - - try { - const response = await fetch(ZINE_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ prompt }), - }) - - if (!response.ok) { - let errorMessage = `HTTP ${response.status}` - try { - const errorData = await response.json() as { error?: string } - if (errorData.error) errorMessage = errorData.error - } catch { - // Ignore JSON parse errors - } - console.error(`❌ API error for page ${pageNumber}:`, response.status, errorMessage) - throw new Error(errorMessage) - } - - const data: GenerateImageResponse = await response.json() - - if (data.success && data.imageData) { - console.log(`✅ Page ${pageNumber} generated via RunPod proxy`) - // Convert base64 to data URL - return `data:${data.mimeType || 'image/png'};base64,${data.imageData}` - } - - throw new Error('No image data in response') - } catch (error) { - console.error(`❌ Generation failed for page ${pageNumber}:`, error) - // Fallback to placeholder - return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}+%28Generation+Failed%29` - } -} - -// Removed direct Gemini API functions - now using zine.jeffemmett.com proxy - +// Spawn page images on the canvas in a 4x2 grid async function spawnPageOnCanvas( editor: any, imageUrl: string,