create flow

This commit is contained in:
2025-10-01 15:39:35 +02:00
parent 2908afc259
commit c7ffaca548
4 changed files with 507 additions and 12 deletions
+34 -10
View File
@@ -30,7 +30,7 @@ const CreateLyricsWithAi = () => {
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0); const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [progress, setProgress] = useState(16); const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null); const [parentLayout, setparentLayout] = useState(null);
const containerWidth = isWeb ? parentLayout?.width : windowWidth; const containerWidth = windowWidth;
// Collected state across steps // Collected state across steps
const [objective, setObjective] = useState(null); // from Goals list const [objective, setObjective] = useState(null); // from Goals list
@@ -50,11 +50,23 @@ const CreateLyricsWithAi = () => {
const sel = selectedProject?.selections || {}; const sel = selectedProject?.selections || {};
const cfg = selectedProject?.config || {}; const cfg = selectedProject?.config || {};
// Text inputs / choix simples const rawOtherObjective =
if (objective == null && typeof sel.objective === "string" && sel.objective) typeof sel.otherObjective === "string" ? sel.otherObjective : "";
setObjective(sel.objective); const hasSavedOtherObjective = rawOtherObjective.trim().length > 0;
if (!otherObjective && typeof sel.otherObjective === "string") const savedObjective =
setOtherObjective(sel.otherObjective); 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); if (!context && typeof sel.context === "string") setContext(sel.context);
// Emotion: accepter objet {title, description} ou string "Titre : description" // Emotion: accepter objet {title, description} ou string "Titre : description"
@@ -72,10 +84,22 @@ const CreateLyricsWithAi = () => {
} }
} }
if (style == null && typeof sel.style === "string" && sel.style) const rawOtherStyle =
setStyle(sel.style); typeof sel.otherStyle === "string" ? sel.otherStyle : "";
if (!otherStyle && typeof sel.otherStyle === "string") const hasSavedOtherStyle = rawOtherStyle.trim().length > 0;
setOtherStyle(sel.otherStyle); 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") if (!audience && typeof sel.audience === "string")
setAudience(sel.audience); setAudience(sel.audience);
@@ -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: () => (
<Goals
selected={objective}
setSelected={setObjective}
otherObjective={otherObjective}
setOtherObjective={setOtherObjective}
/>
),
},
{
key: "context",
render: () => (
<SpecificityContext context={context} setContext={setContext} />
),
},
{
key: "emotion",
render: () => (
<EmotionConvey selected={emotion} setSelected={setEmotion} />
),
},
{
key: "style",
render: () => (
<SongStyle
selected={style}
setSelected={setStyle}
otherStyle={otherStyle}
setOtherStyle={setOtherStyle}
/>
),
},
{
key: "audience",
render: () => <SongTo audience={audience} setAudience={setAudience} />,
},
{
key: "structure",
render: () => (
<SongStructure selected={structure} setSelected={setStructure} />
),
},
{
key: "customStructure",
render: () => (
<CustomizeSongStructure
baseStructure={
parsedStructure ||
(Array.isArray(customStructure)
? customStructure
: Array.isArray(selectedProject?.config?.structure)
? selectedProject.config.structure
: [])
}
onChange={setCustomStructure}
/>
),
},
{
key: "rhymes",
render: () => <Rhymes selected={rhymes} setSelected={setRhymes} />,
},
{
key: "creating",
render: () => (
<CreatingLyrics
active={selectedIndex === 8}
config={lyricsConfig}
selections={{
objective,
otherObjective,
context,
emotion,
style,
otherStyle,
audience,
structure,
parsedStructure,
customStructure,
rhymes,
}}
/>
),
},
];
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 (
<Page
headerType="NONE"
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
backgroundImg={background.homeBG}
>
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
<View
style={{
flex: 1,
paddingBottom: gutters,
gap: responsiveHeight(5),
}}
>
<View
style={{ flex: 1 }}
onLayout={(e) => setParentLayout(e.nativeEvent.layout)}
>
<FlatList
ref={scrollRef}
data={steps}
keyExtractor={(item) => item.key}
horizontal
pagingEnabled
scrollEnabled={false}
showsHorizontalScrollIndicator={false}
initialScrollIndex={selectedIndex}
getItemLayout={getItemLayout}
style={{ width: containerWidth }}
renderItem={({ item }) => (
<View
style={{
width: containerWidth,
height: parentLayout?.height,
paddingHorizontal: gutters,
}}
>
{item.render()}
</View>
)}
/>
</View>
{selectedIndex !== 8 && (
<GradientButton
maxWidth={500}
title={selectedIndex === 7 ? "Générer" : "Suivant"}
disabled={isNextDisabled}
onPress={onPressNext}
containerStyle={{ alignSelf: "center", width: "100%" }}
/>
)}
</View>
</Page>
);
};
export default CreateLyricsWithAi;
+8 -1
View File
@@ -19,6 +19,13 @@ const Goals = ({
const selected = selectedProp ?? internalSelected; const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected; const setSelected = setSelectedProp ?? setInternalSelected;
const handleOtherObjectiveChange = (value) => {
setOtherObjective?.(value);
if (value?.trim()?.length && selected != null) {
setSelected(null);
}
};
// Selection handled by ListSelection // Selection handled by ListSelection
return ( return (
@@ -41,7 +48,7 @@ const Goals = ({
label="Tu as un autre objectif?" label="Tu as un autre objectif?"
placeholder="Décrire lobjectif" placeholder="Décrire lobjectif"
value={otherObjective} value={otherObjective}
setValue={setOtherObjective} setValue={handleOtherObjectiveChange}
/> />
</View> </View>
); );
+8 -1
View File
@@ -18,6 +18,13 @@ const SongStyle = ({
const selected = selectedProp ?? internalSelected; const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected; const setSelected = setSelectedProp ?? setInternalSelected;
const handleOtherStyleChange = (value) => {
setOtherStyle?.(value);
if (value?.trim()?.length && selected != null) {
setSelected(null);
}
};
// Selection handled by ListSelection // Selection handled by ListSelection
return ( return (
@@ -42,7 +49,7 @@ const SongStyle = ({
label="Tu as un autre style de chanson ?" label="Tu as un autre style de chanson ?"
placeholder="Décrire lobjectif" placeholder="Décrire lobjectif"
value={otherStyle} value={otherStyle}
setValue={setOtherStyle} setValue={handleOtherStyleChange}
/> />
</View> </View>
); );