Merge pull request #403 from storytold/bflat-fy-vd-title-continue-disable

don't allow continue to next step if there's no title
This commit is contained in:
BFlat authored and GitHub committed 2023-12-02 20:33:15 +07:00
commit 65f67121fa
7 files changed
+361 -186

No files matched your search

@@ -0,0 +1,83 @@
import React, { ReactNode } from "react";
// Header, Body, and Footer Interfaces and Components
interface HeaderProps {
children: ReactNode;
padding?: boolean;
}
const Header: React.FC<HeaderProps> = ({ children, padding }) => {
const headerClassName = `${padding ? "p-3 py-4 p-md-4" : ""}`.trim();
return <div className={headerClassName}>{children}</div>;
};
interface BodyProps {
children: ReactNode;
padding?: boolean;
}
const Body: React.FC<BodyProps> = ({ children, padding }) => {
const bodyClassName = `${padding ? "p-3 py-4 p-md-4" : ""}`.trim();
return <div className={bodyClassName}>{children}</div>;
};
interface FooterProps {
children: ReactNode;
padding?: boolean;
}
const Footer: React.FC<FooterProps> = ({ children, padding }) => {
const footerClassName = `${padding ? "p-3 py-4 p-md-4" : ""}`.trim();
return <div className={footerClassName}>{children}</div>;
};
// Panel Interface
interface PanelProps {
children: ReactNode;
divider?: boolean;
clear?: boolean;
className?: string;
}
// Panel Component
const SplitPanel: React.FC<PanelProps> & {
Header: typeof Header;
Body: typeof Body;
Footer: typeof Footer;
} = ({ children, divider, clear, className }) => {
let header: ReactNode, body: ReactNode, footer: ReactNode;
const panelClassName = `${clear ? "panel-clear" : "panel"} ${
className ? className : ""
}`.trim();
React.Children.toArray(children).forEach((child: ReactNode) => {
if (React.isValidElement(child)) {
switch (child.type) {
case Header:
header = child;
break;
case Body:
body = child;
break;
case Footer:
footer = child;
break;
}
}
});
return (
<div className={panelClassName}>
{header}
{divider && <hr className="m-0" />}
{body}
{divider && <hr className="m-0" />}
{footer}
</div>
);
};
// Attach Header, Body, and Footer as static properties
SplitPanel.Header = Header;
SplitPanel.Body = Body;
SplitPanel.Footer = Footer;
export default SplitPanel;
@@ -0,0 +1 @@
export { default } from "./SplitPanel";
@@ -8,13 +8,21 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
textArea?: boolean;
}
export default function TempInput({ label, icon, textArea, ...rest }: InputProps) {
return <>
{ label && <label className="sub-title">{label}</label> }
<div className={`form-group ${icon ? "input-icon" : ""}`}>
{ icon && <FontAwesomeIcon icon={icon} className="form-control-feedback" /> }
<input className="form-control" {...rest} />
export default function TempInput({
label,
icon,
textArea,
...rest
}: InputProps) {
return (
<div>
{label && <label className="sub-title">{label}</label>}
<div className={`form-group ${icon ? "input-icon" : ""}`}>
{icon && (
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
)}
<input className="form-control" {...rest} />
</div>
</div>
</>;
};
);
}
@@ -1,8 +1,13 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { faEye, faLanguage, faPencil, faWaveform } from "@fortawesome/pro-solid-svg-icons";
import {
faEye,
faLanguage,
faPencil,
faWaveform,
} from "@fortawesome/pro-solid-svg-icons";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import Panel from "components/common/Panel";
import { Panel } from "components/common";
import { Stepper } from "./components/Stepper";
import { StepperControls } from "./components/StepperControls";
import { UploadSamples } from "./components/steps/UploadSamples";
@@ -21,16 +26,23 @@ interface RouteParams {
dataset_token?: string;
}
function VoiceDesignerFormPage({ enqueueInferenceJob, sessionWrapper }: { enqueueInferenceJob: any, sessionWrapper: SessionWrapper }) {
function VoiceDesignerFormPage({
enqueueInferenceJob,
sessionWrapper,
}: {
enqueueInferenceJob: any;
sessionWrapper: SessionWrapper;
}) {
const history = useHistory();
const { datasets, inputCtrl, languages, visibilityOptions, voices } = useVoiceRequests({});
const { datasets, inputCtrl, languages, visibilityOptions, voices } =
useVoiceRequests({});
const [language, languageSet] = useState("en");
const [visibility, visibilitySet] = useState("hidden");
const [title, titleSet] = useState("");
const [fetched,fetchedSet] = useState(false);
const [deleting,deletingSet] = useState([]); // samples currently being uploaded
const [inProgress,inProgressSet] = useState([]); // samples currently being uploaded
const [samples,samplesSet] = useState([]); // fetched/uploaded samples
const [fetched, fetchedSet] = useState(false);
const [deleting, deletingSet] = useState([]); // samples currently being uploaded
const [inProgress, inProgressSet] = useState([]); // samples currently being uploaded
const [samples, samplesSet] = useState([]); // fetched/uploaded samples
const audioProps = useFile({});
const { user, sessionFetched } = useSession();
@@ -67,15 +79,26 @@ function VoiceDesignerFormPage({ enqueueInferenceJob, sessionWrapper }: { enqueu
const initialStep = history.location.pathname.includes("/upload") ? 1 : 0;
const [currentStep, setCurrentStep] = useState(initialStep);
const steps = existingVoice ? ["Edit Details", "Edit Samples"] : ["Voice Details", "Upload Samples"];
const uploadProps = { audioProps, datasetToken: dataset_token, deleting, deletingSet, inProgress, inProgressSet, samples, samplesSet };
const steps = existingVoice
? ["Edit Details", "Edit Samples"]
: ["Voice Details", "Upload Samples"];
const uploadProps = {
audioProps,
datasetToken: dataset_token,
deleting,
deletingSet,
inProgress,
inProgressSet,
samples,
samplesSet,
};
const displayStep = (step: any) => {
switch (step) {
case 0:
return <VoiceDetails {...{ datasetInputs }} />;
case 1:
return <UploadSamples {...uploadProps }/>;
return <UploadSamples {...uploadProps} />;
default:
return null;
}
@@ -96,27 +119,30 @@ function VoiceDesignerFormPage({ enqueueInferenceJob, sessionWrapper }: { enqueu
if (!existingVoice) {
// It's a new creation and on the first step
datasets.create("",{
title,
creator_set_visibility: visibility,
idempotency_token: uuidv4(),
}).then((res: any) => {
if (res && res.success && res.token) {
history.push(`/voice-designer/dataset/${ res.token }/upload`);
}
});
datasets
.create("", {
title,
creator_set_visibility: visibility,
idempotency_token: uuidv4(),
})
.then((res: any) => {
if (res && res.success && res.token) {
history.push(`/voice-designer/dataset/${res.token}/upload`);
}
});
} else if (dataset_token) {
// It's edit mode and on the first step
datasets.edit(dataset_token,{
title,
creator_set_visibility: visibility,
ietf_language_tag: language
}).then((res: any) => {
if (res && res.success && res.token) {
history.push(`/voice-designer/dataset/${dataset_token}/upload`);
}
});
datasets
.edit(dataset_token, {
title,
creator_set_visibility: visibility,
ietf_language_tag: language,
})
.then((res: any) => {
if (res && res.success && res.token) {
history.push(`/voice-designer/dataset/${dataset_token}/upload`);
}
});
}
setCurrentStep(currentStep + 1);
} else if (currentStep < steps.length - 1) {
@@ -145,14 +171,13 @@ function VoiceDesignerFormPage({ enqueueInferenceJob, sessionWrapper }: { enqueu
useEffect(() => {
if (!fetched && dataset_token) {
fetchedSet(true);
datasets.get(dataset_token,{})
.then((res) => {
datasets.get(dataset_token, {}).then(res => {
languageSet(res.ietf_language_tag);
titleSet(res.title);
visibilitySet(res.creator_set_visibility);
});
}
},[dataset_token,datasets,fetched]);
}, [dataset_token, datasets, fetched]);
if (sessionFetched && !user) {
history.push("/voice-designer");
@@ -175,23 +200,27 @@ function VoiceDesignerFormPage({ enqueueInferenceJob, sessionWrapper }: { enqueu
existingVoice ? "/voice-designer/datasets" : "/voice-designer/voices"
}
/>
{ sessionFetched && user && <Panel>
<div className="p-3 px-lg-4 bg-stepper">
<Stepper steps={steps} currentStep={currentStep} />
</div>
{sessionFetched && user && (
<Panel>
<div className="p-3 px-lg-4 bg-stepper">
<Stepper steps={steps} currentStep={currentStep} />
</div>
<div className="p-3 py-4 p-md-4">{displayStep(currentStep)}</div>
<div className="p-3 py-4 p-md-4">{displayStep(currentStep)}</div>
<StepperControls
steps={steps}
currentStep={currentStep}
onBack={handleBack}
onNext={handleNext}
onCreate={handleCreateVoice}
createDisabled={ !!deleting.length || !!inProgress.length || !samples.length } // deleting, uploading, or no samples = disabled
/>
</Panel>
}
<StepperControls
steps={steps}
currentStep={currentStep}
onBack={handleBack}
onNext={handleNext}
onCreate={handleCreateVoice}
createDisabled={
!!deleting.length || !!inProgress.length || !samples.length
} // deleting, uploading, or no samples = disabled
continueDisabled={title.length === 0}
/>
</Panel>
)}
</Container>
);
}
@@ -39,9 +39,14 @@ function VoiceDesignerMainPage(props: Props) {
usePrefixedDocumentTitle("AI Voice Designer");
const { pathname } = useLocation();
const { t } = useLocalize("FaceAnimator");
const { datasets, voices, isLoading } = useVoiceRequests({ requestDatasets: true,requestVoices: true });
const { datasets, voices, isLoading } = useVoiceRequests({
requestDatasets: true,
requestVoices: true,
});
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf(pathname);
const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf(
pathname
);
const [deleteItem, setDeleteItem] = useState("");
const [deleteType, setDeleteType] = useState("");
const [deleteText, setDeleteText] = useState({
@@ -85,18 +90,26 @@ function VoiceDesignerMainPage(props: Props) {
};
const DataBadge = () => <span className="dataset-badge mb-0">Dataset</span>;
const VoiceBadge = () => <FontAwesomeIcon icon={faMicrophone} className="me-2 me-lg-3" />;
const VoiceBadge = () => (
<FontAwesomeIcon icon={faMicrophone} className="me-2 me-lg-3" />
);
// these need to be abstracted to use over again -V
const voiceClick = (todo: any, type: string) => ({ target }: { target: any }) => {
let voiceToken = voices.list[target.name.split(",")[0].split(":")[1]].voice_token;
todo(voiceToken, type);
};
const voiceClick =
(todo: any, type: string) =>
({ target }: { target: any }) => {
let voiceToken =
voices.list[target.name.split(",")[0].split(":")[1]].voice_token;
todo(voiceToken, type);
};
const datasetClick = (todo: any, type: string) => ({ target }: { target: any }) => {
let datasetToken = datasets.list[target.name.split(",")[0].split(":")[1]].dataset_token;
todo(datasetToken, type);
};
const datasetClick =
(todo: any, type: string) =>
({ target }: { target: any }) => {
let datasetToken =
datasets.list[target.name.split(",")[0].split(":")[1]].dataset_token;
todo(datasetToken, type);
};
const actionDataSets = datasets.list.map((dataset, i) => {
return {
@@ -175,109 +188,122 @@ function VoiceDesignerMainPage(props: Props) {
to: "/pricing",
};
const dataPlaceholder = () => <div className="d-flex flex-column list-items p-5 align-items-center">
<h5 className="fw-semibold mb-3">
You haven't created any voices.
</h5>
<Button
icon={faPlus} // 1
label="Create New Voice" // 2
small={true}
to="/voice-designer/create" // 3
/>
</div>;
const dataPlaceholder = () => (
<div className="d-flex flex-column list-items p-5 align-items-center">
<h5 className="fw-semibold mb-3">You haven't created any voices.</h5>
<Button
icon={faPlus} // 1
label="Create New Voice" // 2
small={true}
to="/voice-designer/create" // 3
/>
</div>
);
const LogggedInView = () => <>
<InferenceJobsList
{...{
t,
inferenceJobs: props.inferenceJobsByCategory.get(
FrontendInferenceJobType.VoiceDesignerCreateVoice
),
statusTxt,
}}
/>
<Panel>
<nav>
<ul className="nav nav-tabs">
<div className="d-flex flex-grow-1">
<li className="nav-item">
<NavLink
to="/voice-designer/voices"
className="nav-link fs-6 px-3 px-lg-4"
activeClassName="active"
>
My Voices
</NavLink>
</li>
<li className="nav-item">
<NavLink
to="/voice-designer/datasets"
className="nav-link fs-6"
activeClassName="active"
>
My Datasets
</NavLink>
</li>
</div>
</ul>
</nav>
const LogggedInView = () => (
<>
<InferenceJobsList
{...{
t,
inferenceJobs: props.inferenceJobsByCategory.get(
FrontendInferenceJobType.VoiceDesignerCreateVoice
),
statusTxt,
}}
/>
<Panel mb={true}>
<nav>
<ul className="nav nav-tabs">
<div className="d-flex flex-grow-1">
<li className="nav-item">
<NavLink
to="/voice-designer/voices"
className="nav-link fs-6 px-3 px-lg-4"
activeClassName="active"
>
My Voices
</NavLink>
</li>
<li className="nav-item">
<NavLink
to="/voice-designer/datasets"
className="nav-link fs-6"
activeClassName="active"
>
My Datasets
</NavLink>
</li>
</div>
</ul>
</nav>
<div className="p-3 p-lg-4">
<ListItems {...{ data: view ? actionVoices : actionDataSets, dataPlaceholder, isLoading }}/>
<div className="p-3 p-lg-4">
<ListItems
{...{
data: view ? actionVoices : actionDataSets,
dataPlaceholder,
isLoading,
}}
/>
</div>
</Panel>
</>
);
const LoggedOutView = () => (
<Panel padding={true}>
<div className="d-flex flex-column align-items-center py-3 my-3 py-md-4 my-md-4 gap-4">
<div className="text-center">
<h4 className="fw-bold">Please log in to access voice creation.</h4>
<p className="text-center opacity-75">
If you don't have an account yet, sign up now to unlock this
feature!
</p>
</div>
<div className="d-flex gap-3 align-items-center">
<Button
label="Sign Up"
variant="primary"
icon={faPenToSquare}
to="/signup"
/>
<Button
label="Login"
variant="secondary"
icon={faRightToBracket}
to="/login"
/>
</div>
</div>
</Panel>
</>;
const LoggedOutView = () => <Panel padding={true}>
<div className="d-flex flex-column align-items-center py-3 my-3 py-md-4 my-md-4 gap-4">
<div className="text-center">
<h4 className="fw-bold">Please log in to access voice creation.</h4>
<p className="text-center opacity-75">
If you don't have an account yet, sign up now to unlock this
feature!
</p>
</div>
<div className="d-flex gap-3 align-items-center">
<Button
label="Sign Up"
variant="primary"
icon={faPenToSquare}
to="/signup"
/>
<Button
label="Login"
variant="secondary"
icon={faRightToBracket}
to="/login"
/>
</div>
</div>
</Panel>;
);
return (
<>
<Container type="panel">
<PageHeader {...{
button: user ? createVoiceButton : signUpButton,
...!user ? { secondaryButton: pricingButton } : {},
title: "Voice Designer",
titleIcon: faWaveform,
subText: "Create your own AI voice by providing audio files of the voice you want to clone.",
panel: false,
imageUrl: "/images/header/voice-designer.png"
}}/>
{ user ? <LogggedInView /> : <LoggedOutView /> }
{/* Delete Modal */}
<Modal
show={isDeleteModalOpen}
handleClose={closeDeleteModal}
title={deleteText.title}
content={<p>{deleteText.text}</p>}
onConfirm={handleDelete}
/>
<PageHeader
{...{
button: user ? createVoiceButton : signUpButton,
...(!user ? { secondaryButton: pricingButton } : {}),
title: "Voice Designer",
titleIcon: faWaveform,
subText:
"Create your own AI voice by providing audio files of the voice you want to clone.",
panel: false,
imageUrl: "/images/header/voice-designer.png",
}}
/>
{user ? <LogggedInView /> : <LoggedOutView />}
{/* Delete Modal */}
<Modal
show={isDeleteModalOpen}
handleClose={closeDeleteModal}
title={deleteText.title}
content={<p>{deleteText.text}</p>}
onConfirm={handleDelete}
/>
</Container>
</>
);
@@ -46,7 +46,7 @@ function VoiceDesignerVoiceEditPage() {
useEffect(() => {
if (!fetched && voice_token) {
fetchedSet(true);
voices.get(voice_token, {}).then((res) => {
voices.get(voice_token, {}).then(res => {
languageSet(res.ietf_language_tag);
titleSet(res.title);
visibilitySet(res.creator_set_visibility);
@@ -66,8 +66,8 @@ function VoiceDesignerVoiceEditPage() {
backbuttonTo="/voice-designer"
/>
<Panel padding={true}>
<div className="d-flex flex-column gap-4">
<Panel>
<div className="d-flex flex-column gap-4 p-3 py-4 p-md-4">
<div className="row gy-4">
<TempInput
{...{
@@ -104,8 +104,19 @@ function VoiceDesignerVoiceEditPage() {
}}
/>
</div>
<Button {...{ label: "Save", onClick }} />
</div>
<hr className="mt-0 mb-4" />
<div className="p-3 pb-4 px-lg-4 pt-0">
<div className="d-flex gap-3 justify-content-end">
<Button
{...{
label: "Cancel",
to: "/voice-designer",
variant: "secondary",
}}
/>
<Button {...{ label: "Save Changes", onClick }} />
</div>
</div>
</Panel>
</Container>
@@ -1,4 +1,6 @@
import { Button } from "components/common";
import React from "react";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
interface StepperControlsProps {
onBack: () => void;
@@ -7,6 +9,7 @@ interface StepperControlsProps {
createDisabled: boolean;
steps: string[];
currentStep: number;
continueDisabled?: boolean;
}
const StepperControls: React.FC<StepperControlsProps> = ({
@@ -15,28 +18,42 @@ const StepperControls: React.FC<StepperControlsProps> = ({
steps,
currentStep,
onCreate,
createDisabled
createDisabled,
continueDisabled,
}) => {
return (
<div className="p-3 pb-4 px-lg-4 pt-0 d-flex gap-3">
{currentStep === 1 && (
<button className="btn btn-secondary w-100" onClick={onBack}>
Back
</button>
)}
<>
<hr className="mt-0 mb-4" />
{currentStep === 0 && (
<button className="btn btn-primary w-100" onClick={onNext}>
Continue
</button>
)}
<div className="p-3 pb-4 px-lg-4 pt-0 d-flex gap-3 justify-content-end">
{currentStep === 1 && (
<Button label="Back" variant="secondary" onClick={onBack} />
)}
{currentStep === 1 && (
<button disabled={ createDisabled } className="btn btn-primary w-100" onClick={onCreate}>
Create Voice
</button>
)}
</div>
{currentStep === 0 && (
<>
<Button label="Cancel" variant="secondary" to="/voice-designer" />
<Button
label="Continue"
onClick={onNext}
disabled={continueDisabled}
iconFlip={true}
icon={faChevronRight}
/>
</>
)}
{currentStep === 1 && (
<Button
label="Create Voice"
onClick={onCreate}
disabled={createDisabled}
iconFlip={true}
icon={faChevronRight}
/>
)}
</div>
</>
);
};