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}
/>
);