Skip to content

Commit 3f36265

Browse files
committed
Merge branch 'develop' into fix-new-popup-issues
2 parents 8ba318e + f08759b commit 3f36265

File tree

21 files changed

+944
-667
lines changed

21 files changed

+944
-667
lines changed

apps/pro-web/app/api/generate-images/route.ts

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GenerateImageRequest } from '@/types'
1+
import type { AspectRatio, GenerateImageRequest } from '@/types'
22
import { openai } from '@ai-sdk/openai'
33
import { GoogleGenAI } from '@google/genai'
44
import { experimental_generateImage as generateImage } from 'ai'
@@ -20,13 +20,36 @@ export const maxDuration = 120 // seconds
2020
*/
2121
const DEFAULT_IMAGE_SIZE = '1024x1024'
2222

23+
/**
24+
* Valid aspect ratios for Gemini API
25+
*/
26+
const VALID_ASPECT_RATIOS: AspectRatio[] = [
27+
'1:1',
28+
'2:3',
29+
'3:2',
30+
'3:4',
31+
'4:3',
32+
'4:5',
33+
'5:4',
34+
'9:16',
35+
'16:9',
36+
'21:9',
37+
]
38+
2339
/**
2440
* Check if model is a Gemini model
2541
*/
2642
const isGeminiModel = (modelId: string): boolean => {
2743
return modelId.startsWith('gemini-')
2844
}
2945

46+
/**
47+
* Validate aspect ratio value
48+
*/
49+
const isValidAspectRatio = (value: string): value is AspectRatio => {
50+
return VALID_ASPECT_RATIOS.includes(value as AspectRatio)
51+
}
52+
3053
/**
3154
* Wrap a promise with a timeout
3255
*/
@@ -42,6 +65,29 @@ const withTimeout = <T>(
4265
])
4366
}
4467

