mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 09:48:44 +00:00
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:
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>
|
||||
</>
|
||||
);
|
||||
|
||||
+16
-5
@@ -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>
|
||||
|
||||
+35
-18
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user