import React, { useMemo, useState } from 'react' import { Platform, View } from 'react-native' import { responsiveHeight } from 'react-native-responsive-dimensions' import { background } from '../../assets' import GradientButton from '../../components/GradientButton' import MusicLandHeader from '../../components/MusicLandHeader' import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from '../../data/data' import Page from '../../layouts/Page' import { goBack } from '../../navigation/NavigationService' import { useUser } from '../../providers/UserDataProvider' import { gutters } from '../../styles' import { sanitizeStructureList } from '../../utils/songStructure' import CreatingLyrics from './CreatingLyrics' import CustomizeSongStructure from './CustomizeSongStructure' import EmotionConvey from './EmotionConvey' import Goals from './Goals' import Rhymes from './Rhymes' import SongStructure from './SongStructure' import SongStyle from './SongStyle' import SongTo from './SongTo' import SpecificityContext from './SpecificityContext' const MAX_STEP_INDEX = 8 const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false } const CreateLyricsWithAi = () => { const { selectedProject } = useUser() const [selectedIndex, setSelectedIndex] = useState(0) const handleLyricsError = React.useCallback(() => { setSelectedIndex(7) }, [setSelectedIndex]) // Collected state across steps const [objective, setObjective] = useState(null) // from Goals list const [otherObjective, setOtherObjective] = useState('') const [context, setContext] = useState('') const [emotion, setEmotion] = useState(null) const [style, setStyle] = useState(null) // from list const [otherStyle, setOtherStyle] = useState('') const [audience, setAudience] = useState('') const [structure, setStructure] = useState(null) // selected structure string const [rhymes, setRhymes] = useState(null) const [customStructure, setCustomStructure] = useState(null) // array like ['couplet','refrain'] const trimmedOtherObjective = typeof otherObjective === 'string' ? otherObjective.trim() : '' const shouldUseOtherObjective = objective === OTHER_OBJECTIVE_OPTION const persistedOtherObjective = shouldUseOtherObjective ? trimmedOtherObjective : '' const trimmedOtherStyle = typeof otherStyle === 'string' ? otherStyle.trim() : '' const shouldUseOtherStyle = style === OTHER_STYLE_OPTION const persistedOtherStyle = shouldUseOtherStyle ? trimmedOtherStyle : '' // Pré-remplir les états depuis le projet sélectionné si disponibles React.useEffect(() => { if (!selectedProject) return const sel = selectedProject?.selections || {} const cfg = selectedProject?.config || {} const rawOtherObjective = typeof sel.otherObjective === 'string' ? sel.otherObjective : '' const trimmedSavedOtherObjective = rawOtherObjective.trim() const hasSavedOtherObjective = trimmedSavedOtherObjective.length > 0 const savedObjective = typeof sel.objective === 'string' && sel.objective.trim().length > 0 ? sel.objective.trim() : null if (!otherObjective && hasSavedOtherObjective) { setOtherObjective(rawOtherObjective) } const shouldSelectOtherObjective = hasSavedOtherObjective && (!savedObjective || savedObjective === OTHER_OBJECTIVE_OPTION) const resolvedSavedObjective = shouldSelectOtherObjective ? OTHER_OBJECTIVE_OPTION : savedObjective if (resolvedSavedObjective && objective !== resolvedSavedObjective) { setObjective(resolvedSavedObjective) } else if (!resolvedSavedObjective && objective !== null) { setObjective(null) } if (!context && typeof sel.context === 'string') setContext(sel.context) // Emotion: accepter objet {title, description} ou string "Titre : description" if (emotion == null && sel.emotion) { if (typeof sel.emotion === 'object' && sel.emotion.title) { setEmotion({ title: sel.emotion.title, description: sel.emotion.description || '', }) } else if (typeof sel.emotion === 'string') { const [t, d] = sel.emotion.split(':') const title = (t || '').trim() const description = (d || '').trim() if (title) setEmotion({ title, description }) } } const rawOtherStyle = typeof sel.otherStyle === 'string' ? sel.otherStyle : '' const trimmedSavedOtherStyle = rawOtherStyle.trim() const hasSavedOtherStyle = trimmedSavedOtherStyle.length > 0 const savedStyle = typeof sel.style === 'string' && sel.style.trim().length > 0 ? sel.style.trim() : null if (!otherStyle && hasSavedOtherStyle) { setOtherStyle(rawOtherStyle) } const shouldSelectOtherStyle = hasSavedOtherStyle && (!savedStyle || savedStyle === OTHER_STYLE_OPTION) const resolvedSavedStyle = shouldSelectOtherStyle ? OTHER_STYLE_OPTION : savedStyle if (resolvedSavedStyle && style !== resolvedSavedStyle) { setStyle(resolvedSavedStyle) } else if (!resolvedSavedStyle && style !== null) { setStyle(null) } if (!audience && typeof sel.audience === 'string') setAudience(sel.audience) // Structure choisie (string) si déjà enregistrée dans selections if (structure == null && typeof sel.structure === 'string' && sel.structure) setStructure(sel.structure) // Rimes if (rhymes == null && typeof sel.rhymes === 'string' && sel.rhymes) setRhymes(sel.rhymes) // Structure personnalisée: prioriser selections.customStructure puis config.structure const savedCustom = Array.isArray(sel.customStructure) ? sanitizeStructureList(sel.customStructure, CUSTOM_SANITIZE_OPTIONS) : null const cfgStructure = Array.isArray(cfg.structure) ? sanitizeStructureList(cfg.structure, CUSTOM_SANITIZE_OPTIONS) : null const fallbackStructure = savedCustom && savedCustom.length ? savedCustom : cfgStructure if (!Array.isArray(customStructure) && fallbackStructure?.length) { setCustomStructure(fallbackStructure) } else if ( Array.isArray(customStructure) && fallbackStructure?.length && (customStructure.length !== fallbackStructure.length || customStructure.some((value, idx) => value !== fallbackStructure[idx])) ) { setCustomStructure(fallbackStructure) } }, [selectedProject]) React.useEffect(() => { console.debug('[CreateLyricsWithAi.web] structure state', { structure, customStructure, }) }, [structure, customStructure]) const parsedStructure = useMemo(() => { // Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain" try { if (!structure || typeof structure !== 'string') return null const parts = structure.split(',') const result = [] parts.forEach((seg) => { const s = seg.trim().toLowerCase() const coupletMatch = s.match(/(\d+)\s+couplet/) const refrainMatch = s.match(/(\d+)\s+refrain/) if (coupletMatch) { const count = parseInt(coupletMatch[1], 10) for (let i = 0; i < count; i++) result.push('couplet') } if (refrainMatch) { const count = parseInt(refrainMatch[1], 10) for (let i = 0; i < count; i++) result.push('refrain') } }) const sanitizedResult = sanitizeStructureList(result) return sanitizedResult.length ? sanitizedResult : null } catch (e) { return null } }, [structure]) const sanitizedCustomStructure = useMemo(() => { if (!Array.isArray(customStructure) || customStructure.length === 0) { return [] } return sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS) }, [customStructure]) const sanitizedParsedStructure = useMemo(() => { if (!Array.isArray(parsedStructure) || parsedStructure.length === 0) { return [] } return sanitizeStructureList(parsedStructure, CUSTOM_SANITIZE_OPTIONS) }, [parsedStructure]) const fallbackProjectStructure = useMemo(() => { const raw = selectedProject?.config?.structure if (!Array.isArray(raw) || raw.length === 0) return [] return sanitizeStructureList(raw, CUSTOM_SANITIZE_OPTIONS) }, [selectedProject]) const baseStructureForCustomizer = useMemo(() => { if (sanitizedCustomStructure.length > 0) return sanitizedCustomStructure if (sanitizedParsedStructure.length > 0) return sanitizedParsedStructure return fallbackProjectStructure }, [sanitizedCustomStructure, sanitizedParsedStructure, fallbackProjectStructure]) const progress = useMemo(() => 16 + selectedIndex * 9, [selectedIndex]) const lyricsConfig = useMemo(() => { const resolvedObjective = shouldUseOtherObjective ? persistedOtherObjective || undefined : objective || undefined const resolvedStyle = shouldUseOtherStyle ? persistedOtherStyle || undefined : style || undefined const sanitizedCustom = sanitizedCustomStructure const sanitizedParsed = sanitizedParsedStructure return { objective: resolvedObjective, context: context?.trim() ? context.trim() : undefined, emotion: emotion?.title && emotion?.description ? `${emotion.title} : ${emotion.description}` : undefined, style: resolvedStyle, audience: audience?.trim() ? audience.trim() : undefined, structure: sanitizedCustom.length > 0 ? sanitizedCustom : sanitizedParsed.length > 0 ? sanitizedParsed : undefined, rhymes: rhymes || undefined, } }, [ shouldUseOtherObjective, persistedOtherObjective, objective, shouldUseOtherStyle, persistedOtherStyle, context, emotion, style, audience, sanitizedParsedStructure, rhymes, sanitizedCustomStructure, ]) const isNextDisabled = React.useMemo(() => { switch (selectedIndex) { case 0: { const hasObjective = typeof objective === 'string' && objective.length > 0 const hasOther = trimmedOtherObjective.length > 0 return !(hasObjective || hasOther) } case 1: { const hasContext = typeof context === 'string' && context.trim().length > 0 return !hasContext } case 2: { const hasEmotion = (emotion && typeof emotion === 'object' && !!emotion.title) || (typeof emotion === 'string' && emotion.trim().length > 0) return !hasEmotion } case 3: { const hasStyle = typeof style === 'string' && style.length > 0 const hasOther = trimmedOtherStyle.length > 0 return !(hasStyle || hasOther) } case 4: { const hasAudience = typeof audience === 'string' && audience.trim().length > 0 return !hasAudience } case 5: { const hasStructure = typeof structure === 'string' && structure.length > 0 return !hasStructure } case 7: { const hasRhymes = typeof rhymes === 'string' && rhymes.length > 0 return !hasRhymes } default: return false } }, [ selectedIndex, objective, trimmedOtherObjective, context, emotion, style, trimmedOtherStyle, audience, structure, rhymes, ]) const onPressNext = async () => { setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX)) } const onPressBack = () => { if (selectedIndex > 0) { setSelectedIndex((idx) => Math.max(0, idx - 1)) } else { goBack() } } const steps = [ { key: 'goals', render: () => ( ), }, { key: 'context', render: () => , }, { key: 'emotion', render: () => , }, { key: 'style', render: () => ( ), }, { key: 'audience', render: () => , }, { key: 'structure', render: () => , }, { key: 'customStructure', render: () => ( ), }, { key: 'rhymes', render: () => , }, { key: 'creating', render: () => ( ), }, ] const currentStep = steps[selectedIndex] return ( {currentStep ? ( {currentStep.render()} ) : null} {selectedIndex !== 8 && ( )} ) } export default CreateLyricsWithAi