{/* Style/Tone selectors */}
{/* Chat messages */}
{shape.props.ideationMessages.length === 0 && (
What topic would you like to make a zine about?
)}
{shape.props.ideationMessages.map((msg) => (
{msg.content}
))}
{/* Input area */}
setInputValue(e.target.value)}
onKeyDown={(e) => {
e.stopPropagation()
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}}
onPointerDown={(e) => e.stopPropagation()}
placeholder={shape.props.topic ? "Add details or feedback..." : "Enter your zine topic..."}
style={inputStyle}
/>
{/* Action buttons */}
{!shape.props.contentOutline && shape.props.topic && (
)}
{shape.props.contentOutline && (
)}
)
// ========== DRAFTS PHASE ==========
const renderDraftsPhase = () => (
š
Generating Drafts...
{/* Progress bar */}
Page {shape.props.currentGeneratingPage}/8
{/* Page status list */}
{Array.from({ length: 8 }, (_, i) => i + 1).map(pageNum => {
const isComplete = shape.props.draftPages.some(p => p.pageNumber === pageNum)
const isGenerating = shape.props.currentGeneratingPage === pageNum
return (
{isComplete ? 'ā' : pageNum}
)
})}
)
// ========== FEEDBACK PHASE ==========
const renderFeedbackPhase = () => {
const [selectedPage, setSelectedPage] = useState(null)
const [feedbackInput, setFeedbackInput] = useState('')
return (
Review your drafts. Approve pages or add feedback for revision.
{/* Page grid */}
{shape.props.draftPages.map((page) => {
const feedback = shape.props.pageFeedback.find(f => f.pageNumber === page.pageNumber)
const isApproved = feedback?.approved
const hasFeedback = feedback?.feedbackText
return (
setSelectedPage(page.pageNumber)}
onPointerDown={(e) => e.stopPropagation()}
style={{
position: 'relative',
borderRadius: '4px',
overflow: 'hidden',
cursor: 'pointer',
border: selectedPage === page.pageNumber
? `2px solid ${MycroZineGeneratorShape.PRIMARY_COLOR}`
: isApproved
? '2px solid #4ade80'
: hasFeedback
? '2px solid #fbbf24'
: '2px solid #333',
}}
>
{page.pageNumber} {isApproved ? 'ā' : hasFeedback ? 'ā' : ''}
)
})}
{/* Selected page actions */}
{selectedPage && (
Page {selectedPage}
setFeedbackInput(e.target.value)}
onPointerDown={(e) => e.stopPropagation()}
placeholder="Describe changes needed..."
style={inputStyle}
/>
{feedbackInput && (
)}
)}
{/* Continue button */}
)
}
// ========== FINALIZING PHASE ==========
const renderFinalizingPhase = () => (
š
Applying Feedback...
Regenerating {shape.props.pageFeedback.filter(f => !f.approved && f.feedbackText).length} pages
{shape.props.currentGeneratingPage > 0 && (
Currently updating page {shape.props.currentGeneratingPage}...
)}
)
// ========== COMPLETE PHASE ==========
const renderCompletePhase = () => {
const pages = shape.props.finalPages.length > 0 ? shape.props.finalPages : shape.props.draftPages
return (
š
Zine Complete!
"{shape.props.title || shape.props.topic}"
{/* Print preview - 2x4 grid */}
{pages.map((page) => (

))}
{/* Action buttons */}
)
}
// Phase indicator
const phaseLabels: Record = {
ideation: '1. Ideation',
drafts: '2. Drafts',
feedback: '3. Feedback',
finalizing: '4. Finalizing',
complete: '5. Complete',
}
return (
š MycroZine
{phaseLabels[shape.props.phase]}
}
>
{/* Error display */}
{shape.props.error && (
{shape.props.error}
)}
{renderPhaseContent()}
)
}
override indicator(shape: IMycroZineGenerator) {
return (
)
}
}
// ============================================================================
// Helper Functions
// ============================================================================
function buildImagePrompt(
pageOutline: ZinePageOutline,
topic: string,
style: ZineStyle,
tone: ZineTone
): string {
const styleDesc = STYLES[style]
const toneDesc = TONES[tone]
return `Punk zine page ${pageOutline.pageNumber}/8 for "${topic}".
${pageOutline.imagePrompt}
Title text: "${pageOutline.title}"
${pageOutline.subtitle ? `Subtitle: "${pageOutline.subtitle}"` : ''}
${pageOutline.keyPoints.length > 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
async function spawnPageOnCanvas(
editor: any,
imageUrl: string,
index: number,
parentShape: IMycroZineGenerator
): Promise {
try {
// Calculate position in a 4x2 grid to the right of the generator
const col = index % 4
const row = Math.floor(index / 4)
const spacing = 20
const pageWidth = 200
const pageHeight = 300
const x = parentShape.props.w + 50 + col * (pageWidth + spacing)
const y = row * (pageHeight + spacing)
// Get parent shape position
const parentBounds = editor.getShapePageBounds(parentShape.id)
if (!parentBounds) return undefined
const shapeId = createShapeId()
// Create an image shape on the canvas
editor.createShape({
id: shapeId,
type: 'image',
x: parentBounds.x + x,
y: parentBounds.y + y,
props: {
w: pageWidth,
h: pageHeight,
url: imageUrl,
},
})
return shapeId
} catch (err) {
console.error('Failed to spawn page on canvas:', err)
return undefined
}
}
// ============================================================================
// Styles
// ============================================================================
const inputStyle: React.CSSProperties = {
flex: 1,
padding: '10px 12px',
backgroundColor: '#1a1a1a',
border: '1px solid #333',
borderRadius: '6px',
color: '#fff',
fontSize: '13px',
outline: 'none',
}
const buttonStyle: React.CSSProperties = {
padding: '10px 16px',
backgroundColor: '#00ff00',
border: 'none',
borderRadius: '6px',
color: '#000',
fontSize: '13px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'opacity 0.15s',
}
const selectStyle: React.CSSProperties = {
flex: 1,
padding: '8px 12px',
backgroundColor: '#1a1a1a',
border: '1px solid #333',
borderRadius: '6px',
color: '#fff',
fontSize: '12px',
cursor: 'pointer',
}