This commit is contained in:
Flatlogic Bot 2026-03-02 03:48:38 +00:00
parent 709d21c1b5
commit 70b32b34db
7 changed files with 501 additions and 209 deletions

View File

@ -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 {
}
};
};

View File

@ -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 <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
}
}

View File

@ -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({
</div>
</div>
)
}
}

View File

@ -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

View File

@ -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) {
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<IntroGuide
steps={steps}
stepsName={stepName}
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</>
)}
</Provider>
)
}
export default appWithTranslation(MyApp);
export default appWithTranslation(MyApp);

View File

@ -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) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
export default function Home() {
const title = 'AI Music Studio Pro';
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="bg-slate-950 text-white min-h-screen font-sans selection:bg-emerald-500/30">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Professional AI Music Generator')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your AI Music Studio Admin app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
{/* Hero Section */}
<SectionFullScreen bg="none" className="relative overflow-hidden">
{/* Abstract background elements */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-900/20 blur-[120px] rounded-full" />
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-blue-900/20 blur-[120px] rounded-full" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full opacity-20"
style={{ backgroundImage: 'radial-gradient(circle, #334155 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<div className="z-10 container mx-auto px-6 flex flex-col items-center justify-center text-center py-20">
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 mb-8 animate-fade-in">
<BaseIcon path={mdiAutoFix} size={16} className="text-emerald-400" />
<span className="text-xs font-medium text-emerald-400 tracking-wider uppercase">Next-Gen AI Generation</span>
</div>
<h1 className="text-5xl md:text-7xl font-extrabold tracking-tight mb-6 bg-clip-text text-transparent bg-gradient-to-b from-white to-slate-400">
Professional AI <br /> Music Creator
</h1>
<p className="max-w-2xl text-lg md:text-xl text-slate-400 mb-10 leading-relaxed">
Generate studio-quality songs in seconds. Choose your style, era, and voice.
Lyrics synchronization across 200+ languages with synchronized AI vocals.
</p>
<div className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-6">
<Link href="/studio">
<button className="px-8 py-4 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold rounded-xl transition-all duration-300 transform hover:scale-105 hover:shadow-[0_0_20px_rgba(16,185,129,0.4)] flex items-center justify-center">
<BaseIcon path={mdiPlayCircle} size={24} className="mr-2" />
ENTER STUDIO
</button>
</Link>
<Link href="/login">
<button className="px-8 py-4 bg-slate-800 hover:bg-slate-700 text-white font-bold rounded-xl border border-slate-700 transition-all duration-300 flex items-center justify-center">
<BaseIcon path={mdiAccountKey} size={24} className="mr-2" />
ADMIN LOGIN
</button>
</Link>
</div>
<div className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 w-full max-w-4xl">
{[
{ label: '200+ Languages', icon: mdiMusic },
{ label: 'Studio Voices', icon: mdiMusic },
{ label: 'Sync Lyrics', icon: mdiMusic },
{ label: 'MP4 Download', icon: mdiCloudDownload }
].map((feature, i) => (
<div key={i} className="flex flex-col items-center p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
<div className="w-10 h-10 rounded-full bg-emerald-500/10 flex items-center justify-center mb-3 text-emerald-400">
<BaseIcon path={feature.icon} size={20} />
</div>
<span className="text-sm font-medium text-slate-300">{feature.label}</span>
</div>
))}
</div>
</div>
</SectionFullScreen>
{/* Footer */}
<footer className="border-t border-slate-900 bg-slate-950/50 py-12">
<div className="container mx-auto px-6 flex flex-col md:flex-row items-center justify-between">
<div className="flex items-center space-x-2 mb-4 md:mb-0">
<div className="w-8 h-8 rounded-lg bg-emerald-500 flex items-center justify-center">
<BaseIcon path={mdiMusic} size={20} className="text-slate-950" />
</div>
<span className="text-xl font-bold tracking-tight">{title}</span>
</div>
<div className="flex space-x-8 text-sm text-slate-500">
<Link href="/privacy-policy" className="hover:text-emerald-400 transition-colors">Privacy Policy</Link>
<Link href="/terms-of-use" className="hover:text-emerald-400 transition-colors">Terms of Service</Link>
<span>© 2026 {title}. All rights reserved.</span>
</div>
</div>
</footer>
</div>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};

View File

@ -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<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(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 (
<SectionMain className="flex items-center justify-center min-h-[70vh]">
<CardBox className="w-full max-w-md shadow-2xl border-emerald-500/20">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center mx-auto mb-4">
<BaseIcon path={mdiLock} size={32} className="text-emerald-500" />
</div>
<h2 className="text-2xl font-bold text-white">Private Studio Access</h2>
<p className="text-slate-400 mt-2">Enter your unique administrator key to continue.</p>
</div>
<form onSubmit={handleKeySubmit} className="space-y-4">
<FormField label="Access Key" help={showKeyError ? 'Invalid access key. Please check again.' : ''}>
<input
type="password"
className={`w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500 focus:border-emerald-500 ${showKeyError ? 'border-red-500' : ''}`}
placeholder="STUDIO-XXXX-XXXX"
value={keyInput}
onChange={(e) => setKeyInput(e.target.value)}
/>
</FormField>
<BaseButton
type="submit"
color="info"
label="Unlock Studio"
className="w-full py-3 bg-emerald-500 hover:bg-emerald-600 border-none text-slate-950 font-bold"
/>
</form>
</CardBox>
</SectionMain>
);
}
return (
<>
<HeadInstance>
<title>{getPageTitle('AI Music Studio')}</title>
</HeadInstance>
<audio
ref={audioRef}
onEnded={() => setPlayingSongId(null)}
className="hidden"
/>
<SectionMain>
<SectionTitleLineWithButton icon={mdiMusic} title="AI Music Generation Studio" main>
{''}
</SectionTitleLineWithButton>
{generationSuccess && (
<NotificationBar color="success" icon={mdiCheckCircle}>
<b>Success!</b> Your song generation has been queued. It will appear in the library shortly.
</NotificationBar>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<CardBox className="border-emerald-500/10">
<Formik initialValues={initialValues} validate={validate} onSubmit={handleSubmit}>
{({ values, errors, touched, setFieldValue }) => (
<Form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Song Title" error={errors.song_title && touched.song_title}>
<Field
name="song_title"
placeholder="My Epic AI Track"
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500"
/>
</FormField>
<FormField label="Generation Mode">
<Field
as="select"
name="generation_mode"
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500"
>
<option value="manual_lyrics">Manual Lyrics</option>
<option value="auto_lyrics">AI Auto-Lyrics</option>
<option value="remix_reference">Remix Reference</option>
</Field>
</FormField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Language" error={errors.languageId && touched.languageId}>
<SelectField
itemRef="languages"
showField="language_name"
field={{ name: 'languageId', value: values.languageId }}
form={{ setFieldValue }}
options={{ id: 'languageId' }}
disabled={false}
/>
</FormField>
<FormField label="Music Era" error={errors.eraId && touched.eraId}>
<SelectField
itemRef="eras"
showField="era_name"
field={{ name: 'eraId', value: values.eraId }}
form={{ setFieldValue }}
options={{ id: 'eraId' }}
disabled={false}
/>
</FormField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Music Style" error={errors.styleId && touched.styleId}>
<SelectField
itemRef="music_styles"
showField="style_name"
field={{ name: 'styleId', value: values.styleId }}
form={{ setFieldValue }}
options={{ id: 'styleId' }}
disabled={false}
/>
</FormField>
<FormField label="Vocal Character">
<SelectField
itemRef="ai_voices"
showField="voice_name"
field={{ name: 'voiceId', value: values.voiceId }}
form={{ setFieldValue }}
options={{ id: 'voiceId' }}
disabled={false}
/>
</FormField>
</div>
<FormField label="Lyrics" error={errors.lyrics_text && touched.lyrics_text}>
<Field
as="textarea"
name="lyrics_text"
placeholder="Enter your lyrics here or let the AI generate them..."
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 h-48 font-mono text-sm focus:ring-emerald-500"
disabled={values.generation_mode === 'auto_lyrics'}
/>
</FormField>
<BaseButtons>
<BaseButton
type="submit"
color="info"
label={isGenerating ? 'Generating...' : 'Start Generation'}
icon={mdiAutoFix}
className="w-full md:w-auto px-10 py-3 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold border-none transition-all hover:scale-105"
disabled={isGenerating}
/>
</BaseButtons>
</Form>
)}
</Formik>
</CardBox>
</div>
<div className="space-y-6">
<CardBox className="border-slate-800">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-white flex items-center">
<BaseIcon path={mdiHistory} className="mr-2 text-emerald-500" />
Recent Library
</h3>
</div>
{songsLoading ? (
<div className="flex justify-center py-10">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-500" />
</div>
) : (
<div className="space-y-4">
{songs?.map((song: any) => {
const hasAudio = song.media_assets_song?.some((a: any) => a.asset_type.startsWith('audio'));
return (
<div key={song.id} className="p-3 rounded-xl bg-slate-800/50 border border-slate-700 hover:border-emerald-500/50 transition-colors group">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0 mr-2">
<div className="font-bold text-sm text-white truncate">{song.song_title}</div>
<div className="text-[10px] text-slate-400 mt-1 uppercase tracking-wider">{song.style?.style_name} {song.era?.era_name}</div>
</div>
<div className="flex space-x-1 shrink-0">
{hasAudio && (
<BaseButton
icon={playingSongId === song.id ? mdiPause : mdiPlay}
color="info"
small
roundedFull
onClick={() => togglePlay(song)}
className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 border-none"
/>
)}
<BaseButton
icon={mdiDownload}
color="white"
small
roundedFull
onClick={() => handleDownload(song)}
disabled={!song.media_assets_song?.length}
className="bg-slate-700 hover:bg-slate-600 text-white border-none"
/>
</div>
</div>
<div className="mt-3">
<div className="flex items-center justify-between text-[10px] mb-1">
<span className="text-slate-500">Status</span>
<span className={`font-bold ${song.status === 'ready' ? 'text-emerald-400' : 'text-amber-400'}`}>
{song.status.toUpperCase()}
</span>
</div>
<div className="w-full bg-slate-900 rounded-full h-1">
<div
className={`h-full rounded-full ${song.status === 'ready' ? 'bg-emerald-500' : 'bg-amber-500 animate-pulse'}`}
style={{ width: song.status === 'ready' ? '100%' : '60%' }}
/>
</div>
</div>
</div>
);
})}
{songs?.length === 0 && (
<div className="text-center py-10 text-slate-500">
<BaseIcon path={mdiAlertCircle} size={48} className="mx-auto mb-2 opacity-20" />
<p>No tracks generated yet</p>
</div>
)}
</div>
)}
<div className="mt-6 pt-6 border-t border-slate-700">
<BaseButton
href="/songs/songs-list"
label="View Full Library"
color="white"
className="w-full text-xs text-slate-400 bg-transparent border-slate-700 hover:bg-slate-800"
/>
</div>
</CardBox>
<CardBox className="bg-gradient-to-br from-indigo-900/20 to-blue-900/20 border-blue-500/20">
<div className="flex items-center space-x-3 text-blue-400 mb-2">
<BaseIcon path={mdiMicrophone} size={24} />
<h4 className="font-bold">Studio Tip</h4>
</div>
<p className="text-xs text-slate-400 leading-relaxed">
AI voices are more expressive when lyrics include punctuation. Try adding exclamation marks or ellipses for better vocal phrasing.
</p>
</CardBox>
</div>
</div>
</SectionMain>
</>
);
};
StudioPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default StudioPage;