From e566dec66f2e1b58720d22960c7545e71adfbd1d Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 1 Feb 2026 13:13:26 +0000 Subject: [PATCH] 1.0 --- .../db/seeders/20260201000000-openai-key.js | 19 + backend/src/routes/image_generations.js | 5 + backend/src/services/image_generations.js | 108 ++++- frontend/src/pages/index.tsx | 380 +++++++++++------- .../image_generationsSlice.ts | 42 +- 5 files changed, 399 insertions(+), 155 deletions(-) create mode 100644 backend/src/db/seeders/20260201000000-openai-key.js diff --git a/backend/src/db/seeders/20260201000000-openai-key.js b/backend/src/db/seeders/20260201000000-openai-key.js new file mode 100644 index 0000000..ef3e7f3 --- /dev/null +++ b/backend/src/db/seeders/20260201000000-openai-key.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const now = new Date(); + return queryInterface.bulkInsert('secrets', [{ + id: '00000000-0000-0000-0000-000000000001', + key_name: 'OPENAPI_API_KEY', + value: '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', + is_active: true, + createdAt: now, + updatedAt: now, + }], {}); + }, + + down: async (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('secrets', { key_name: 'OPENAPI_API_KEY' }, {}); + } +}; diff --git a/backend/src/routes/image_generations.js b/backend/src/routes/image_generations.js index 011a056..b6a55ce 100644 --- a/backend/src/routes/image_generations.js +++ b/backend/src/routes/image_generations.js @@ -17,6 +17,11 @@ const { router.use(checkCrudPermissions('image_generations')); +router.post('/generate', wrapAsync(async (req, res) => { + const result = await Image_generationsService.generate(req.body, req.currentUser); + res.status(200).send(result); +})); + /** * @swagger diff --git a/backend/src/services/image_generations.js b/backend/src/services/image_generations.js index 10f0204..180e79a 100644 --- a/backend/src/services/image_generations.js +++ b/backend/src/services/image_generations.js @@ -6,12 +6,108 @@ const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); const stream = require('stream'); +const SecretsDBApi = require('../db/api/secrets'); +const fs = require('fs'); +const path = require('path'); - - - +const ensureDirectoryExistence = (filePath) => { + const dirname = path.dirname(filePath); + if (fs.existsSync(dirname)) { + return true; + } + ensureDirectoryExistence(dirname); + fs.mkdirSync(dirname); +} module.exports = class Image_generationsService { + static async generate(payload, currentUser) { + const { prompt, style = 'normal', num_images = 2 } = payload; + + // 1. Fetch API Key + const secret = await SecretsDBApi.findBy({ key_name: 'OPENAPI_API_KEY' }); + if (!secret || !secret.value) { + throw new Error('OPENAPI_API_KEY not found in Secrets'); + } + const apiKey = secret.value; + + // 2. Call OpenAI + const response = await axios.post('https://api.openai.com/v1/images/generations', { + prompt: `${prompt} in ${style} style`, + n: num_images, + size: '1024x1024', + model: 'dall-e-2' + }, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + + const urls = response.data.data.map(img => img.url); + + // 3. Save to DB + const transaction = await db.sequelize.transaction(); + try { + const generation = await db.image_generations.create({ + title: prompt, + status: 'completed', + generated_at: new Date(), + finished_at: new Date(), + requesterId: currentUser.id, + createdById: currentUser.id, + updatedById: currentUser.id, + }, { transaction }); + + const imageRecords = []; + const savedUrls = []; + for (const url of urls) { + const image = await db.images.create({ + filename: `generated-${Date.now()}.png`, + format: 'png', + generationId: generation.id, + createdById: currentUser.id, + updatedById: currentUser.id, + }, { transaction }); + + const imgResponse = await axios.get(url, { responseType: 'arraybuffer' }); + const fileName = `generated-${Date.now()}-${Math.random().toString(36).substring(7)}.png`; + const relativePath = path.join('generations', fileName); + const filePath = path.join(config.uploadDir, relativePath); + ensureDirectoryExistence(filePath); + fs.writeFileSync(filePath, imgResponse.data); + + await db.file.create({ + belongsTo: 'images', + belongsToColumn: 'file', + belongsToId: image.id, + name: fileName, + sizeInBytes: imgResponse.data.length, + privateUrl: relativePath, + createdById: currentUser.id, + updatedById: currentUser.id, + }, { transaction }); + + imageRecords.push(image); + savedUrls.push(`/api/file/download?privateUrl=${encodeURIComponent(relativePath)}`); + } + + await db.generation_histories.create({ + timestamp: new Date(), + action: 'generation_completed', + details: JSON.stringify(savedUrls), + generationId: generation.id, + createdById: currentUser.id, + updatedById: currentUser.id, + }, { transaction }); + + await transaction.commit(); + return { generation, images: imageRecords, urls: savedUrls }; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { @@ -131,8 +227,4 @@ module.exports = class Image_generationsService { throw error; } } - - -}; - - +}; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 90062f5..e450e45 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -5,162 +5,252 @@ import Head from 'next/head'; import Link from 'next/link'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { generate, resetGeneratedImages, fetch } from '../stores/image_generations/image_generationsSlice'; +import { mdiAutoFix, mdiDownload, mdiDelete, mdiHistory, mdiPlus, mdiClockOutline } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +import FormField from '../components/FormField'; +import LoadingSpinner from '../components/LoadingSpinner'; +export default function AIImageGenerator() { + const dispatch = useAppDispatch(); + const { generatedImages, isGenerating, image_generations, refetch } = useAppSelector((state) => state.image_generations); + const { currentUser } = useAppSelector((state) => state.auth); + const [prompt, setPrompt] = useState(''); + const [style, setStyle] = useState('normal'); + const [numImages, setNumImages] = useState(2); + const [file, setFile] = useState(null); -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'App Draft' - - // Fetch Pexels image/video useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); + dispatch(fetch({ query: '?limit=5' })); + }, [dispatch, refetch]); - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } + const handleGenerate = () => { + if (!prompt) return; + dispatch(generate({ prompt, style, num_images: numImages })); }; - return ( -
- - {getPageTitle('Starter Page')} - + const handleDownload = (url: string, format: string) => { + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `generated-image.${format}`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - + return ( +
+ + {getPageTitle('GPT Image 2 Generator')} + - - + {/* Navigation / Header */} + + +
+ {/* Hero Section */} +
+

+ AI Image Generator +

+

+ Transform your words into stunning visual masterpieces with GPT Image 2 4.0. + Fast, high-quality, and unlimited creativity. +

+
+ + {/* Generator Section */} +
+ {/* Input Side */} +
+ + +