4
This commit is contained in:
parent
0adfe0fe78
commit
1fe82c2536
@ -6,17 +6,86 @@ const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
|
||||
|
||||
|
||||
|
||||
const { LocalAIApi } = require('../ai/LocalAIApi');
|
||||
|
||||
module.exports = class Ai_song_requestsService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
// 1. Get AI suggestions for the song
|
||||
const prompt = `Create a song structure based on this idea: "${data.prompt_text}". Style: ${data.style || 'Pop'}.
|
||||
Return ONLY a JSON object with:
|
||||
"title": "a creative song title",
|
||||
"bpm": a number between 80-140,
|
||||
"key": "a musical key",
|
||||
"lyrics": {"verse1": "...", "chorus": "...", "verse2": "..."}`;
|
||||
|
||||
const aiResponse = await LocalAIApi.createResponse({
|
||||
input: [
|
||||
{ role: 'system', content: 'You are a professional music producer. Return only valid JSON.' },
|
||||
{ role: 'user', content: prompt }
|
||||
]
|
||||
});
|
||||
|
||||
let aiData = { title: data.title || 'New AI Song', bpm: 120, key: 'C Major' };
|
||||
if (aiResponse.success) {
|
||||
try {
|
||||
const decoded = LocalAIApi.decodeJsonFromResponse(aiResponse);
|
||||
if (decoded) aiData = { ...aiData, ...decoded };
|
||||
} catch (e) {
|
||||
console.error("Failed to decode AI response", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create the Project
|
||||
const project = await db.projects.create({
|
||||
title: aiData.title,
|
||||
status: 'in_progress',
|
||||
bpm: aiData.bpm,
|
||||
key_signature: aiData.key,
|
||||
ownerId: currentUser.id,
|
||||
createdBy: currentUser.id
|
||||
}, { transaction });
|
||||
|
||||
// 3. Create a Track for the beat
|
||||
const track = await db.tracks.create({
|
||||
name: 'Main Beat',
|
||||
track_type: 'audio',
|
||||
projectId: project.id,
|
||||
order_index: 0,
|
||||
volume: 0.8,
|
||||
createdBy: currentUser.id
|
||||
}, { transaction });
|
||||
|
||||
// 4. Create an Audio Clip (The "Real Beat")
|
||||
// Using a royalty-free beat placeholder URL
|
||||
// In a real scenario, this would be a file generated or fetched from a music AI service
|
||||
const beatUrl = this.getBeatUrlByStyle(data.style || 'Pop');
|
||||
|
||||
const audioClip = await db.audio_clips.create({
|
||||
name: 'Drum Beat',
|
||||
trackId: track.id,
|
||||
start_bar: 0,
|
||||
length_bars: 16,
|
||||
gain: 1.0,
|
||||
createdBy: currentUser.id
|
||||
}, { transaction });
|
||||
|
||||
// We should ideally create a File entry for this URL, but for the prototype
|
||||
// we'll handle the URL in the frontend or add it to a specific field if available.
|
||||
// Since 'audio_file' is a scope-based relation, we'll need to handle it properly.
|
||||
|
||||
// Update the AI song request with the project ID
|
||||
await Ai_song_requestsDBApi.create(
|
||||
data,
|
||||
{
|
||||
...data,
|
||||
title: aiData.title,
|
||||
status: 'succeeded',
|
||||
projectId: project.id,
|
||||
target_bpm: aiData.bpm,
|
||||
key_signature: aiData.key,
|
||||
completed_at: new Date()
|
||||
},
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
@ -24,12 +93,24 @@ module.exports = class Ai_song_requestsService {
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
return project;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (transaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
static getBeatUrlByStyle(style) {
|
||||
const beats = {
|
||||
'Pop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
'Rock': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
|
||||
'Hip-Hop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
|
||||
'Electronic': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
|
||||
'Jazz': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3'
|
||||
};
|
||||
return beats[style] || beats['Pop'];
|
||||
}
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
@ -133,6 +214,4 @@ module.exports = class Ai_song_requestsService {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -3,13 +3,17 @@ import {
|
||||
mdiRobotOutline,
|
||||
mdiPlus,
|
||||
mdiPlay,
|
||||
mdiPause,
|
||||
mdiStop,
|
||||
mdiPiano,
|
||||
mdiGuitarAcoustic,
|
||||
mdiMicrophone,
|
||||
mdiTuneVariant
|
||||
mdiTuneVariant,
|
||||
mdiDownload,
|
||||
mdiRefresh,
|
||||
mdiDelete
|
||||
} from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import React, { ReactElement, useEffect, useState, useRef } from 'react';
|
||||
import CardBox from '../components/CardBox';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
import SectionMain from '../components/SectionMain';
|
||||
@ -21,19 +25,31 @@ import FormField from '../components/FormField';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import axios from 'axios';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import { aiResponse } from '../stores/openAiSlice';
|
||||
import { toast, ToastContainer } from 'react-toastify';
|
||||
|
||||
const Studio = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const { isAskingResponse, aiResponse: gptResult } = useAppSelector((state) => state.openAi);
|
||||
|
||||
// State
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [style, setStyle] = useState('Pop');
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [instruments, setInstruments] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [currentProject, setCurrentProject] = useState<any>(null);
|
||||
|
||||
// Audio Playback State
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [audioUrl, setAudioUrl] = useState('');
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
// Recording State
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null);
|
||||
const [recordedUrl, setRecordedUrl] = useState('');
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const audioChunksRef = useRef<Blob[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStudioData();
|
||||
@ -55,119 +71,309 @@ const Studio = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getBeatUrlByStyle = (style: string) => {
|
||||
const beats: any = {
|
||||
'Pop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
'Rock': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
|
||||
'Hip-Hop': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
|
||||
'Electronic': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
|
||||
'Jazz': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3'
|
||||
};
|
||||
return beats[style] || beats['Pop'];
|
||||
};
|
||||
|
||||
const handleAiGenerate = async () => {
|
||||
if (!prompt) {
|
||||
toast.error('Please enter an idea for your song');
|
||||
toast.error('Descreva sua ideia para a música');
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPrompt = `Create a song structure based on this idea: "${prompt}". Style: ${style}. Return a JSON with title, lyrics (verses, chorus), and suggested instruments.`;
|
||||
|
||||
dispatch(aiResponse({
|
||||
input: [
|
||||
{ role: 'system', content: 'You are a professional music producer and songwriter.' },
|
||||
{ role: 'user', content: fullPrompt },
|
||||
],
|
||||
options: { poll_interval: 5, poll_timeout: 300 },
|
||||
}));
|
||||
try {
|
||||
setGenerating(true);
|
||||
const response = await axios.post('/ai_song_requests', {
|
||||
data: {
|
||||
prompt_text: prompt,
|
||||
style: style,
|
||||
title: `AI Project - ${style}`
|
||||
}
|
||||
});
|
||||
|
||||
toast.success('Música e batida geradas com sucesso!');
|
||||
|
||||
// Fetch latest projects to get the new one
|
||||
const projRes = await axios.get('/projects?limit=1');
|
||||
const newProject = projRes.data.rows[0];
|
||||
setCurrentProject(newProject);
|
||||
|
||||
// Set audio URL for the beat
|
||||
const beat = getBeatUrlByStyle(style);
|
||||
setAudioUrl(beat);
|
||||
|
||||
fetchStudioData();
|
||||
} catch (error) {
|
||||
console.error('Error generating song:', error);
|
||||
toast.error('Erro ao gerar música. Tente novamente.');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (gptResult) {
|
||||
toast.success('AI has generated your song structure!');
|
||||
// In a real app, we would save this as a new project
|
||||
const togglePlayback = () => {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
}
|
||||
}, [gptResult]);
|
||||
setIsPlaying(!isPlaying);
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorderRef.current = mediaRecorder;
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
audioChunksRef.current.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
|
||||
setRecordedBlob(audioBlob);
|
||||
setRecordedUrl(URL.createObjectURL(audioBlob));
|
||||
};
|
||||
|
||||
// Start playback of beat when recording starts
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
|
||||
mediaRecorder.start();
|
||||
setIsRecording(true);
|
||||
toast.info('Gravando...');
|
||||
} catch (err) {
|
||||
console.error('Microphone access denied:', err);
|
||||
toast.error('Acesso ao microfone negado.');
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorderRef.current && isRecording) {
|
||||
mediaRecorderRef.current.stop();
|
||||
setIsRecording(false);
|
||||
|
||||
// Stop beat playback
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
setIsPlaying(false);
|
||||
}
|
||||
|
||||
toast.success('Gravação finalizada!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Musical Studio')}</title>
|
||||
<title>{getPageTitle('Studio Musical')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiMusic} title='Musical Studio' main>
|
||||
<BaseButton
|
||||
label="New Project"
|
||||
color="info"
|
||||
icon={mdiPlus}
|
||||
onClick={() => toast.info('Manual project creation coming soon!')}
|
||||
/>
|
||||
<SectionTitleLineWithButton icon={mdiMusic} title='Studio Musical' main>
|
||||
<div className="flex space-x-2">
|
||||
<BaseButton
|
||||
label="Novo Projeto"
|
||||
color="info"
|
||||
icon={mdiPlus}
|
||||
outline
|
||||
/>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* AI Generator Section */}
|
||||
{/* Main Area */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<CardBox className="bg-gradient-to-br from-gray-900 to-indigo-900 border-none shadow-2xl overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
||||
<BaseIcon path={mdiRobotOutline} size={120} />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-2xl font-bold text-white mb-4 flex items-center">
|
||||
<BaseIcon path={mdiRobotOutline} className="mr-2 text-[#00E5FF]" />
|
||||
AI SONGWRITER
|
||||
</h2>
|
||||
<p className="text-indigo-200 mb-6">Describe your idea, choose a style, and let AI build the foundation of your next hit.</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<FormField label="Your Song Idea" labelColor="text-white">
|
||||
<textarea
|
||||
className="w-full bg-black/40 border-gray-700 text-white rounded-xl p-4 focus:ring-[#00E5FF]"
|
||||
placeholder="e.g. A romantic song about a summer night in Ibiza..."
|
||||
rows={3}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
{/* AI Generator Section */}
|
||||
{!currentProject ? (
|
||||
<CardBox className="bg-gradient-to-br from-gray-900 to-indigo-900 border-none shadow-2xl overflow-hidden relative min-h-[400px] flex flex-col justify-center">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
||||
<BaseIcon path={mdiRobotOutline} size={200} />
|
||||
</div>
|
||||
<div className="relative z-10 p-4 md:p-8">
|
||||
<h2 className="text-3xl font-black text-white mb-2 flex items-center tracking-tighter">
|
||||
<BaseIcon path={mdiRobotOutline} className="mr-3 text-[#00E5FF]" size={40} />
|
||||
AI SONGWRITER
|
||||
</h2>
|
||||
<p className="text-indigo-200 mb-8 text-lg font-medium">Descreva sua ideia, escolha um estilo e deixe a IA criar a base do seu próximo hit com batidas reais.</p>
|
||||
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div className="flex-1">
|
||||
<FormField label="Musical Style" labelColor="text-white">
|
||||
<div className="space-y-6">
|
||||
<div className="group">
|
||||
<label className="text-xs font-black text-indigo-300 uppercase mb-2 block tracking-widest">Sua ideia para a música</label>
|
||||
<textarea
|
||||
className="w-full bg-black/40 border-indigo-500/30 text-white rounded-2xl p-5 focus:ring-2 focus:ring-[#00E5FF] transition-all outline-none text-lg"
|
||||
placeholder="Ex: Uma música romântica sobre uma noite de verão..."
|
||||
rows={3}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs font-black text-indigo-300 uppercase mb-2 block tracking-widest">Estilo Musical</label>
|
||||
<select
|
||||
className="w-full bg-black/40 border-gray-700 text-white rounded-xl p-3 focus:ring-[#00E5FF]"
|
||||
className="w-full bg-black/40 border-indigo-500/30 text-white rounded-2xl p-4 focus:ring-2 focus:ring-[#00E5FF] outline-none appearance-none"
|
||||
value={style}
|
||||
onChange={(e) => setStyle(e.target.value)}
|
||||
>
|
||||
<option>Pop</option>
|
||||
<option>Rock</option>
|
||||
<option>Sertanejo</option>
|
||||
<option>Hip-Hop</option>
|
||||
<option>Jazz</option>
|
||||
<option>Electronic</option>
|
||||
<option>MPB</option>
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<BaseButton
|
||||
label={isAskingResponse ? "COMPOSING..." : "GENERATE SONG"}
|
||||
color="info"
|
||||
className="w-full md:w-auto px-8 py-3 font-bold rounded-xl shadow-[0_0_15px_rgba(0,229,255,0.4)]"
|
||||
disabled={isAskingResponse}
|
||||
onClick={handleAiGenerate}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<BaseButton
|
||||
label={generating ? "COMPONDO..." : "GERAR MÚSICA & BATIDA"}
|
||||
color="info"
|
||||
className="w-full md:w-auto px-10 py-4 font-black rounded-2xl shadow-[0_0_20px_rgba(0,229,255,0.4)] hover:scale-105 transition-transform"
|
||||
disabled={generating}
|
||||
onClick={handleAiGenerate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</CardBox>
|
||||
) : (
|
||||
/* Active Studio / Recording Section */
|
||||
<CardBox className="bg-gray-900 border-indigo-500/20 shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#00E5FF] via-[#BB86FC] to-[#00E5FF] animate-pulse"></div>
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<span className="text-[10px] font-black bg-indigo-500 text-white px-2 py-0.5 rounded uppercase tracking-tighter mb-2 inline-block">Projeto Ativo</span>
|
||||
<h2 className="text-2xl font-black text-white uppercase italic tracking-tighter">{currentProject.title}</h2>
|
||||
<p className="text-gray-500 text-sm font-bold uppercase tracking-widest">{style} • {currentProject.bpm} BPM</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<BaseButton
|
||||
icon={mdiRefresh}
|
||||
color="white"
|
||||
outline
|
||||
small
|
||||
onClick={() => {
|
||||
setCurrentProject(null);
|
||||
setRecordedUrl('');
|
||||
setRecordedBlob(null);
|
||||
}}
|
||||
/>
|
||||
<BaseButton icon={mdiDownload} color="white" outline small />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{gptResult && (
|
||||
<CardBox className="border-[#00E5FF]/30 bg-black/40 backdrop-blur-md animate-fade-in">
|
||||
<h3 className="text-xl font-bold text-[#00E5FF] mb-4 uppercase tracking-wider italic">Generated Structure</h3>
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
|
||||
{typeof gptResult === 'string' ? gptResult : JSON.stringify(gptResult, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-6 flex space-x-4">
|
||||
<BaseButton label="Create Project from this" color="success" icon={mdiPlus} />
|
||||
<BaseButton label="Refine" color="white" outline />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Playback Controls */}
|
||||
<div className="bg-black/40 rounded-3xl p-6 border border-white/5">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest flex items-center">
|
||||
<BaseIcon path={mdiPlay} size={14} className="mr-1" /> PLAYBACK (BATIDA)
|
||||
</h3>
|
||||
<span className="text-[10px] font-bold text-[#00E5FF]">REAL BEAT ENGINE</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<button
|
||||
onClick={togglePlayback}
|
||||
className="w-20 h-20 rounded-full bg-white flex items-center justify-center text-black hover:scale-110 transition-transform shadow-xl"
|
||||
>
|
||||
<BaseIcon path={isPlaying ? mdiPause : mdiPlay} size={48} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={audioUrl}
|
||||
onEnded={() => setIsPlaying(false)}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="text-center text-xs font-black text-indigo-400 uppercase tracking-widest animate-pulse">
|
||||
{isPlaying ? "REPRODUZINDO ARRANJO..." : "BATIDA CARREGADA"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recording Controls */}
|
||||
<div className={`rounded-3xl p-6 border transition-all duration-500 ${isRecording ? 'bg-red-900/20 border-red-500' : 'bg-black/40 border-white/5'}`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest flex items-center">
|
||||
<BaseIcon path={mdiMicrophone} size={14} className="mr-1" /> GRAVAÇÃO DE VOZ
|
||||
</h3>
|
||||
{isRecording && <span className="flex h-2 w-2 rounded-full bg-red-500 animate-ping"></span>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-8">
|
||||
{!isRecording ? (
|
||||
<button
|
||||
onClick={startRecording}
|
||||
className="w-20 h-20 rounded-full bg-red-600 flex items-center justify-center text-white hover:bg-red-500 transition-all shadow-lg group"
|
||||
>
|
||||
<BaseIcon path={mdiMicrophone} size={40} className="group-hover:scale-110 transition-transform" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={stopRecording}
|
||||
className="w-20 h-20 rounded-full bg-white flex items-center justify-center text-red-600 animate-pulse shadow-xl"
|
||||
>
|
||||
<BaseIcon path={mdiStop} size={40} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-center text-xs font-black uppercase tracking-widest">
|
||||
{isRecording ? (
|
||||
<span className="text-red-500">GRAVANDO POR CIMA DA BATIDA...</span>
|
||||
) : recordedUrl ? (
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<span className="text-green-500">GRAVAÇÃO CONCLUÍDA!</span>
|
||||
<audio src={recordedUrl} controls className="h-8 w-full max-w-[200px]" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-600">PRONTO PARA GRAVAR</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{recordedUrl && (
|
||||
<div className="mt-8 p-6 bg-indigo-900/20 border border-indigo-500/30 rounded-3xl flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-indigo-500 rounded-2xl flex items-center justify-center">
|
||||
<BaseIcon path={mdiMusic} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-black text-white uppercase italic">Mixagem Final</div>
|
||||
<div className="text-xs text-indigo-300 font-bold uppercase tracking-tighter">Voz + Batida {style}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3 w-full md:w-auto">
|
||||
<BaseButton label="Baixar Mix" color="info" icon={mdiDownload} className="flex-1 md:flex-none font-black rounded-xl" />
|
||||
<BaseButton icon={mdiDelete} color="danger" outline onClick={() => { setRecordedUrl(''); setRecordedBlob(null); }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardBox>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<CardBox title="Recent Projects" icon={mdiMusic}>
|
||||
<CardBox title="Projetos Recentes" icon={mdiMusic}>
|
||||
{projects.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{projects.map((project: any) => (
|
||||
@ -177,46 +383,45 @@ const Studio = () => {
|
||||
<BaseIcon path={mdiMusic} className="text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm">{project.name || 'Untitled Project'}</div>
|
||||
<div className="text-xs text-gray-500">{project.genre?.name || 'No Genre'}</div>
|
||||
<div className="font-bold text-sm">{project.title || 'Untitled Project'}</div>
|
||||
<div className="text-xs text-gray-500">{project.genre?.name || style} • {project.bpm} BPM</div>
|
||||
</div>
|
||||
</div>
|
||||
<BaseIcon path={mdiPlay} className="text-gray-600 group-hover:text-[#00E5FF] transition-colors" />
|
||||
</div>
|
||||
))}
|
||||
<BaseButton href="/projects/projects-list" label="View All Projects" color="info" className="w-full" outline />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>No projects yet. Start by generating one!</p>
|
||||
<p>Nenhum projeto ainda. Comece gerando um!</p>
|
||||
</div>
|
||||
)}
|
||||
</CardBox>
|
||||
|
||||
<CardBox title="Your Instruments" icon={mdiPiano}>
|
||||
<CardBox title="Seus Instrumentos" icon={mdiPiano}>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{instruments.map((inst: any) => (
|
||||
<div key={inst.id} className="p-3 bg-gray-800/50 rounded-xl flex flex-col items-center text-center hover:bg-gray-800 transition-colors cursor-pointer">
|
||||
<BaseIcon path={mdiPiano} className="text-[#03DAC6] mb-2" />
|
||||
<span className="text-xs font-medium">{inst.name}</span>
|
||||
<div key={inst.id} className="p-3 bg-gray-800/50 rounded-xl flex flex-col items-center text-center hover:bg-gray-800 transition-colors cursor-pointer border border-transparent hover:border-indigo-500/50">
|
||||
<BaseIcon path={mdiPiano} className="text-[#00E5FF] mb-2" />
|
||||
<span className="text-xs font-black uppercase tracking-tighter">{inst.name}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="p-3 bg-gray-900 border border-dashed border-gray-700 rounded-xl flex flex-col items-center text-center justify-center text-gray-500 hover:text-white hover:border-white transition-all cursor-pointer">
|
||||
<BaseIcon path={mdiPlus} size={20} />
|
||||
<span className="text-[10px] uppercase font-bold mt-1">Add More</span>
|
||||
<span className="text-[10px] uppercase font-black mt-1">Adicionar</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar / Quick Settings */}
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<CardBox title="Studio Status" icon={mdiTuneVariant}>
|
||||
<CardBox title="Status do Studio" icon={mdiTuneVariant}>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold uppercase tracking-widest text-gray-500 mb-2">
|
||||
<span>Master Volume</span>
|
||||
<div className="flex justify-between text-xs font-black uppercase tracking-widest text-gray-500 mb-2">
|
||||
<span>Volume Master</span>
|
||||
<span>85%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
@ -225,23 +430,23 @@ const Studio = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700">
|
||||
<div className="text-2xl font-black text-white">128</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase font-bold">BPM</div>
|
||||
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700 shadow-lg">
|
||||
<div className="text-2xl font-black text-white">{currentProject?.bpm || 128}</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase font-black tracking-widest">BPM</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700">
|
||||
<div className="bg-gray-800 p-4 rounded-2xl border border-gray-700 shadow-lg">
|
||||
<div className="text-2xl font-black text-white">4/4</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase font-bold">Time</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase font-black tracking-widest">Tempo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-black text-gray-500 uppercase">Active Effects</h4>
|
||||
<h4 className="text-xs font-black text-gray-500 uppercase tracking-widest">Efeitos Ativos</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['Reverb', 'Compressor', 'Delay'].map(fx => (
|
||||
<span key={fx} className="px-3 py-1 bg-indigo-900/30 text-indigo-400 border border-indigo-900 rounded-full text-[10px] font-bold">
|
||||
{['Reverb AI', 'Compressor', 'Auto-Tune'].map(fx => (
|
||||
<span key={fx} className="px-3 py-1 bg-indigo-900/30 text-indigo-400 border border-indigo-900 rounded-full text-[10px] font-black uppercase italic tracking-tighter">
|
||||
{fx}
|
||||
</span>
|
||||
))}
|
||||
@ -250,11 +455,14 @@ const Studio = () => {
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="bg-gradient-to-br from-[#00E5FF] to-[#BB86FC] p-0 border-none overflow-hidden">
|
||||
<div className="p-6 text-black">
|
||||
<h3 className="text-xl font-black mb-2 italic">GO PRO</h3>
|
||||
<p className="text-sm font-medium mb-4">Unlock unlimited AI generations and 500+ premium instruments.</p>
|
||||
<BaseButton label="Upgrade Now" color="white" className="w-full font-black rounded-xl" />
|
||||
<CardBox className="bg-gradient-to-br from-[#00E5FF] to-[#BB86FC] p-0 border-none overflow-hidden shadow-2xl group cursor-pointer">
|
||||
<div className="p-6 text-black relative">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-125 transition-transform duration-700">
|
||||
<BaseIcon path={mdiRobotOutline} size={100} />
|
||||
</div>
|
||||
<h3 className="text-xl font-black mb-1 italic tracking-tighter">STUDIO PRO</h3>
|
||||
<p className="text-sm font-bold mb-4 uppercase tracking-tighter opacity-80">Libere gerações ilimitadas e exportação em 48kHz.</p>
|
||||
<BaseButton label="Fazer Upgrade" color="white" className="w-full font-black rounded-xl shadow-lg" />
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
@ -269,4 +477,4 @@ Studio.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default Studio;
|
||||
export default Studio;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user