From 70b32b34db3f6ed2ff3bfffdf2667eaa2028753a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 2 Mar 2026 03:48:38 +0000 Subject: [PATCH] 1 --- backend/src/db/api/songs.js | 12 +- frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/menuAside.ts | 7 +- frontend/src/pages/_app.tsx | 57 +--- frontend/src/pages/index.tsx | 227 +++++--------- frontend/src/pages/studio/index.tsx | 397 +++++++++++++++++++++++++ 7 files changed, 501 insertions(+), 209 deletions(-) create mode 100644 frontend/src/pages/studio/index.tsx diff --git a/backend/src/db/api/songs.js b/backend/src/db/api/songs.js index 9d38bd1..59ef6b0 100644 --- a/backend/src/db/api/songs.js +++ b/backend/src/db/api/songs.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -431,6 +430,14 @@ module.exports = class SongsDBApi { }, + { + model: db.media_assets, + as: 'media_assets_song', + include: [{ + model: db.file, + as: 'file_blob', + }] + } ]; @@ -688,5 +695,4 @@ module.exports = class SongsDBApi { } -}; - +}; \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index eb155e3..1986306 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({ ) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 5840292..3fb26ad 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -2,6 +2,11 @@ import * as icon from '@mdi/js'; import { MenuAsideItem } from './interfaces' const menuAside: MenuAsideItem[] = [ + { + href: '/studio', + label: 'Music Studio', + icon: icon.mdiMusicNotePlus, + }, { href: '/dashboard', icon: icon.mdiViewDashboardOutline, @@ -152,4 +157,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 2f52f84..05cac9e 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -8,14 +8,10 @@ import { Provider } from 'react-redux'; import '../css/main.css'; import axios from 'axios'; import { baseURLApi } from '../config'; -import { useRouter } from 'next/router'; import ErrorBoundary from "../components/ErrorBoundary"; import DevModeBadge from '../components/DevModeBadge'; -import 'intro.js/introjs.css'; import { appWithTranslation } from 'next-i18next'; import '../i18n'; -import IntroGuide from '../components/IntroGuide'; -import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps'; // Initialize axios axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API @@ -35,10 +31,6 @@ type AppPropsWithLayout = AppProps & { function MyApp({ Component, pageProps }: AppPropsWithLayout) { // Use the layout defined at the page level, if available const getLayout = Component.getLayout || ((page) => page); - const router = useRouter(); - const [stepsEnabled, setStepsEnabled] = React.useState(false); - const [stepName, setStepName] = React.useState(''); - const [steps, setSteps] = React.useState([]); axios.interceptors.request.use( config => { @@ -111,44 +103,6 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { return () => window.removeEventListener('message', handleMessage); }, []); - React.useEffect(() => { - // Tour is disabled by default in generated projects. - return; - const isCompleted = (stepKey: string) => { - return localStorage.getItem(`completed_${stepKey}`) === 'true'; - }; - if (router.pathname === '/login' && !isCompleted('loginSteps')) { - setSteps(loginSteps); - setStepName('loginSteps'); - setStepsEnabled(true); - }else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) { - setTimeout(() => { - setSteps(appSteps); - setStepName('appSteps'); - setStepsEnabled(true); - }, 1000); - } else if (router.pathname === '/users/users-list' && !isCompleted('usersSteps')) { - setTimeout(() => { - setSteps(usersSteps); - setStepName('usersSteps'); - setStepsEnabled(true); - }, 1000); - } else if (router.pathname === '/roles/roles-list' && !isCompleted('rolesSteps')) { - setTimeout(() => { - setSteps(rolesSteps); - setStepName('rolesSteps'); - setStepsEnabled(true); - }, 1000); - } else { - setSteps([]); - setStepsEnabled(false); - } - }, [router.pathname]); - - const handleExit = () => { - setStepsEnabled(false); - }; - const title = 'AI Music Studio Admin' const description = "Single-admin AI music studio to generate songs with lyrics, AI vocals, playback, and downloads." const url = "https://flatlogic.com/" @@ -185,17 +139,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - - {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && } + + {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && } )} ) } -export default appWithTranslation(MyApp); +export default appWithTranslation(MyApp); \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index fab6045..9590ae2 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,103 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; 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 { mdiMusic, mdiAccountKey, mdiPlayCircle, mdiCloudDownload, mdiAutoFix } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; - -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('video'); - const [contentPosition, setContentPosition] = useState('right'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'AI Music Studio Admin' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( -
- -
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; +export default function Home() { + const title = 'AI Music Studio Pro'; return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Professional AI Music Generator')} - -
- {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

-
- - - - - -
+ {/* Hero Section */} + + {/* Abstract background elements */} +
+
+
+
-
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+
+
+ + Next-Gen AI Generation +
+ +

+ Professional AI
Music Creator +

+ +

+ Generate studio-quality songs in seconds. Choose your style, era, and voice. + Lyrics synchronization across 200+ languages with synchronized AI vocals. +

+ +
+ + + + + + + +
+ +
+ {[ + { label: '200+ Languages', icon: mdiMusic }, + { label: 'Studio Voices', icon: mdiMusic }, + { label: 'Sync Lyrics', icon: mdiMusic }, + { label: 'MP4 Download', icon: mdiCloudDownload } + ].map((feature, i) => ( +
+
+ +
+ {feature.label} +
+ ))} +
+
+ + + {/* Footer */} +
+
+
+
+ +
+ {title} +
+ +
+ Privacy Policy + Terms of Service + © 2026 {title}. All rights reserved. +
+
+
); } -Starter.getLayout = function getLayout(page: ReactElement) { +Home.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file diff --git a/frontend/src/pages/studio/index.tsx b/frontend/src/pages/studio/index.tsx new file mode 100644 index 0000000..b62a655 --- /dev/null +++ b/frontend/src/pages/studio/index.tsx @@ -0,0 +1,397 @@ + +import React, { ReactElement, useEffect, useState, useRef } from 'react'; +import HeadInstance from 'next/head'; +import { Formik, Form, Field } from 'formik'; +import { mdiMusic, mdiMicrophone, mdiAutoFix, mdiHistory, mdiPlay, mdiDownload, mdiAlertCircle, mdiCheckCircle, mdiLock, mdiPause } from '@mdi/js'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseButtons from '../../components/BaseButtons'; +import { getPageTitle } from '../../config'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { fetch as fetchSongs, create as createSong } from '../../stores/songs/songsSlice'; +import { create as createJob } from '../../stores/generation_jobs/generation_jobsSlice'; +import { SelectField } from '../../components/SelectField'; +import BaseIcon from '../../components/BaseIcon'; +import FormField from '../../components/FormField'; +import NotificationBar from '../../components/NotificationBar'; + +const STUDIO_KEY = 'STUDIO-2026-PRO'; + +const StudioPage = () => { + const dispatch = useAppDispatch(); + const { songs, loading: songsLoading } = useAppSelector((state) => state.songs); + const [hasKey, setHasKey] = useState(false); + const [keyInput, setKeyInput] = useState(''); + const [showKeyError, setShowKeyError] = useState(false); + const [isGenerating, setIsGenerating] = useState(false); + const [generationSuccess, setGenerationSuccess] = useState(false); + + // Player state + const [playingSongId, setPlayingSongId] = useState(null); + const audioRef = useRef(null); + + useEffect(() => { + const storedKey = localStorage.getItem('studio_key'); + if (storedKey === STUDIO_KEY) { + setHasKey(true); + } + dispatch(fetchSongs({ query: '?limit=10&offset=0&sort=createdAt_DESC' })); + }, [dispatch]); + + const handleKeySubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (keyInput === STUDIO_KEY) { + localStorage.setItem('studio_key', STUDIO_KEY); + setHasKey(true); + setShowKeyError(false); + } else { + setShowKeyError(true); + } + }; + + const initialValues = { + song_title: '', + generation_mode: 'manual_lyrics', + lyrics_text: '', + languageId: '', + styleId: '', + eraId: '', + voiceId: '', + }; + + const validate = (values: any) => { + const errors: any = {}; + if (!values.song_title) errors.song_title = 'Required'; + if (values.generation_mode === 'manual_lyrics' && !values.lyrics_text) errors.lyrics_text = 'Required'; + if (!values.languageId) errors.languageId = 'Required'; + if (!values.styleId) errors.styleId = 'Required'; + if (!values.eraId) errors.eraId = 'Required'; + return errors; + }; + + const handleSubmit = async (values: any, { resetForm }: any) => { + setIsGenerating(true); + try { + const songResult = await dispatch(createSong({ + song_title: values.song_title, + generation_mode: values.generation_mode, + lyrics_text: values.lyrics_text, + languageId: values.languageId, + styleId: values.styleId, + eraId: values.eraId, + status: 'queued', + requested_at: new Date().toISOString(), + })).unwrap(); + + await dispatch(createJob({ + songId: songResult.id, + job_type: 'full_generation', + status: 'pending', + priority: 1, + })).unwrap(); + + setGenerationSuccess(true); + resetForm(); + dispatch(fetchSongs({ query: '?limit=10&offset=0&sort=createdAt_DESC' })); + setTimeout(() => setGenerationSuccess(false), 5000); + } catch (error) { + console.error('Failed to generate song:', error); + } finally { + setIsGenerating(false); + } + }; + + const togglePlay = (song: any) => { + const asset = song.media_assets_song?.find((a: any) => a.asset_type.startsWith('audio')); + if (!asset || !asset.file_blob?.[0]) return; + + const fileId = asset.file_blob[0].id; + const url = `/api/file/download?id=${fileId}`; + + if (playingSongId === song.id) { + audioRef.current?.pause(); + setPlayingSongId(null); + } else { + if (audioRef.current) { + audioRef.current.src = url; + audioRef.current.play(); + } + setPlayingSongId(song.id); + } + }; + + const handleDownload = (song: any) => { + const asset = song.media_assets_song?.find((a: any) => a.asset_type.startsWith('audio') || a.asset_type === 'video_mp4'); + if (!asset || !asset.file_blob?.[0]) return; + + const fileId = asset.file_blob[0].id; + window.open(`/api/file/download?id=${fileId}`, '_blank'); + }; + + if (!hasKey) { + return ( + + +
+
+ +
+

Private Studio Access

+

Enter your unique administrator key to continue.

+
+ +
+ + setKeyInput(e.target.value)} + /> + + + + +
+
+ ); + } + + return ( + <> + + {getPageTitle('AI Music Studio')} + + +