68+
/**
69+
* Extract mime type and base64 data from a data URL or pure base64 string
70+
* Returns { mimeType, data } where data is the pure base64 string
71+
*/
72+
const parseBase64Image = (
73+
base64String: string,
74+
): { mimeType: string; data: string } => {
75+
if (base64String.startsWith('data:')) {
76+
const match = base64String.match(/^data:([^;]+);base64,(.+)$/)
77+
if (match) {
78+
return {
79+
mimeType: match[1],
80+
data: match[2],
81+
}
82+
}
83+
}
84+
// Default to image/png for pure base64 strings
85+
return {
86+
mimeType: 'image/png',
87+
data: base64String,
88+
}
89+
}
90+
4591
/**
4692
* Image generation endpoint
4793
*/
@@ -59,6 +105,8 @@ export async function POST(req: NextRequest) {
59105
modelId = body.modelId
60106
previousImage = body.previousImage
61107
editMode = body.editMode
108+
const referenceImages = body.referenceImages
109+
const aspectRatio = body.aspectRatio
62110

63111
// * Validate request parameters
64112
if (!prompt || !modelId) {
@@ -67,6 +115,13 @@ export async function POST(req: NextRequest) {
67115
return NextResponse.json({ error }, { status: 400 })
68116
}
69117

118+
// * Validate aspect ratio if provided
119+
if (aspectRatio && !isValidAspectRatio(aspectRatio)) {
120+
const error = `Invalid aspect ratio: ${aspectRatio}. Valid values are: ${VALID_ASPECT_RATIOS.join(', ')}`
121+
console.error(`${error} [requestId=${requestId}]`)
122+
return NextResponse.json({ error }, { status: 400 })
123+
}
124+
70125
// * Start timing
71126
const startstamp = performance.now()
72127

@@ -91,27 +146,42 @@ export async function POST(req: NextRequest) {
91146
// Add prompt
92147
contentParts.push({ text: prompt })
93148

149+
// Add reference images if provided
150+
if (referenceImages && referenceImages.length > 0) {
151+
for (const refImage of referenceImages) {
152+
const { mimeType, data } = parseBase64Image(refImage)
153+
contentParts.push({
154+
inlineData: {
155+
mimeType,
156+
data,
157+
},
158+
})
159+
}
160+
}
161+
94162
// Add previous image if in edit mode
95163
if (editMode && previousImage) {
96-
// Remove data URL prefix if present
97-
const base64Data = previousImage.includes('base64,')
98-
? previousImage.split('base64,')[1]
99-
: previousImage
164+
const { mimeType, data } = parseBase64Image(previousImage)
100165

101166
contentParts.push({
102167
inlineData: {
103-
mimeType: 'image/png',
104-
data: base64Data,
168+
mimeType,
169+
data,
105170
},
106171
})
107172
}
108173

109174
const response = await googleAI.models.generateContent({
110175
model: modelId,
111176
contents: contentParts,
112-
})
113-
114-
// Extract image from response
177+
config: {
178+
responseModalities: ['IMAGE'],
179+
imageConfig: {
180+
imageSize: '2K',
181+
...(aspectRatio && { aspectRatio }),
182+
},
183+
},
184+
}) // Extract image from response
115185
let imageBase64: string | undefined
116186

117187
// Access the candidates array from the response

apps/pro-web/app/api/media/templates/route.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { readdir } from 'node:fs/promises'
22
import { join } from 'node:path'
3-
import {
4-
type Template,
5-
parseTemplateFilename,
6-
} from '@/lib/helpers/workspace/media'
3+
import { parseTemplateFilename } from '@/lib/helpers/workspace/media'
74
import { logErrorToSentry } from '@/lib/sentry'
5+
import type { Template } from '@/types'
86
import { NextResponse } from 'next/server'
97

108
/**
Lines changed: 10 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -1,225 +1,43 @@
11
'use client'
22

3-
import type { Template } from '@/lib/helpers/workspace/media'
43
import { useWorkspaceMedia } from '@/lib/hooks/use-workspace-media'
5-
import type { LucideProps } from 'lucide-react'
6-
import type React from 'react'
7-
import { useEffect, useState } from 'react'
4+
import { useEffect } from 'react'
85
import { MediaCanvas } from './ui/canvas'
96
import { ReferenceImagesPanel } from './ui/reference-images'
107
import { MediaSidebar } from './ui/sidebar'
118
import { MediaWizards } from './wizards'
12-
import type { FrameSize } from './wizards/steps'
139

1410
export function MediaWorkspace() {
15-
const [referenceImages, setReferenceImages] = useState<string[]>([])
16-
const [currentVersion, setCurrentVersion] = useState(0)
17-
const [allTemplates, setAllTemplates] = useState<Template[]>([])
18-
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true)
19-
2011
// Workspace media context
21-
const {
22-
dialogStep,
23-
generatedImage,
24-
imageError,
25-
isGeneratingImage,
26-
openTemplates,
27-
resetImageGeneration,
28-
selectedSize,
29-
selectedTemplate,
30-
setDialogStep,
31-
setSelectedSize,
32-
setSelectedTemplate,
33-
setShowVersionHistory,
34-
showVersionHistory,
35-
} = useWorkspaceMedia()
36-
37-
// TODO: Migrate to TanStack Query
38-
// Load templates from API on mount
39-
useEffect(() => {
40-
async function fetchTemplates() {
41-
try {
42-
setIsLoadingTemplates(true)
43-
const response = await fetch('/api/media/templates')
44-
if (!response.ok) {
45-
throw new Error('Failed to fetch templates')
46-
}
47-
const templates: Template[] = await response.json()
48-
setAllTemplates(templates)
49-
} catch (error) {
50-
console.error('Error fetching templates:', error)
51-
setAllTemplates([])
52-
} finally {
53-
setIsLoadingTemplates(false)
54-
}
55-
}
56-
57-
fetchTemplates()
58-
}, [])
59-
60-
// Reference images management
61-
const removeReferenceImage = (index: number) => {
62-
setReferenceImages(referenceImages.filter((_, i) => i !== index))
63-
}
64-
65-
const addReferenceImage = (imageUrl: string) => {
66-
if (referenceImages.length < 4) {
67-
setReferenceImages([...referenceImages, imageUrl])
68-
}
69-
}
12+
const { generatedImage, resetImageGeneration, selectedTemplate } =
13+
useWorkspaceMedia()
7014

71-
const addMultipleReferenceImages = (imageUrls: string[]) => {
72-
const availableSlots = 4 - referenceImages.length
73-
const imagesToAdd = imageUrls.slice(0, availableSlots)
74-
setReferenceImages([...referenceImages, ...imagesToAdd])
75-
}
76-
77-
const addImageFromLibrary = (image: string) => {
78-
if (referenceImages.length < 4) {
79-
setReferenceImages([...referenceImages, image])
80-
}
81-
}
82-
83-
// Reset generated image when template or size changes
84-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on template/size change
15+
// Reset generated image when template changes
16+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on template change
8517
useEffect(() => {
8618
if (generatedImage) {
8719
resetImageGeneration()
8820
}
89-
}, [selectedTemplate?.id, selectedSize?.id])
21+
}, [selectedTemplate?.id])
9022

9123
return (
9224
<div className="flex flex-col lg:flex-row size-full bg-background min-h-0">
9325
{/* Sidebar Media: Templates, Social Media and Brand Kit */}
94-
<MediaSidebar onTemplatesClick={openTemplates} />
26+
<MediaSidebar />
9527

