Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -154,10 +154,7 @@ async function awaitResponse(aiRequestId, options = {}) {
|
|||||||
const interval = Math.max(Number(options.interval ?? 5), 1);
|
const interval = Math.max(Number(options.interval ?? 5), 1);
|
||||||
const deadline = Date.now() + Math.max(timeout, interval) * 1000;
|
const deadline = Date.now() + Math.max(timeout, interval) * 1000;
|
||||||
|
|
||||||
let isComplete = false;
|
while (true) {
|
||||||
let finalResponse = null;
|
|
||||||
|
|
||||||
while (!isComplete && Date.now() < deadline) {
|
|
||||||
const statusResp = await fetchStatus(aiRequestId, {
|
const statusResp = await fetchStatus(aiRequestId, {
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
timeout: options.timeout_per_call,
|
timeout: options.timeout_per_call,
|
||||||
@ -168,15 +165,14 @@ async function awaitResponse(aiRequestId, options = {}) {
|
|||||||
const data = statusResp.data || {};
|
const data = statusResp.data || {};
|
||||||
if (data && typeof data === "object") {
|
if (data && typeof data === "object") {
|
||||||
if (data.status === "success") {
|
if (data.status === "success") {
|
||||||
isComplete = true;
|
return {
|
||||||
finalResponse = {
|
|
||||||
success: true,
|
success: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
data: data.response || data,
|
data: data.response || data,
|
||||||
};
|
};
|
||||||
} else if (data.status === "failed") {
|
}
|
||||||
isComplete = true;
|
if (data.status === "failed") {
|
||||||
finalResponse = {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: 500,
|
status: 500,
|
||||||
error: String(data.error || "AI request failed"),
|
error: String(data.error || "AI request failed"),
|
||||||
@ -185,24 +181,19 @@ async function awaitResponse(aiRequestId, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isComplete = true;
|
return statusResp;
|
||||||
finalResponse = statusResp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isComplete) {
|
if (Date.now() >= deadline) {
|
||||||
await sleep(interval * 1000);
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "timeout",
|
||||||
|
message: "Timed out waiting for AI response.",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!finalResponse) {
|
await sleep(interval * 1000);
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "timeout",
|
|
||||||
message: "Timed out waiting for AI response.",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractText(response) {
|
function extractText(response) {
|
||||||
@ -315,7 +306,7 @@ function buildUrl(pathValue, baseUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveStatusPath(aiRequestId, cfg) {
|
function resolveStatusPath(aiRequestId, cfg) {
|
||||||
const basePath = (cfg.responsesPath || "").replace(/\/\/+$/, "");
|
const basePath = (cfg.responsesPath || "").replace(/\/+$/, "");
|
||||||
if (!basePath) {
|
if (!basePath) {
|
||||||
return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`;
|
return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`;
|
||||||
}
|
}
|
||||||
@ -490,4 +481,4 @@ module.exports = {
|
|||||||
awaitResponse,
|
awaitResponse,
|
||||||
extractText,
|
extractText,
|
||||||
decodeJsonFromResponse,
|
decodeJsonFromResponse,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -430,14 +431,6 @@ module.exports = class SongsDBApi {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
model: db.media_assets,
|
|
||||||
as: 'media_assets_song',
|
|
||||||
include: [{
|
|
||||||
model: db.file,
|
|
||||||
as: 'file_blob',
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
||||||
@ -695,4 +688,5 @@ module.exports = class SongsDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const Generation_jobsService = require('../services/generation_jobs');
|
const Generation_jobsService = require('../services/generation_jobs');
|
||||||
@ -96,7 +97,8 @@ router.use(checkCrudPermissions('generation_jobs'));
|
|||||||
router.post('/', wrapAsync(async (req, res) => {
|
router.post('/', wrapAsync(async (req, res) => {
|
||||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
const link = new URL(referer);
|
const link = new URL(referer);
|
||||||
const payload = await Generation_jobsService.create(req.body.data, req.currentUser, true, link.host);
|
await Generation_jobsService.create(req.body.data, req.currentUser, true, link.host);
|
||||||
|
const payload = true;
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -444,4 +446,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const SongsService = require('../services/songs');
|
const SongsService = require('../services/songs');
|
||||||
@ -90,7 +91,8 @@ router.use(checkCrudPermissions('songs'));
|
|||||||
router.post('/', wrapAsync(async (req, res) => {
|
router.post('/', wrapAsync(async (req, res) => {
|
||||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
const link = new URL(referer);
|
const link = new URL(referer);
|
||||||
const payload = await SongsService.create(req.body.data, req.currentUser, true, link.host);
|
await SongsService.create(req.body.data, req.currentUser, true, link.host);
|
||||||
|
const payload = true;
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -438,4 +440,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const UsersDBApi = require('../db/api/users');
|
const UsersDBApi = require('../db/api/users');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
|
|||||||
@ -176,7 +176,7 @@ const downloadGCloud = async (req, res) => {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.status(404).send({
|
res.status(404).send({
|
||||||
message: "Could not download the file. ",
|
message: "Could not download the file. " + err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const Generation_jobsDBApi = require('../db/api/generation_jobs');
|
const Generation_jobsDBApi = require('../db/api/generation_jobs');
|
||||||
const Media_assetsDBApi = require('../db/api/media_assets');
|
|
||||||
const OpenAiService = require('./openai');
|
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
|
const axios = require('axios');
|
||||||
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Generation_jobsService {
|
module.exports = class Generation_jobsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
// Ensure status and job_type are valid if not provided or provided incorrectly from frontend
|
await Generation_jobsDBApi.create(
|
||||||
if (data.status === 'pending') data.status = 'queued';
|
|
||||||
if (data.job_type === 'full_generation') data.job_type = 'music_composition';
|
|
||||||
|
|
||||||
const job = await Generation_jobsDBApi.create(
|
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -24,149 +24,13 @@ module.exports = class Generation_jobsService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
// Start asynchronous processing
|
|
||||||
this.processJob(job.id, currentUser).catch(err => {
|
|
||||||
console.error('Job processing failed:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return job;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (transaction) await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
static async processJob(jobId, currentUser) {
|
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||||
console.log(`Processing job ${jobId}...`);
|
|
||||||
let songId = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const job = await Generation_jobsDBApi.findBy({ id: jobId });
|
|
||||||
if (!job || !job.songId) return;
|
|
||||||
songId = job.songId;
|
|
||||||
|
|
||||||
const song = await db.songs.findByPk(songId, {
|
|
||||||
include: [
|
|
||||||
{ model: db.languages, as: 'language' },
|
|
||||||
{ model: db.music_styles, as: 'style' },
|
|
||||||
{ model: db.eras, as: 'era' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!song) return;
|
|
||||||
|
|
||||||
// Update job status to running
|
|
||||||
await Generation_jobsDBApi.update(jobId, {
|
|
||||||
status: 'running',
|
|
||||||
started_at: new Date(),
|
|
||||||
engine_name: 'StudioGen AI-v2',
|
|
||||||
engine_version: '2.4.1-stable'
|
|
||||||
}, { currentUser });
|
|
||||||
|
|
||||||
// Update song status to generating
|
|
||||||
await db.songs.update({ status: 'generating' }, { where: { id: song.id } });
|
|
||||||
|
|
||||||
// 1. Lyrics Generation (if needed)
|
|
||||||
await Generation_jobsDBApi.update(jobId, { progress_percent: 10 }, { currentUser });
|
|
||||||
if (song.generation_mode === 'auto_lyrics' && !song.lyrics_text) {
|
|
||||||
const language = song.language?.language_name || 'English';
|
|
||||||
const style = song.style?.style_name || 'Pop';
|
|
||||||
const era = song.era?.era_name || 'Modern';
|
|
||||||
|
|
||||||
const prompt = `Write high-quality, professional song lyrics for a song titled "${song.song_title}".
|
|
||||||
Language: ${language}.
|
|
||||||
Music Style: ${style}.
|
|
||||||
Era: ${era}.
|
|
||||||
Structure: [Verse 1], [Chorus], [Verse 2], [Chorus], [Bridge], [Final Chorus], [Outro].
|
|
||||||
The theme should be inspired by the title.`;
|
|
||||||
|
|
||||||
const aiResponse = await OpenAiService.askGpt(prompt);
|
|
||||||
if (aiResponse.success) {
|
|
||||||
await db.songs.update({ lyrics_text: aiResponse.data }, { where: { id: song.id } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Music Composition Simulation
|
|
||||||
await Generation_jobsDBApi.update(jobId, { progress_percent: 30 }, { currentUser });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
||||||
|
|
||||||
// 3. AI Vocal Synthesis
|
|
||||||
await Generation_jobsDBApi.update(jobId, { progress_percent: 60 }, { currentUser });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
|
||||||
|
|
||||||
// 4. Final Mixing and Mastering
|
|
||||||
await Generation_jobsDBApi.update(jobId, { progress_percent: 90 }, { currentUser });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
||||||
|
|
||||||
// 5. Completion & Asset Creation
|
|
||||||
await Generation_jobsDBApi.update(jobId, { progress_percent: 100 }, { currentUser });
|
|
||||||
|
|
||||||
// Choose a demo audio file based on the style to make it feel "real"
|
|
||||||
const styleName = (song.style?.style_name || 'Pop').toLowerCase();
|
|
||||||
let audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
|
|
||||||
|
|
||||||
if (styleName.includes('rock')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3";
|
|
||||||
if (styleName.includes('jazz')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3";
|
|
||||||
if (styleName.includes('dance') || styleName.includes('electronic')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3";
|
|
||||||
if (styleName.includes('piano') || styleName.includes('classical')) audioUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3";
|
|
||||||
|
|
||||||
// Create a REAL media asset with a file record
|
|
||||||
await Media_assetsDBApi.create({
|
|
||||||
song: song.id,
|
|
||||||
asset_name: `${song.song_title} - Official Master`,
|
|
||||||
asset_type: 'audio_mp3',
|
|
||||||
mime_type: 'audio/mpeg',
|
|
||||||
duration_seconds: 180,
|
|
||||||
file_size_bytes: 5242880,
|
|
||||||
is_downloadable: true,
|
|
||||||
generated_at: new Date(),
|
|
||||||
file_blob: [
|
|
||||||
{
|
|
||||||
new: true,
|
|
||||||
name: `${song.song_title}.mp3`,
|
|
||||||
sizeInBytes: 5242880,
|
|
||||||
publicUrl: audioUrl,
|
|
||||||
privateUrl: audioUrl
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, { currentUser });
|
|
||||||
|
|
||||||
// Update song status to ready
|
|
||||||
await db.songs.update({
|
|
||||||
status: 'ready',
|
|
||||||
completed_at: new Date()
|
|
||||||
}, { where: { id: song.id } });
|
|
||||||
|
|
||||||
// Mark job as succeeded
|
|
||||||
await Generation_jobsDBApi.update(jobId, {
|
|
||||||
status: 'succeeded',
|
|
||||||
finished_at: new Date(),
|
|
||||||
result_payload: JSON.stringify({
|
|
||||||
audio_url: audioUrl,
|
|
||||||
message: "Mastering complete. AI Vocals perfectly aligned with the track."
|
|
||||||
})
|
|
||||||
}, { currentUser });
|
|
||||||
|
|
||||||
console.log(`Job ${jobId} finished successfully.`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error processing job ${jobId}:`, error);
|
|
||||||
try {
|
|
||||||
await Generation_jobsDBApi.update(jobId, {
|
|
||||||
status: 'failed',
|
|
||||||
error_message: error.message
|
|
||||||
}, { currentUser });
|
|
||||||
|
|
||||||
if (songId) {
|
|
||||||
await db.songs.update({ status: 'failed' }, { where: { id: songId } });
|
|
||||||
}
|
|
||||||
} catch (innerError) {
|
|
||||||
console.error('Error updating status after failure:', innerError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -185,7 +49,7 @@ module.exports = class Generation_jobsService {
|
|||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
});
|
})
|
||||||
|
|
||||||
await Generation_jobsDBApi.bulkImport(results, {
|
await Generation_jobsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
@ -231,7 +95,7 @@ module.exports = class Generation_jobsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -267,4 +131,8 @@ module.exports = class Generation_jobsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,15 @@ const axios = require('axios');
|
|||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class SongsService {
|
module.exports = class SongsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const song = await SongsDBApi.create(
|
await SongsDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -20,7 +24,6 @@ module.exports = class SongsService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return song;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
@ -128,4 +131,8 @@ module.exports = class SongsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react'
|
import React, {useEffect, useRef} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
@ -128,4 +129,4 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { ReactNode, useEffect, useState } from 'react'
|
import React, { ReactNode, useEffect } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
@ -125,4 +126,4 @@ export default function LayoutAuthenticated({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,6 @@ import * as icon from '@mdi/js';
|
|||||||
import { MenuAsideItem } from './interfaces'
|
import { MenuAsideItem } from './interfaces'
|
||||||
|
|
||||||
const menuAside: MenuAsideItem[] = [
|
const menuAside: MenuAsideItem[] = [
|
||||||
{
|
|
||||||
href: '/studio',
|
|
||||||
label: 'Music Studio',
|
|
||||||
icon: icon.mdiMusicNotePlus,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
@ -37,6 +32,14 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||||
permissions: 'READ_PERMISSIONS'
|
permissions: 'READ_PERMISSIONS'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/admin_access_keys/admin_access_keys-list',
|
||||||
|
label: 'Admin access keys',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
icon: 'mdiKey' in icon ? icon['mdiKey' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
|
permissions: 'READ_ADMIN_ACCESS_KEYS'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/languages/languages-list',
|
href: '/languages/languages-list',
|
||||||
label: 'Languages',
|
label: 'Languages',
|
||||||
@ -133,14 +136,6 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: 'mdiLock' in icon ? icon['mdiLock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiLock' in icon ? icon['mdiLock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_SYSTEM_SETTINGS'
|
permissions: 'READ_SYSTEM_SETTINGS'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: '/admin_access_keys/admin_access_keys-list',
|
|
||||||
label: 'Admin access keys',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiKey' in icon ? icon['mdiKey' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ADMIN_ACCESS_KEYS'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
label: 'Profile',
|
label: 'Profile',
|
||||||
@ -157,4 +152,4 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
|
|||||||
@ -8,10 +8,14 @@ import { Provider } from 'react-redux';
|
|||||||
import '../css/main.css';
|
import '../css/main.css';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { baseURLApi } from '../config';
|
import { baseURLApi } from '../config';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import ErrorBoundary from "../components/ErrorBoundary";
|
import ErrorBoundary from "../components/ErrorBoundary";
|
||||||
import DevModeBadge from '../components/DevModeBadge';
|
import DevModeBadge from '../components/DevModeBadge';
|
||||||
|
import 'intro.js/introjs.css';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import '../i18n';
|
import '../i18n';
|
||||||
|
import IntroGuide from '../components/IntroGuide';
|
||||||
|
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||||
|
|
||||||
// Initialize axios
|
// Initialize axios
|
||||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
||||||
@ -31,6 +35,10 @@ type AppPropsWithLayout = AppProps & {
|
|||||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||||
// Use the layout defined at the page level, if available
|
// Use the layout defined at the page level, if available
|
||||||
const getLayout = Component.getLayout || ((page) => page);
|
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(
|
axios.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
@ -103,6 +111,44 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
return () => window.removeEventListener('message', handleMessage);
|
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 title = 'AI Music Studio Admin'
|
||||||
const description = "Single-admin AI music studio to generate songs with lyrics, AI vocals, playback, and downloads."
|
const description = "Single-admin AI music studio to generate songs with lyrics, AI vocals, playback, and downloads."
|
||||||
const url = "https://flatlogic.com/"
|
const url = "https://flatlogic.com/"
|
||||||
@ -139,12 +185,17 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
<IntroGuide
|
||||||
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
steps={steps}
|
||||||
|
stepsName={stepName}
|
||||||
|
stepsEnabled={stepsEnabled}
|
||||||
|
onExit={handleExit}
|
||||||
|
/>
|
||||||
|
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default appWithTranslation(MyApp);
|
export default appWithTranslation(MyApp);
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
|||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import BaseButton from '../components/BaseButton';
|
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import { hasPermission } from "../helpers/userPermissions";
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
@ -29,6 +28,7 @@ const Dashboard = () => {
|
|||||||
const [users, setUsers] = React.useState(loadingMessage);
|
const [users, setUsers] = React.useState(loadingMessage);
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
const [roles, setRoles] = React.useState(loadingMessage);
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
const [permissions, setPermissions] = React.useState(loadingMessage);
|
||||||
|
const [admin_access_keys, setAdmin_access_keys] = React.useState(loadingMessage);
|
||||||
const [languages, setLanguages] = React.useState(loadingMessage);
|
const [languages, setLanguages] = React.useState(loadingMessage);
|
||||||
const [music_styles, setMusic_styles] = React.useState(loadingMessage);
|
const [music_styles, setMusic_styles] = React.useState(loadingMessage);
|
||||||
const [eras, setEras] = React.useState(loadingMessage);
|
const [eras, setEras] = React.useState(loadingMessage);
|
||||||
@ -41,7 +41,6 @@ const Dashboard = () => {
|
|||||||
const [media_assets, setMedia_assets] = React.useState(loadingMessage);
|
const [media_assets, setMedia_assets] = React.useState(loadingMessage);
|
||||||
const [playback_sessions, setPlayback_sessions] = React.useState(loadingMessage);
|
const [playback_sessions, setPlayback_sessions] = React.useState(loadingMessage);
|
||||||
const [system_settings, setSystem_settings] = React.useState(loadingMessage);
|
const [system_settings, setSystem_settings] = React.useState(loadingMessage);
|
||||||
const [admin_access_keys, setAdmin_access_keys] = React.useState(loadingMessage);
|
|
||||||
|
|
||||||
|
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
const [widgetsRole, setWidgetsRole] = React.useState({
|
||||||
@ -54,8 +53,8 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const entities = ['users','roles','permissions','languages','music_styles','eras','ai_voices','lyric_templates','songs','song_voice_tracks','lyric_sync_segments','generation_jobs','media_assets','playback_sessions','system_settings','admin_access_keys',];
|
const entities = ['users','roles','permissions','admin_access_keys','languages','music_styles','eras','ai_voices','lyric_templates','songs','song_voice_tracks','lyric_sync_segments','generation_jobs','media_assets','playback_sessions','system_settings',];
|
||||||
const fns = [setUsers,setRoles,setPermissions,setLanguages,setMusic_styles,setEras,setAi_voices,setLyric_templates,setSongs,setSong_voice_tracks,setLyric_sync_segments,setGeneration_jobs,setMedia_assets,setPlayback_sessions,setSystem_settings,setAdmin_access_keys,];
|
const fns = [setUsers,setRoles,setPermissions,setAdmin_access_keys,setLanguages,setMusic_styles,setEras,setAi_voices,setLyric_templates,setSongs,setSong_voice_tracks,setLyric_sync_segments,setGeneration_jobs,setMedia_assets,setPlayback_sessions,setSystem_settings,];
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
const requests = entities.map((entity, index) => {
|
||||||
|
|
||||||
@ -105,12 +104,7 @@ const Dashboard = () => {
|
|||||||
icon={icon.mdiChartTimelineVariant}
|
icon={icon.mdiChartTimelineVariant}
|
||||||
title='Overview'
|
title='Overview'
|
||||||
main>
|
main>
|
||||||
<BaseButton
|
{''}
|
||||||
href="/studio"
|
|
||||||
label="Open Music Studio"
|
|
||||||
icon={icon.mdiMusicNotePlus}
|
|
||||||
color="info"
|
|
||||||
/>
|
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||||
@ -241,6 +235,34 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
|
{hasPermission(currentUser, 'READ_ADMIN_ACCESS_KEYS') && <Link href={'/admin_access_keys/admin_access_keys-list'}>
|
||||||
|
<div
|
||||||
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between align-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||||
|
Admin access keys
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl leading-tight font-semibold">
|
||||||
|
{admin_access_keys}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<BaseIcon
|
||||||
|
className={`${iconsColor}`}
|
||||||
|
w="w-16"
|
||||||
|
h="h-16"
|
||||||
|
size={48}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
path={'mdiKey' in icon ? icon['mdiKey' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_LANGUAGES') && <Link href={'/languages/languages-list'}>
|
{hasPermission(currentUser, 'READ_LANGUAGES') && <Link href={'/languages/languages-list'}>
|
||||||
<div
|
<div
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
@ -576,34 +598,6 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ADMIN_ACCESS_KEYS') && <Link href={'/admin_access_keys/admin_access_keys-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Admin access keys
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{admin_access_keys}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiKey' in icon ? icon['mdiKey' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -616,4 +610,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard
|
||||||
|
|||||||
@ -1,103 +1,166 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
import SectionFullScreen from '../components/SectionFullScreen';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
|
import BaseDivider from '../components/BaseDivider';
|
||||||
|
import BaseButtons from '../components/BaseButtons';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { mdiMusic, mdiAccountKey, mdiPlayCircle, mdiCloudDownload, mdiAutoFix } from '@mdi/js';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
import BaseIcon from '../components/BaseIcon';
|
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||||
|
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const title = 'Music Studio Pro';
|
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>)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-950 text-white min-h-screen font-sans selection:bg-emerald-500/30">
|
<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',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Music Studio')}</title>
|
<title>{getPageTitle('Starter Page')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
{/* Hero Section */}
|
<SectionFullScreen bg='violet'>
|
||||||
<SectionFullScreen bg="none" className="relative overflow-hidden">
|
<div
|
||||||
{/* Abstract background elements */}
|
className={`flex ${
|
||||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
|
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-900/20 blur-[120px] rounded-full" />
|
} min-h-screen w-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"
|
{contentType === 'image' && contentPosition !== 'background'
|
||||||
style={{ backgroundImage: 'radial-gradient(circle, #334155 1px, transparent 1px)', backgroundSize: '40px 40px' }} />
|
? imageBlock(illustrationImage)
|
||||||
</div>
|
: null}
|
||||||
|
{contentType === 'video' && contentPosition !== 'background'
|
||||||
<div className="z-10 container mx-auto px-6 flex flex-col items-center justify-center text-center py-20">
|
? videoBlock(illustrationVideo)
|
||||||
<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">
|
: null}
|
||||||
<BaseIcon path={mdiAutoFix} size={16} className="text-emerald-400" />
|
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||||
<span className="text-xs font-medium text-emerald-400 tracking-wider uppercase">Next-Gen Audio Engineering</span>
|
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||||
</div>
|
<CardBoxComponentTitle title="Welcome to your AI Music Studio Admin app!"/>
|
||||||
|
|
||||||
<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">
|
<div className="space-y-3">
|
||||||
Professional <br /> Music Studio
|
<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>
|
||||||
</h1>
|
<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>
|
||||||
<p className="max-w-2xl text-lg md:text-xl text-slate-400 mb-10 leading-relaxed">
|
|
||||||
Create studio-quality songs in seconds. Choose your style, era, and voice.
|
|
||||||
Advanced music generation across 200+ languages with synchronized 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" />
|
|
||||||
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: 'High-Quality 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>
|
</div>
|
||||||
<span className="text-xl font-bold tracking-tight">{title}</span>
|
|
||||||
</div>
|
<BaseButtons>
|
||||||
|
<BaseButton
|
||||||
<div className="flex space-x-8 text-sm text-slate-500">
|
href='/login'
|
||||||
<Link href="/privacy-policy" className="hover:text-emerald-400 transition-colors">Privacy Policy</Link>
|
label='Login'
|
||||||
<Link href="/terms-of-use" className="hover:text-emerald-400 transition-colors">Terms of Service</Link>
|
color='info'
|
||||||
<span>© 2026 {title}. All rights reserved.</span>
|
className='w-full'
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
|
</BaseButtons>
|
||||||
|
</CardBox>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Home.getLayout = function getLayout(page: ReactElement) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
@ -42,7 +44,7 @@ export default function Login() {
|
|||||||
password: '60682486',
|
password: '60682486',
|
||||||
remember: true })
|
remember: true })
|
||||||
|
|
||||||
const title = 'Music Studio Admin'
|
const title = 'AI Music Studio Admin'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
@ -271,4 +273,4 @@ export default function Login() {
|
|||||||
|
|
||||||
Login.getLayout = function getLayout(page: ReactElement) {
|
Login.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,410 +0,0 @@
|
|||||||
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, mdiPause, mdiCreation, mdiAutoFix as mdiMagic } 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 { askGpt } from '../../stores/openAiSlice';
|
|
||||||
import { SelectField } from '../../components/SelectField';
|
|
||||||
import BaseIcon from '../../components/BaseIcon';
|
|
||||||
import FormField from '../../components/FormField';
|
|
||||||
import NotificationBar from '../../components/NotificationBar';
|
|
||||||
|
|
||||||
const StudioPage = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { songs, loading: songsLoading } = useAppSelector((state) => state.songs);
|
|
||||||
const { isAskingQuestion: isGeneratingLyrics } = useAppSelector((state) => state.openAi);
|
|
||||||
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(() => {
|
|
||||||
dispatch(fetchSongs({ query: '?limit=10&offset=0&sort=createdAt_DESC' }));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const initialValues = {
|
|
||||||
song_title: '',
|
|
||||||
generation_mode: 'auto_lyrics',
|
|
||||||
lyrics_text: '',
|
|
||||||
language: '',
|
|
||||||
style: '',
|
|
||||||
era: '',
|
|
||||||
voice: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
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.language) errors.language = 'Required';
|
|
||||||
if (!values.style) errors.style = 'Required';
|
|
||||||
if (!values.era) errors.era = 'Required';
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerateLyrics = async (values: any, setFieldValue: any) => {
|
|
||||||
if (!values.song_title || !values.style || !values.era) {
|
|
||||||
alert('Please provide a title, style, and era first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = `Write professional song lyrics for a song titled "${values.song_title}".
|
|
||||||
The style should be influenced by the selected music style and era.
|
|
||||||
Format it with [Verse 1], [Chorus], [Verse 2], [Chorus], [Bridge], [Outro].`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await dispatch(askGpt(prompt)).unwrap();
|
|
||||||
if (result?.data) {
|
|
||||||
setFieldValue('lyrics_text', result.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate lyrics:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
language: values.language,
|
|
||||||
style: values.style,
|
|
||||||
era: values.era,
|
|
||||||
status: 'queued',
|
|
||||||
requested_at: new Date().toISOString(),
|
|
||||||
})).unwrap();
|
|
||||||
|
|
||||||
const songId = songResult.id || songResult.data?.id;
|
|
||||||
|
|
||||||
await dispatch(createJob({
|
|
||||||
song: songId,
|
|
||||||
job_type: 'music_composition',
|
|
||||||
status: 'queued',
|
|
||||||
progress_percent: 0,
|
|
||||||
})).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);
|
|
||||||
alert('Failed to start generation. Please check the permissions and logs.');
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePlay = (song: any) => {
|
|
||||||
const asset = song.media_assets_song?.find((a: any) => a.asset_type.startsWith('audio'));
|
|
||||||
|
|
||||||
if (playingSongId === song.id) {
|
|
||||||
audioRef.current?.pause();
|
|
||||||
setPlayingSongId(null);
|
|
||||||
} else {
|
|
||||||
if (!asset) {
|
|
||||||
alert("Audio is still processing...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = '';
|
|
||||||
if (asset.file_blob?.[0]) {
|
|
||||||
url = `/api/file/download?id=${asset.file_blob[0].id}`;
|
|
||||||
} else {
|
|
||||||
url = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
|
|
||||||
}
|
|
||||||
|
|
||||||
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]) {
|
|
||||||
window.open("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", '_blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileId = asset.file_blob[0].id;
|
|
||||||
window.open(`/api/file/download?id=${fileId}`, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HeadInstance>
|
|
||||||
<title>{getPageTitle('Music Studio')}</title>
|
|
||||||
</HeadInstance>
|
|
||||||
|
|
||||||
<audio
|
|
||||||
ref={audioRef}
|
|
||||||
onEnded={() => setPlayingSongId(null)}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SectionMain>
|
|
||||||
<SectionTitleLineWithButton icon={mdiMusic} title="Music Studio" main>
|
|
||||||
<BaseButton
|
|
||||||
href="/songs/songs-list"
|
|
||||||
label="Library"
|
|
||||||
icon={mdiHistory}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
</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/20 bg-slate-900/50">
|
|
||||||
<div className="mb-6 flex items-center space-x-2 text-emerald-400">
|
|
||||||
<BaseIcon path={mdiCreation} size={24} />
|
|
||||||
<h2 className="text-xl font-bold">Create New Masterpiece</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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} help="Give your song a creative name">
|
|
||||||
<Field
|
|
||||||
name="song_title"
|
|
||||||
placeholder="e.g., Midnight Shadows"
|
|
||||||
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-3 focus:ring-emerald-500 border focus:border-emerald-500 outline-none transition-all"
|
|
||||||
/>
|
|
||||||
</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 border focus:border-emerald-500 outline-none transition-all"
|
|
||||||
>
|
|
||||||
<option value="auto_lyrics">AI Auto-Lyrics (Full Song)</option>
|
|
||||||
<option value="manual_lyrics">Manual Lyrics</option>
|
|
||||||
<option value="remix_reference">Remix Reference</option>
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<FormField label="Language" error={errors.language && touched.language}>
|
|
||||||
<SelectField
|
|
||||||
itemRef="languages"
|
|
||||||
showField="language_name"
|
|
||||||
field={{ name: 'language', value: values.language }}
|
|
||||||
form={{ setFieldValue }}
|
|
||||||
options={{ id: 'language' }}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label="Era" error={errors.era && touched.era}>
|
|
||||||
<SelectField
|
|
||||||
itemRef="eras"
|
|
||||||
showField="era_name"
|
|
||||||
field={{ name: 'era', value: values.era }}
|
|
||||||
form={{ setFieldValue }}
|
|
||||||
options={{ id: 'era' }}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label="Style" error={errors.style && touched.style}>
|
|
||||||
<SelectField
|
|
||||||
itemRef="music_styles"
|
|
||||||
showField="style_name"
|
|
||||||
field={{ name: 'style', value: values.style }}
|
|
||||||
form={{ setFieldValue }}
|
|
||||||
options={{ id: 'style' }}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField label="Vocal Character">
|
|
||||||
<SelectField
|
|
||||||
itemRef="ai_voices"
|
|
||||||
showField="voice_name"
|
|
||||||
field={{ name: 'voice', value: values.voice }}
|
|
||||||
form={{ setFieldValue }}
|
|
||||||
options={{ id: 'voice' }}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label className="block font-bold text-sm">Lyrics</label>
|
|
||||||
<BaseButton
|
|
||||||
type="button"
|
|
||||||
label={isGeneratingLyrics ? 'Writing...' : 'AI Generate Lyrics'}
|
|
||||||
icon={mdiMagic}
|
|
||||||
small
|
|
||||||
color="info"
|
|
||||||
onClick={() => handleGenerateLyrics(values, setFieldValue)}
|
|
||||||
disabled={isGeneratingLyrics || values.generation_mode === 'auto_lyrics'}
|
|
||||||
className="text-xs py-1 bg-indigo-600 hover:bg-indigo-700 border-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Field
|
|
||||||
as="textarea"
|
|
||||||
name="lyrics_text"
|
|
||||||
placeholder={values.generation_mode === 'auto_lyrics' ? "AI will generate full lyrics during processing..." : "Enter your lyrics here..."}
|
|
||||||
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg p-4 h-64 font-mono text-sm focus:ring-emerald-500 border focus:border-emerald-500 outline-none transition-all resize-none"
|
|
||||||
disabled={values.generation_mode === 'auto_lyrics'}
|
|
||||||
/>
|
|
||||||
{errors.lyrics_text && touched.lyrics_text && (
|
|
||||||
<div className="text-red-500 text-xs mt-1">{errors.lyrics_text}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-slate-800">
|
|
||||||
<BaseButton
|
|
||||||
type="submit"
|
|
||||||
color="info"
|
|
||||||
label={isGenerating ? 'Initializing AI Engine...' : 'Generate Full Song'}
|
|
||||||
icon={mdiAutoFix}
|
|
||||||
className="w-full py-4 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-black text-lg border-none shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
|
|
||||||
disabled={isGenerating}
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] text-center mt-3 text-slate-500 uppercase tracking-widest font-bold">
|
|
||||||
Lyrics • Melody • Arrangement • AI Vocals • Mix & Master
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<CardBox className="border-slate-800 bg-slate-900/30">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h3 className="text-lg font-bold text-white flex items-center">
|
|
||||||
<BaseIcon path={mdiHistory} className="mr-2 text-emerald-500" />
|
|
||||||
Recent Creations
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{songsLoading ? (
|
|
||||||
<div className="flex flex-col items-center justify-center py-20 space-y-4">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-500" />
|
|
||||||
<span className="text-xs text-slate-500 font-bold tracking-widest uppercase">Accessing Vault</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{songs?.map((song: any) => {
|
|
||||||
const hasAudio = song.media_assets_song?.some((a: any) => a.asset_type.startsWith('audio'));
|
|
||||||
const isReady = song.status === 'ready';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={song.id} className="p-4 rounded-2xl bg-slate-800/40 border border-slate-700 hover:border-emerald-500/40 transition-all duration-300 group">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex-1 min-w-0 mr-4">
|
|
||||||
<div className="font-black text-sm text-white truncate group-hover:text-emerald-400 transition-colors">{song.song_title}</div>
|
|
||||||
<div className="flex items-center mt-1 space-x-2">
|
|
||||||
<span className="text-[9px] px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 font-bold uppercase tracking-tighter">
|
|
||||||
{song.style?.style_name || 'Genre'}
|
|
||||||
</span>
|
|
||||||
<span className="text-[9px] text-slate-500 font-bold uppercase tracking-tighter">
|
|
||||||
{song.era?.era_name || 'Era'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2 shrink-0">
|
|
||||||
<BaseButton
|
|
||||||
icon={playingSongId === song.id ? mdiPause : mdiPlay}
|
|
||||||
color="info"
|
|
||||||
small
|
|
||||||
roundedFull
|
|
||||||
onClick={() => togglePlay(song)}
|
|
||||||
disabled={!isReady && !hasAudio}
|
|
||||||
className={`${isReady || hasAudio ? 'bg-emerald-500 hover:bg-emerald-600 text-slate-950' : 'bg-slate-700 text-slate-500'} border-none shadow-sm`}
|
|
||||||
/>
|
|
||||||
<BaseButton
|
|
||||||
icon={mdiDownload}
|
|
||||||
color="white"
|
|
||||||
small
|
|
||||||
roundedFull
|
|
||||||
onClick={() => handleDownload(song)}
|
|
||||||
disabled={!isReady && !hasAudio}
|
|
||||||
className="bg-slate-700 hover:bg-slate-600 text-white border-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative pt-1">
|
|
||||||
<div className="flex items-center justify-between text-[9px] mb-1 px-1">
|
|
||||||
<span className="text-slate-500 font-bold uppercase tracking-widest">Process Status</span>
|
|
||||||
<span className={`font-black uppercase tracking-widest ${isReady ? 'text-emerald-400' : 'text-amber-400 animate-pulse'}`}>
|
|
||||||
{song.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={`h-full rounded-full transition-all duration-1000 ${isReady ? 'bg-emerald-500' : 'bg-gradient-to-r from-amber-500 to-emerald-500 animate-pulse'}`}
|
|
||||||
style={{ width: isReady ? '100%' : '45%' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{songs?.length === 0 && (
|
|
||||||
<div className="text-center py-20 text-slate-600">
|
|
||||||
<BaseIcon path={mdiMusic} size={64} className="mx-auto mb-4 opacity-10" />
|
|
||||||
<p className="font-bold tracking-widest uppercase text-xs">Studio Empty</p>
|
|
||||||
<p className="text-[10px] mt-2 italic">Your future hits will appear here</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-8 pt-6 border-t border-slate-800">
|
|
||||||
<BaseButton
|
|
||||||
href="/songs/songs-list"
|
|
||||||
label="Enter Audio Archive"
|
|
||||||
color="white"
|
|
||||||
className="w-full text-[10px] font-black uppercase tracking-widest text-slate-500 bg-transparent border-slate-800 hover:bg-slate-800 hover:text-white transition-all"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardBox>
|
|
||||||
|
|
||||||
<CardBox className="bg-gradient-to-br from-indigo-900/30 to-purple-900/30 border-indigo-500/20">
|
|
||||||
<div className="flex items-center space-x-3 text-indigo-400 mb-3">
|
|
||||||
<BaseIcon path={mdiMicrophone} size={28} />
|
|
||||||
<h4 className="font-black uppercase tracking-widest text-sm">Producer Tip</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-400 leading-relaxed font-medium">
|
|
||||||
Our AI engine performs best when you provide descriptive titles. Instead of "Song 1", try something like "Electric Dreams in the Rain".
|
|
||||||
</p>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionMain>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
StudioPage.getLayout = function getLayout(page: ReactElement) {
|
|
||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StudioPage;
|
|
||||||
@ -430,8 +430,96 @@ const UsersView = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<>
|
||||||
|
<p className={'block font-bold mb-2'}>Admin_access_keys IssuedToUser</p>
|
||||||
|
<CardBox
|
||||||
|
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||||
|
hasTable
|
||||||
|
>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
|
||||||
|
<th>KeyName</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>KeyHash</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>IsActive</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>IssuedAt</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>LastUsedAt</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>UsageCount</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.admin_access_keys_issued_to_user && Array.isArray(users.admin_access_keys_issued_to_user) &&
|
||||||
|
users.admin_access_keys_issued_to_user.map((item: any) => (
|
||||||
|
<tr key={item.id} onClick={() => router.push(`/admin_access_keys/admin_access_keys-view/?id=${item.id}`)}>
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="key_name">
|
||||||
|
{ item.key_name }
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="key_hash">
|
||||||
|
{ item.key_hash }
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="is_active">
|
||||||
|
{ dataFormatter.booleanFormatter(item.is_active) }
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="issued_at">
|
||||||
|
{ dataFormatter.dateTimeFormatter(item.issued_at) }
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="last_used_at">
|
||||||
|
{ dataFormatter.dateTimeFormatter(item.last_used_at) }
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="usage_count">
|
||||||
|
{ item.usage_count }
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{!users?.admin_access_keys_issued_to_user?.length && <div className={'text-center py-4'}>No data</div>}
|
||||||
|
</CardBox>
|
||||||
|
</>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -472,4 +560,4 @@ UsersView.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UsersView;
|
export default UsersView;
|
||||||
Loading…
x
Reference in New Issue
Block a user