diff --git a/src/screens/Writing/CreateLyricsWithAi.js b/src/screens/Writing/CreateLyricsWithAi.js index a8041ca..c5b3a74 100644 --- a/src/screens/Writing/CreateLyricsWithAi.js +++ b/src/screens/Writing/CreateLyricsWithAi.js @@ -30,7 +30,7 @@ const CreateLyricsWithAi = () => { const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0); const [progress, setProgress] = useState(16); const [parentLayout, setparentLayout] = useState(null); - const containerWidth = isWeb ? parentLayout?.width : windowWidth; + const containerWidth = windowWidth; // Collected state across steps const [objective, setObjective] = useState(null); // from Goals list @@ -50,11 +50,23 @@ const CreateLyricsWithAi = () => { const sel = selectedProject?.selections || {}; const cfg = selectedProject?.config || {}; - // Text inputs / choix simples - if (objective == null && typeof sel.objective === "string" && sel.objective) - setObjective(sel.objective); - if (!otherObjective && typeof sel.otherObjective === "string") - setOtherObjective(sel.otherObjective); + const rawOtherObjective = + typeof sel.otherObjective === "string" ? sel.otherObjective : ""; + const hasSavedOtherObjective = rawOtherObjective.trim().length > 0; + const savedObjective = + typeof sel.objective === "string" && sel.objective.trim().length > 0 + ? sel.objective + : null; + + if (!otherObjective && hasSavedOtherObjective) { + setOtherObjective(rawOtherObjective); + if (objective !== null) { + setObjective(null); + } + } else if (!hasSavedOtherObjective && objective == null && savedObjective) { + setObjective(savedObjective); + } + if (!context && typeof sel.context === "string") setContext(sel.context); // Emotion: accepter objet {title, description} ou string "Titre : description" @@ -72,10 +84,22 @@ const CreateLyricsWithAi = () => { } } - if (style == null && typeof sel.style === "string" && sel.style) - setStyle(sel.style); - if (!otherStyle && typeof sel.otherStyle === "string") - setOtherStyle(sel.otherStyle); + const rawOtherStyle = + typeof sel.otherStyle === "string" ? sel.otherStyle : ""; + const hasSavedOtherStyle = rawOtherStyle.trim().length > 0; + const savedStyle = + typeof sel.style === "string" && sel.style.trim().length > 0 + ? sel.style + : null; + + if (!otherStyle && hasSavedOtherStyle) { + setOtherStyle(rawOtherStyle); + if (style !== null) { + setStyle(null); + } + } else if (!hasSavedOtherStyle && style == null && savedStyle) { + setStyle(savedStyle); + } if (!audience && typeof sel.audience === "string") setAudience(sel.audience); diff --git a/src/screens/Writing/CreateLyricsWithAi.web.js b/src/screens/Writing/CreateLyricsWithAi.web.js new file mode 100644 index 0000000..fb2c89f --- /dev/null +++ b/src/screens/Writing/CreateLyricsWithAi.web.js @@ -0,0 +1,457 @@ +import React, { useMemo, useRef, useState } from "react"; +import { Dimensions, FlatList, 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 Page from "../../layouts/Page"; +import { Routes } from "../../navigation"; +import { goBack, navigate } from "../../navigation/NavigationService"; +import { useUser } from "../../providers/UserDataProvider"; +import { gutters } from "../../styles"; +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 { width: windowWidth } = Dimensions.get("window"); + +const CreateLyricsWithAi = () => { + const { selectedProject, updateProjectData } = useUser(); + const hasLyrics = selectedProject?.hasLyrics === true; + const scrollRef = useRef(null); + const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0); + const [progress, setProgress] = useState(16); + const [parentLayout, setParentLayout] = useState(null); + const containerWidth = parentLayout?.width || windowWidth || 1; + + // 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'] + + // 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 hasSavedOtherObjective = rawOtherObjective.trim().length > 0; + const savedObjective = + typeof sel.objective === "string" && sel.objective.trim().length > 0 + ? sel.objective + : null; + + if (!otherObjective && hasSavedOtherObjective) { + setOtherObjective(rawOtherObjective); + if (objective !== null) { + setObjective(null); + } + } else if (!hasSavedOtherObjective && objective == null && savedObjective) { + setObjective(savedObjective); + } + + 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 hasSavedOtherStyle = rawOtherStyle.trim().length > 0; + const savedStyle = + typeof sel.style === "string" && sel.style.trim().length > 0 + ? sel.style + : null; + + if (!otherStyle && hasSavedOtherStyle) { + setOtherStyle(rawOtherStyle); + if (style !== null) { + setStyle(null); + } + } else if (!hasSavedOtherStyle && style == null && savedStyle) { + setStyle(savedStyle); + } + 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) + ? sel.customStructure + : null; + const cfgStructure = Array.isArray(cfg.structure) ? cfg.structure : null; + if (!Array.isArray(customStructure) && (savedCustom || cfgStructure)) { + setCustomStructure(savedCustom || cfgStructure); + } + }, [selectedProject]); + + 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"); + } + }); + return result.length ? result : null; + } catch (e) { + return null; + } + }, [structure]); + + // Si déjà des paroles, forcer l'accès à partir de l'étape 5 et ignorer 0-4 + React.useEffect(() => { + if (hasLyrics && selectedIndex < 5) { + setSelectedIndex(5); + } + }, [hasLyrics]); + + const lyricsConfig = useMemo(() => { + return { + objective: otherObjective?.trim() + ? otherObjective.trim() + : objective || undefined, + context: context?.trim() ? context.trim() : undefined, + emotion: + emotion?.title && emotion?.description + ? `${emotion.title} : ${emotion.description}` + : undefined, + style: otherStyle?.trim() ? otherStyle.trim() : style || undefined, + audience: audience?.trim() ? audience.trim() : undefined, + structure: + (customStructure && + parsedStructure && + customStructure.length === parsedStructure.length + ? customStructure + : parsedStructure) || undefined, + rhymes: rhymes || undefined, + }; + }, [ + objective, + otherObjective, + context, + emotion, + style, + otherStyle, + audience, + parsedStructure, + rhymes, + customStructure, + ]); + + const isNextDisabled = React.useMemo(() => { + switch (selectedIndex) { + case 0: { + const hasObjective = + typeof objective === "string" && objective.length > 0; + const hasOther = + typeof otherObjective === "string" && + otherObjective.trim().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 = + typeof otherStyle === "string" && otherStyle.trim().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, + otherObjective, + context, + emotion, + style, + otherStyle, + audience, + structure, + rhymes, + ]); + + const onPressNext = async () => { + // Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6), + // sauter Rhymes et CreatingLyrics et aller directement sur Lyrics + if (hasLyrics && selectedIndex === 6) { + const chosenStructure = + (customStructure && + parsedStructure && + customStructure.length === parsedStructure.length + ? customStructure + : parsedStructure) || []; + await updateProjectData({ + config: { structure: chosenStructure }, + selections: { + objective, + otherObjective, + context, + emotion, + style, + otherStyle, + audience, + structure, + parsedStructure, + customStructure, + rhymes, + }, + hasLyrics: true, + }); + navigate(Routes.Lyrics); + return; + } + setSelectedIndex((idx) => idx + 1); + }; + + const onPressBack = () => { + // If currently on SongStructure, go back to previous screen instead of previous step + if (selectedIndex === 5) { + goBack(); + } else 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 getItemLayout = React.useCallback( + (_, index) => ({ + length: containerWidth, + offset: containerWidth * index, + index, + }), + [containerWidth], + ); + + // Sync scroll position and progress with selectedIndex + React.useEffect(() => { + if (!parentLayout?.height) return; + try { + const nextProgress = 16 + selectedIndex * 9; + setProgress(nextProgress); + scrollRef.current?.scrollToIndex?.({ + index: selectedIndex, + animated: true, + viewPosition: 0, + }); + } catch (_) {} + }, [selectedIndex, parentLayout?.height, containerWidth]); + + return ( + + + + setParentLayout(e.nativeEvent.layout)} + > + item.key} + horizontal + pagingEnabled + scrollEnabled={false} + showsHorizontalScrollIndicator={false} + initialScrollIndex={selectedIndex} + getItemLayout={getItemLayout} + style={{ width: containerWidth }} + renderItem={({ item }) => ( + + {item.render()} + + )} + /> + + {selectedIndex !== 8 && ( + + )} + + + ); +}; + +export default CreateLyricsWithAi; diff --git a/src/screens/Writing/Goals.js b/src/screens/Writing/Goals.js index cc3d00d..e2f2110 100644 --- a/src/screens/Writing/Goals.js +++ b/src/screens/Writing/Goals.js @@ -19,6 +19,13 @@ const Goals = ({ const selected = selectedProp ?? internalSelected; const setSelected = setSelectedProp ?? setInternalSelected; + const handleOtherObjectiveChange = (value) => { + setOtherObjective?.(value); + if (value?.trim()?.length && selected != null) { + setSelected(null); + } + }; + // Selection handled by ListSelection return ( @@ -41,7 +48,7 @@ const Goals = ({ label="Tu as un autre objectif?" placeholder="Décrire l’objectif" value={otherObjective} - setValue={setOtherObjective} + setValue={handleOtherObjectiveChange} /> ); diff --git a/src/screens/Writing/SongStyle.js b/src/screens/Writing/SongStyle.js index 8ea4bc2..88d7d6d 100644 --- a/src/screens/Writing/SongStyle.js +++ b/src/screens/Writing/SongStyle.js @@ -18,6 +18,13 @@ const SongStyle = ({ const selected = selectedProp ?? internalSelected; const setSelected = setSelectedProp ?? setInternalSelected; + const handleOtherStyleChange = (value) => { + setOtherStyle?.(value); + if (value?.trim()?.length && selected != null) { + setSelected(null); + } + }; + // Selection handled by ListSelection return ( @@ -42,7 +49,7 @@ const SongStyle = ({ label="Tu as un autre style de chanson ?" placeholder="Décrire l’objectif" value={otherStyle} - setValue={setOtherStyle} + setValue={handleOtherStyleChange} /> );