9628
{/* Main Media Content */}
9729
<div className="flex-1 flex flex-col overflow-y-auto overflow-x-hidden min-h-0">
9830
<div className="flex-1 flex flex-col px-3 sm:px-4 lg:px-8 py-3 lg:py-5 gap-2 sm:gap-3 lg:gap-4">
9931
{/* Canvas Area */}
100-
<MediaCanvas
101-
selectedTemplate={selectedTemplate}
102-
selectedSize={selectedSize}
103-
generatedImage={generatedImage}
104-
isGeneratingImage={isGeneratingImage}
105-
imageError={imageError}
106-
showVersionHistory={showVersionHistory}
107-
currentVersion={currentVersion}
108-
versions={versions}
109-
onResetImage={resetImageGeneration}
110-
onCloseVersionHistory={() => setShowVersionHistory(false)}
111-
onVersionChange={setCurrentVersion}
112-
/>
32+
<MediaCanvas />
11333
</div>
11434
</div>
11535

11636
{/* Right Sidebar - Reference Images */}
117-
<ReferenceImagesPanel
118-
referenceImages={referenceImages}
119-
onRemoveImage={removeReferenceImage}
120-
onAddImage={addReferenceImage}
121-
onAddMultipleImages={addMultipleReferenceImages}
122-
/>
37+
<ReferenceImagesPanel />
12338

12439
{/* Media Selection Wizards */}
125-
<MediaWizards
126-
dialogStep={dialogStep}
127-
frameSizes={frameSizes}
128-
allTemplates={allTemplates}
129-
isLoadingTemplates={isLoadingTemplates}
130-
libraryImages={libraryImages}
131-
referenceImages={referenceImages}
132-
onDialogClose={() => setDialogStep(null)}
133-
onSizeSelect={setSelectedSize}
134-
onTemplateSelect={setSelectedTemplate}
135-
onImageSelect={addImageFromLibrary}
136-
/>
40+
<MediaWizards />
13741
</div>
13842
)
13943
}
140-
141-
const frameSizes: FrameSize[] = [
142-
{
143-
id: 'square',
144-
name: 'Square',
145-
ratio: '1:1',
146-
description:
147-
'Instagram Post, LinkedIn Ad/Post/Video, Twitter/X Ad, Facebook Post',
148-
aspectClass: 'aspect-square',
149-
},
150-
{
151-
id: 'landscape',
152-
name: 'Landscape',
153-
ratio: '16:9',
154-
description: 'Twitter/X Post',
155-
aspectClass: 'aspect-video',
156-
},
157-
{
158-
id: 'portrait',
159-
name: 'Portrait',
160-
ratio: '9:16',
161-
description: 'Instagram Story/Video/Reel, TikTok Video',
162-
aspectClass: 'aspect-[9/16]',
163-
},
164-
{
165-
id: 'classic',
166-
name: 'Classic',
167-
ratio: '4:5',
168-
description: 'Instagram Post',
169-
aspectClass: 'aspect-[4/5]',
170-
},
171-
{
172-
id: 'ultrawide',
173-
name: 'Ultra Wide',
174-
ratio: '21:11',
175-
description: 'Facebook Ad',
176-
aspectClass: 'aspect-[21/11]',
177-
},
178-
{
179-
id: 'poster',
180-
name: 'Poster',
181-
ratio: '2:3',
182-
description: 'Pinterest Pin',
183-
aspectClass: 'aspect-[2/3]',
184-
},
185-
{
186-
id: 'photo',
187-
name: 'Photo',
188-
ratio: '5:4',
189-
description: 'Facebook Post',
190-
aspectClass: 'aspect-[5/4]',
191-
},
192-
{
193-
id: 'wide',
194-
name: 'Wide',
195-
ratio: '21:9',
196-
description: 'Banner, Header',
197-
aspectClass: 'aspect-[21/9]',
198-
},
199-
]
200-
201-
const libraryImages = [
202-
'https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=200',
203-
'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=200',
204-
'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=200',
205-
'https://images.unsplash.com/photo-1579546929662-711aa81148cf?w=200',
206-
'https://images.unsplash.com/photo-1501139083538-0139583c060f?w=200',
207-
'https://images.unsplash.com/photo-1509048191080-d2984bad6ae5?w=200',
208-
'https://images.unsplash.com/photo-1501139083538-0139583c060f?w=200',
209-
'https://images.unsplash.com/photo-1509048191080-d2984bad6ae5?w=200',
210-
]
211-
212-
const versions = [
213-
{ id: 1, thumbnail: 'version-1' },
214-
{ id: 2, thumbnail: 'version-2' },
215-
{ id: 3, thumbnail: 'version-3' },
216-
]
217-
218-
export interface MediaCategory {
219-
id: string
220-
name: string
221-
icon: React.ForwardRefExoticComponent<
222-
Omit<LucideProps, 'ref'> & React.RefAttributes<SVGSVGElement>
223-
>
224-
color: string
225-
}

0 commit comments

Comments
 (0)