Merge branch 'feature/mvp' of https://github.com/storytold/storyteller-frontend into feature/mvp

This commit is contained in:
Victor committed 2024-02-22 10:33:43 -08:00
commit cd855b9d63
26 files changed
+856 -213

No files matched your search

@@ -22,6 +22,7 @@ interface AlertProps {
icon?: IconProp;
link?: string;
linkText?: string;
className?: string;
}
function Alert({
@@ -32,6 +33,7 @@ function Alert({
icon,
linkText,
link,
className,
}: AlertProps) {
const [show, setShow] = useState<boolean>(() => {
const closedAt = localStorage.getItem(`alertClosedAt_${id}`);
@@ -55,7 +57,9 @@ function Alert({
<>
{show && (
<div
className={`alert alert-${alertVariant} alert-dismissible fade show`}
className={`alert alert-${alertVariant} alert-dismissible fade show ${
className && className
}`.trim()}
role="alert"
>
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
@@ -15,11 +15,13 @@
&-square {
width: 46.5px;
height: 46.5px;
padding: 0;
}
&-square-small {
width: 37.5px;
height: 37.5px;
padding: 0;
}
&:disabled,
@@ -33,7 +33,9 @@ export default function Button({
const SpinnerComponent = isLoading ? (
<div
className="spinner-border spinner-border-sm text-white ms-2"
className={`spinner-border spinner-border-sm text-white ${
square ? "" : "ms-2"
}`.trim()}
role="status"
>
<span className="visually-hidden">Loading...</span>
@@ -59,7 +61,7 @@ export default function Button({
</>
) : (
<>
{IconComponent}
{isLoading ? null : IconComponent}
{LabelComponent}
{SpinnerComponent}
</>
@@ -21,7 +21,7 @@ export type SelectModalData = {
interface SelectModalProps {
label?: string;
modalTitle?: string;
value?: string;
value?: string | SelectModalData;
onClear: () => void;
required?: boolean;
children: ReactNode
@@ -32,21 +32,15 @@ export default memo(function SelectModal ({
// tabs,
modalTitle = "Select",
onClear,
value = "",
value:valueProps = "",
required,
children
}: SelectModalProps) {
const [isModalOpen, setModalOpen] = useState(false
)
const openModal = () => {
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
};
useEffect(closeModal, [value]);
const [isModalOpen, setModalOpen] = useState(false)
const openModal = () => setModalOpen(true);
const closeModal = () => setModalOpen(false);
useEffect(closeModal, [valueProps]);
const value = typeof valueProps === "string" ? valueProps : valueProps.title;
return (
<>
@@ -41,7 +41,7 @@ export default memo(function SelectModalWrapper({
<SelectModalV2
modalTitle={modalTitle}
label={inputLabel}
value={value.title || value.token}
value={value}
onClear={()=>{onSelect({title:"",token:""})}}
>
<NonRouteTabs tabs={tabs} />
@@ -90,7 +90,7 @@ export default function WeightsTabsContent({
showCreator: true,
type: "weights",
inSelectModal: true,
onResultSelect: onSelect,
onResultBookmarkSelect: onSelect,
};
return (
<div
@@ -0,0 +1,79 @@
@import "scss/colours.scss";
.selection-bubbles-wrapper {
position: relative;
overflow: hidden;
transition: all 0.4s;
.scroll-indicator {
position: absolute;
z-index: 2;
top: 50%;
right: 0px;
transform: translateY(-50%);
color: rgba($white, 0.2);
pointer-events: none;
transition: all 0.4s;
}
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 80px;
background: linear-gradient(to right, rgba($panel, 0), $panel);
pointer-events: none;
display: none;
transition: all 0.4s;
}
&.show-gradient:after {
display: block;
transition: all 0.4s;
}
}
.selection-bubbles {
display: flex;
gap: 0.5rem;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
@media (min-width: 768px) {
flex-wrap: wrap;
justify-content: start;
overflow-x: visible;
}
}
.bubble-button {
flex: 0 0 auto;
border: 2px solid darken($secondary, 3%);
padding: 8px 16px;
border-radius: 0.5rem;
background-color: darken($secondary, 3%);
cursor: pointer;
outline: none;
transition: all 0.2s ease;
font-size: 15px;
font-weight: 500;
color: rgba($white, 0.75);
white-space: nowrap;
}
.bubble-button:hover {
background-color: $secondary;
border-color: $secondary;
}
.bubble-button.selected {
color: $white;
border-color: $primary;
background-color: lighten($secondary, 6%);
}
@@ -0,0 +1,96 @@
import React, { useEffect, useRef, useState } from "react";
import "./SelectionBubbles.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
interface SelectionBubbleProps {
options: string[];
onSelect: (selected: string) => void;
}
export default function SelectionBubbles({
options,
onSelect,
}: SelectionBubbleProps) {
//Select first one as defaults
const [selectedOption, setSelectedOption] = useState<string | null>(
options.length > 0 ? options[0] : null
);
const [showGradient, setShowGradient] = useState(true);
const bubblesRef = useRef<HTMLDivElement>(null);
const handleSelect = (
event: React.MouseEvent<HTMLButtonElement>,
option: string
) => {
event.preventDefault();
setSelectedOption(option);
onSelect(option);
};
useEffect(() => {
if (selectedOption) {
onSelect(selectedOption);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleScroll = () => {
if (bubblesRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = bubblesRef.current;
const isAtEnd = scrollWidth - Math.round(scrollLeft + clientWidth) <= 1; // Adjust tolerance as needed
setShowGradient(!isAtEnd);
}
};
const checkForOverflowContent = () => {
if (bubblesRef.current) {
const { scrollWidth, clientWidth } = bubblesRef.current;
const isOverflowing = scrollWidth > clientWidth;
setShowGradient(isOverflowing);
}
};
useEffect(() => {
checkForOverflowContent();
window.addEventListener("resize", checkForOverflowContent);
return () => window.removeEventListener("resize", checkForOverflowContent);
}, [options]);
useEffect(() => {
if (bubblesRef.current) {
bubblesRef.current.addEventListener("scroll", handleScroll);
return () =>
// eslint-disable-next-line react-hooks/exhaustive-deps
bubblesRef.current?.removeEventListener("scroll", handleScroll);
}
}, []);
return (
<div
className={`selection-bubbles-wrapper ${
showGradient ? "show-gradient" : ""
}`}
>
<div className="selection-bubbles" ref={bubblesRef}>
{options.map(option => (
<button
key={option}
className={`bubble-button ${
selectedOption === option ? "selected" : ""
}`}
onClick={event => handleSelect(event, option)}
>
{option}
</button>
))}
</div>
{showGradient && (
<FontAwesomeIcon
icon={faChevronRight}
className="scroll-indicator fs-5"
/>
)}
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./SelectionBubbles";
@@ -15,7 +15,6 @@ export default function TempTextArea({
...rest
}: TextAreaProps) {
return (
// Changed fragment to div here just so that it can be laid out with bootstrap easily using d-flex, flex-column and responsive gaps which requires grouping.
<div className="fy-textarea">
{label && (
<label className={`sub-title ${required ? "required" : ""}`}>
@@ -0,0 +1,31 @@
.fy-textarea {
margin-bottom: 1rem;
&:only-child,
&:last-child {
margin-bottom: 0;
}
.no-resize {
resize: none;
}
}
.d-flex:not(.flex-column) > .fy-textarea {
margin-bottom: 0;
margin-right: 0.5rem;
&:only-child,
&:last-child {
margin-bottom: 0;
}
}
.gap-1 > .fy-textarea,
.gap-2 > .fy-textarea,
.gap-3 > .fy-textarea,
.gap-4 > .fy-textarea,
.gap-5 > .fy-textarea {
margin-bottom: 0;
margin-right: 0;
}
@@ -1,19 +1,32 @@
import React from "react";
import "./TextArea.scss";
interface TextAreaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
textArea?: boolean;
required?: boolean;
resize?: boolean;
}
export default function TextArea({ label, textArea, ...rest }: TextAreaProps) {
export default function TextArea({
label,
textArea,
required,
resize = true,
...rest
}: TextAreaProps) {
return (
<div>
{label && <label className="sub-title">{label}</label>}
<div className="form-group">
<textarea className="form-control" {...rest} />
</div>
<div className="fy-textarea">
{label && (
<label className={`sub-title ${required ? "required" : ""}`.trim()}>
{label}
</label>
)}
<textarea
className={`form-control ${resize ? "" : "no-resize"}`.trim()}
{...rest}
/>
</div>
);
}
@@ -5,14 +5,10 @@ import Badge from "./Badge";
import Button from "./Button";
import ButtonRevertToDefault from "./ButtonRevertToDefault";
import Checkbox from "./Checkbox";
import {
MediaCards,
SkeletonCard,
WeightsCards,
} from "./Card";
import { MediaCards, SkeletonCard, WeightsCards } from "./Card";
import Container from "./Container";
import CoverImageInput from "./CoverImageInput";
import ErrorMessage from "./ErrorMessage"
import ErrorMessage from "./ErrorMessage";
import FileActions from "./FileActions";
import FileDetails from "./FileDetails";
import FileWrapper from "./FileWrapper";
@@ -38,7 +34,7 @@ import Skeleton from "./Skeleton";
import Slider from "./Slider";
import Spinner from "./Spinner";
import SplitPanel from "./SplitPanel";
import Tabs, {NonRouteTabs} from "./Tabs";
import Tabs, { NonRouteTabs } from "./Tabs";
import TempAudioPlayer from "./TempAudioPlayer";
import TempInput from "./TempInput";
import TempSelect from "./TempSelect";
@@ -49,6 +45,7 @@ import TintSpinner from "./TintSpinner";
import VideoBasic from "./VideoBasic";
import VideoFakeyou from "./VideoFakeyou";
import VideoInput from "./VideoInput";
import SelectionBubbles from "./SelectionBubbles";
export {
Accordion,
@@ -102,5 +99,6 @@ export {
TintSpinner,
VideoBasic,
VideoFakeyou,
VideoInput
};
VideoInput,
SelectionBubbles,
};
@@ -0,0 +1,6 @@
@import "scss/colours.scss";
.topbar-bg-dark {
background-color: $bg !important;
border-bottom: 1px solid transparent !important;
}
@@ -20,6 +20,7 @@ import { InferenceJobsModal } from "components/modals";
import { useDomainConfig } from "context/DomainConfigContext";
import NavItem from "../../common/NavItem/NavItem";
import ProfileDropdown from "components/common/ProfileDropdown";
import "./TopNav.scss";
interface TopNavProps {
sessionWrapper: SessionWrapper;
@@ -50,6 +51,9 @@ export default function TopNav({
const { open } = useModal();
const openModal = () => open({ component: InferenceJobsModal });
const [isScrolled, setIsScrolled] = useState(false);
const loggedIn = sessionWrapper.isLoggedIn();
const showNavItem = !loggedIn && (isOnLandingPage || isOnLoginOrSignUpPage);
const handleMenuButtonClick = () => {
if (window.innerWidth < 1200) {
@@ -101,6 +105,22 @@ export default function TopNav({
};
}, []);
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 100) {
setIsScrolled(true);
} else {
setIsScrolled(false);
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
const logoutHandler = async () => {
await Logout();
querySessionCallback();
@@ -108,8 +128,6 @@ export default function TopNav({
history.push("/");
};
const loggedIn = sessionWrapper.isLoggedIn();
let profileDropdown = <></>;
const { user } = useSession();
@@ -157,7 +175,12 @@ export default function TopNav({
];
return (
<div id="topbar-wrapper" className="position-fixed">
<div
id="topbar-wrapper"
className={`position-fixed ${
!loggedIn && isOnLandingPage && !isScrolled ? "topbar-bg-dark" : ""
}`}
>
<div className="topbar-nav">
<div className="topbar-nav-left">
<div className="d-flex gap-3 align-items-center">
@@ -175,8 +198,7 @@ export default function TopNav({
className="mb-0 d-block d-lg-none"
/>
</Link>
{((!loggedIn && isOnLandingPage) ||
(!loggedIn && isOnLoginOrSignUpPage)) && (
{showNavItem && (
<div className="d-none d-lg-block">
<NavItem
isHoverable={true}
@@ -191,17 +213,20 @@ export default function TopNav({
<div className="topbar-nav-center">
{/* Search Bar */}
<div className="d-none d-lg-block">
<SearchBar
onFocus={onFocusHandler}
onBlur={onBlurHandler}
isFocused={isFocused}
/>
{(!isOnLandingPage && !isOnLoginOrSignUpPage) ||
(loggedIn && !isOnLoginOrSignUpPage) ||
(isOnLandingPage && isScrolled && !isOnLoginOrSignUpPage) ? (
<SearchBar
onFocus={onFocusHandler}
onBlur={onBlurHandler}
isFocused={isFocused}
/>
) : null}
</div>
</div>
<div className="topbar-nav-right">
{((!loggedIn && isOnLandingPage) ||
(!loggedIn && isOnLoginOrSignUpPage)) && (
{showNavItem && (
<NavItem
icon={faStar}
label="Pricing"
@@ -212,15 +237,18 @@ export default function TopNav({
<div className="d-flex align-items-center gap-2">
<div className="d-none d-lg-flex gap-2">
<Button
{...{
icon: faClipboardList,
label: "My Jobs",
onClick: openModal,
variant: "secondary",
small: true,
}}
/>
{!showNavItem && (
<Button
{...{
icon: faClipboardList,
label: "My Jobs",
onClick: openModal,
variant: "secondary",
small: true,
}}
/>
)}
{loggedIn ? (
profileDropdown
) : (
@@ -243,22 +271,27 @@ export default function TopNav({
</>
)}
</div>
<Button
icon={faClipboardList}
variant="secondary"
small={true}
label="My Jobs"
onClick={openModal}
className="d-lg-none"
/>
<Button
icon={faSearch}
variant="secondary"
small={true}
square={true}
onClick={handleSearchButtonClick}
className="d-lg-none"
/>
{!showNavItem && (
<>
<Button
icon={faClipboardList}
variant="secondary"
small={true}
label="My Jobs"
onClick={openModal}
className="d-lg-none"
/>
<Button
icon={faSearch}
variant="secondary"
small={true}
square={true}
onClick={handleSearchButtonClick}
className="d-lg-none"
/>
</>
)}
<Button
icon={menuButtonIcon}
variant="secondary"
+2 -1
View File
@@ -168,8 +168,9 @@ $topbar-height: 66px;
width: 100%;
height: $topbar-height;
background: $panel;
border-bottom: 1px solid rgba(255, 255, 255, 0.0705882353) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.0705882353);
z-index: 1000;
transition: all 0.1s ease;
}
.topbar-nav {
@@ -1545,6 +1545,13 @@ class PageContainer extends React.Component<
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
enqueueInferenceJob={this.props.enqueueInferenceJob}
inferenceJobs={this.props.inferenceJobs}
inferenceJobsByCategory={
this.props.inferenceJobsByCategory
}
ttsInferenceJobs={this.props.ttsInferenceJobs}
enqueueTtsJob={this.props.enqueueTtsJob}
/>
</Route>
</Switch>
@@ -0,0 +1,11 @@
.fy-header {
padding-top: 4rem;
padding-bottom: 7rem;
}
@media (max-width: 992px) {
.fy-header {
padding-top: 0rem;
padding-bottom: 0rem;
}
}
@@ -10,10 +10,24 @@ import Dashboard from "./Dashboard";
import FakeYouLandingBody from "./fakeyou/FakeYouLandingBody";
import { useDomainConfig } from "context/DomainConfigContext";
import StorytellerLanding from "./storyteller/StorytellerLanding";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
import "./LandingPage.scss";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
inferenceJobs: Array<InferenceJob>;
ttsInferenceJobs: Array<TtsInferenceJob>;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
enqueueTtsJob: (jobToken: string) => void;
}
function LandingPage(props: Props) {
@@ -33,6 +47,11 @@ function LandingPage(props: Props) {
<FakeYouLandingHeader
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
inferenceJobs={props.inferenceJobs}
ttsInferenceJobs={props.ttsInferenceJobs}
enqueueInferenceJob={props.enqueueInferenceJob}
inferenceJobsByCategory={props.inferenceJobsByCategory}
enqueueTtsJob={props.enqueueTtsJob}
/>
)}
@@ -1,158 +1,87 @@
import {
faArrowRight,
faSparkles,
faStar,
faUser,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faSparkles } from "@fortawesome/pro-solid-svg-icons";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { WebUrl } from "common/WebUrl";
import { Panel } from "components/common";
import Alert from "components/common/Alert/Alert";
import { useLocalize } from "hooks";
import React, { useMemo } from "react";
import { Link } from "react-router-dom";
import React from "react";
import LandingDemo from "./LandingDemo/FakeYouLandingDemo";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
import { Button, Panel } from "components/common";
interface FakeYouLandingHeaderProps {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
inferenceJobs: Array<InferenceJob>;
ttsInferenceJobs: Array<TtsInferenceJob>;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
enqueueTtsJob: (jobToken: string) => void;
}
export default function FakeYouLandingHeader(props: FakeYouLandingHeaderProps) {
export default function FakeYouLandingHeader({
sessionWrapper,
sessionSubscriptionsWrapper,
inferenceJobs,
ttsInferenceJobs,
enqueueInferenceJob,
inferenceJobsByCategory,
enqueueTtsJob,
}: FakeYouLandingHeaderProps) {
const { t } = useLocalize("LandingPage");
const isLoggedIn = props.sessionWrapper.isLoggedIn();
const isSubscribed = props.sessionSubscriptionsWrapper.hasPaidFeatures();
let signUpButton = <></>;
let viewPricingButton = <></>;
let upgradeButton = <></>;
let myProfileButton = <></>;
if (!isLoggedIn) {
signUpButton = (
<>
<Link
to="/signup"
// onClick={() => {
// Analytics.ttsClickHeroSignup();
// }}
>
<button type="button" className="btn btn-primary w-100">
{t("heroButtonSignUp")}
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
</button>
</Link>
</>
);
viewPricingButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
// onClick={() => {
// Analytics.ttsClickHeroViewPricing();
// }}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonPricing")}
</button>
</Link>
</>
);
}
if (isLoggedIn) {
let displayName = props.sessionWrapper.getDisplayName() as string; // NB: If logged in, should be string
let url = WebUrl.userProfilePage(displayName);
myProfileButton = (
<>
<Link
to={url}
// onClick={() => {
// Analytics.ttsClickHeroViewProfile();
// }}
>
<button type="button" className="btn btn-secondary w-100">
<FontAwesomeIcon icon={faUser} className="me-2" />
{t("heroButtonProfile")}
</button>
</Link>
</>
);
if (!isSubscribed) {
upgradeButton = (
<>
<Link
to={WebUrl.pricingPageWithReferer("tts_hero_user")}
// onClick={() => {
// Analytics.ttsClickHeroUpgradePlan();
// }}
>
<button type="button" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faStar} className="me-2" />
{t("heroButtonUpgradePlan")}
</button>
</Link>
</>
);
}
}
const randomHeroImage = useMemo(() => {
const images = [
// Main Images:
"mascot/kitsune_pose2.webp",
// "mascot/may4th.webp",
// "mascot/halloween_1.webp",
// "mascot/halloween_2.webp",
// "mascot/halloween_3.webp",
// "mascot/xmas_1.webp",
// "mascot/xmas_2.webp",
// "mascot/xmas_3.webp",
// "mascot/xmas_4.webp",
];
return images[Math.floor(Math.random() * images.length)];
}, []);
return (
<Panel clear={true} className="mt-5">
<Alert
id="text-to-image-alert"
icon={faSparkles}
message={t("alertTtiText")}
alertVariant="new"
link="/text-to-image"
linkText={t("alertTtiCta")}
/>
<div className="d-flex flex-column fy-header">
<Panel clear={true}>
<Alert
id="text-to-image-alert"
icon={faSparkles}
message={t("alertTtiText")}
alertVariant="new"
link="/text-to-image"
linkText={t("alertTtiCta")}
className="my-3"
/>
</Panel>
<div className="row flex-md-row-reverse">
<div className="col-12 col-lg-5 p-md-0 d-flex justify-content-center">
<img
src={randomHeroImage}
alt="FakeYou Mascot"
height={440}
width={447}
className="img-fluid"
/>
</div>
<div className="col-12 col-lg-7 d-flex flex-column justify-content-center flex-reverse px-md-5 px-lg-3">
<h1 className="fw-bold display-5 text-center text-lg-start px-md-5 px-lg-0">
{t("heroTitle")}
</h1>
<p className="lead opacity-75 pb-4 text-center text-lg-start px-md-5 px-lg-0 pe-lg-5">
{t("heroText")}
</p>
<div className="d-flex flex-column flex-md-row gap-3 mt-3 mb-4 w-100 justify-content-center justify-content-lg-start">
{upgradeButton}
{signUpButton}
{viewPricingButton}
{myProfileButton}
<div className="my-lg-5 py-lg-5 pt-4 pb-5">
<div className="row g-5">
<div className="col-12 col-lg-6 order-lg-2">
<LandingDemo
inferenceJobs={inferenceJobs}
sessionSubscriptionsWrapper={sessionSubscriptionsWrapper}
enqueueInferenceJob={enqueueInferenceJob}
inferenceJobsByCategory={inferenceJobsByCategory}
ttsInferenceJobs={ttsInferenceJobs}
enqueueTtsJob={enqueueTtsJob}
/>
</div>
<div className="col-12 col-lg-6 order-lg-1 d-flex flex-column align-items-center justify-content-center pt-3 pt-lg-0">
<Panel clear={true}>
<h1 className="fw-bold display-5 text-center text-lg-start px-md-5 px-lg-0">
{t("heroTitle")}
</h1>
<p className="lead opacity-75 pb-4 text-center text-lg-start px-md-5 px-lg-0 pe-lg-5">
{t("heroText")}
</p>
<div className="d-flex justify-content-center justify-content-lg-start">
<Button
label="Get Started Free"
to="/signup"
icon={faArrowRight}
iconFlip={true}
/>
</div>
</Panel>
</div>
</div>
</div>
</Panel>
</div>
);
}
@@ -0,0 +1,36 @@
import React, { useCallback, useState } from "react";
import Wavesurfer from "react-wavesurfer.js";
interface Props {
filename: string;
play: boolean;
onFinish?: () => void;
}
export default function DemoTtsAudioPlayer(props: Props) {
const [position, setPosition] = useState(0);
const handleFinish = useCallback(() => {
setPosition(0);
props.onFinish && props.onFinish();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.onFinish]);
return (
<div className="flex-grow-1 h-100 overflow-hidden">
<Wavesurfer
key={props.filename}
onFinish={handleFinish}
pos={position}
src={props.filename || ""}
height={36}
progressColor="#fc8481"
waveColor="#b09e9e"
cursorColor="#fc8481"
playing={props.play}
responsive={true}
normalize={true}
/>
</div>
);
}
@@ -0,0 +1,277 @@
import React, { useEffect, useState } from "react";
import { faPause, faPlay, faShuffle } from "@fortawesome/pro-solid-svg-icons";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { Panel, Button, TextArea, SelectionBubbles } from "components/common";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
import { v4 as uuidv4 } from "uuid";
import {
GenerateTtsAudio,
GenerateTtsAudioErrorType,
GenerateTtsAudioIsError,
GenerateTtsAudioIsOk,
} from "@storyteller/components/src/api/tts/GenerateTtsAudio";
import { Analytics } from "common/Analytics";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import DemoTtsAudioPlayer from "./DemoAudioPlayer";
import {
GetTtsInferenceJobStatus,
GetTtsInferenceJobStatusIsOk,
} from "@storyteller/components/src/api/jobs/GetTtsInferenceJobStatus";
import RandomTexts from "./RandomTexts";
import "./LandingDemo.scss";
interface TtsInferencePanelProps {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
inferenceJobs: Array<InferenceJob>;
ttsInferenceJobs: Array<TtsInferenceJob>;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
enqueueTtsJob: (jobToken: string) => void;
}
export default function LandingDemo({
inferenceJobs,
sessionSubscriptionsWrapper,
ttsInferenceJobs,
enqueueInferenceJob,
enqueueTtsJob,
inferenceJobsByCategory,
}: TtsInferencePanelProps) {
const [textBuffer, setTextBuffer] = useState("");
const [maybeTtsError, setMaybeTtsError] = useState<
GenerateTtsAudioErrorType | undefined
>(undefined);
const [currentAudioUrl, setCurrentAudioUrl] = useState<string | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [jobToken, setJobToken] = useState<string | null>(null);
const [lastEnqueuedText, setLastEnqueuedText] = useState<string | null>(null);
const [isAudioLoading, setIsAudioLoading] = useState(false);
const [lastSelectedVoice, setLastSelectedVoice] = useState<string | null>(
null
);
const [placeholder, setPlaceholder] = useState("");
const placeholderTexts = [
"You can make a video, animation, or any content you want with your favorite voices.",
"Try using this voice to make a custom greeting, away message, or something special for your friends.",
"You can change what I say with this voice. Just type it!",
"Enter the text you want the voice to say here...",
];
useEffect(() => {
// Randomize placeholder text on component mount
const randomPlaceholderIndex = Math.floor(
Math.random() * placeholderTexts.length
);
setPlaceholder(placeholderTexts[randomPlaceholderIndex]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChangeText = (ev: React.FormEvent<HTMLTextAreaElement>) => {
const textValue = (ev.target as HTMLTextAreaElement).value;
setTextBuffer(textValue);
};
//Set voices here
const voiceModelTokenMap: { [key: string]: string } = {
Rick: "weight_0f762jdzgsy1dhpb86qxy4ssm",
Mickey: "weight_sfyjyr67ag1647xs0r7gmvkks",
Eric: "weight_h8ebh6fyjyrr1vsjregw6yz8y",
Stan: "weight_0cg1294gaf52c7rh0vz7a2ger",
Zelda: "weight_b8rncypy7gw6nb0wthnwe2kk4",
"Angry Male": "weight_hehgvegadf08mfp5rzd69dmh4",
};
const [voiceToken, setVoiceToken] = useState(voiceModelTokenMap["Spongebob"]);
const handleVoiceSelection = (selected: string) => {
console.log(`Selected option: ${selected}`);
setVoiceToken(voiceModelTokenMap[selected]);
};
const handleEnqueueTts = async (ev: React.FormEvent<HTMLButtonElement>) => {
ev.preventDefault();
if (!textBuffer) {
return false;
}
// Check if the text hasn't changed and the voice hasn't changed
if (textBuffer === lastEnqueuedText && voiceToken === lastSelectedVoice) {
setIsPlaying(!isPlaying);
setIsAudioLoading(false);
return false;
}
setIsAudioLoading(true);
const modelToken = voiceToken;
const request = {
uuid_idempotency_token: uuidv4(),
tts_model_token: modelToken,
inference_text: textBuffer,
};
const response = await GenerateTtsAudio(request);
Analytics.ttsGenerate(modelToken, textBuffer.length);
if (GenerateTtsAudioIsOk(response)) {
setMaybeTtsError(undefined);
if (response.inference_job_token_type === "generic") {
enqueueInferenceJob(
response.inference_job_token,
FrontendInferenceJobType.TextToSpeech
);
} else {
enqueueTtsJob(response.inference_job_token);
}
// Store the job token
setJobToken(response.inference_job_token);
// Store the last enqueued text
setLastEnqueuedText(textBuffer);
// Store the last selected voice
setLastSelectedVoice(voiceToken);
} else if (GenerateTtsAudioIsError(response)) {
setMaybeTtsError(response.error);
}
return false;
};
useEffect(() => {
if (!jobToken) return;
const fetch = async () => {
const jobStatusResponse = await GetTtsInferenceJobStatus(jobToken);
if (GetTtsInferenceJobStatusIsOk(jobStatusResponse)) {
const job = inferenceJobs.find(job => job.jobToken === jobToken);
if (job && jobStatusResponse.state.maybe_public_bucket_wav_audio_path) {
const audioLink = new BucketConfig().getGcsUrl(
jobStatusResponse.state.maybe_public_bucket_wav_audio_path
);
setCurrentAudioUrl(audioLink);
if (audioLink !== currentAudioUrl) {
setIsAudioLoading(false);
setIsPlaying(true);
}
}
}
};
fetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jobToken, inferenceJobs, isPlaying]);
const handleAudioFinish = () => {
setIsPlaying(false);
};
const voiceOptions = Object.keys(voiceModelTokenMap);
// Show errors on TTS failure
let maybeError = <></>;
if (!!maybeTtsError) {
let hasMessage = false;
let message = <></>;
switch (maybeTtsError) {
case GenerateTtsAudioErrorType.TooManyRequests:
hasMessage = true;
message = (
<>Too many requests! Please wait a few minutes then try again.</>
);
break;
case GenerateTtsAudioErrorType.ServerError |
GenerateTtsAudioErrorType.BadRequest |
GenerateTtsAudioErrorType.NotFound:
break;
}
if (hasMessage) {
maybeError = (
<div
className="alert alert-primary alert-dismissible fade show mt-3"
role="alert"
>
<button
className="btn-close"
onClick={() => setMaybeTtsError(undefined)}
data-bs-dismiss="alert"
aria-label="Close"
></button>
{message}
</div>
);
}
}
const generateRandomText = () => {
const randomIndex = Math.floor(Math.random() * RandomTexts.length);
const randomText = RandomTexts[randomIndex];
setTextBuffer(randomText);
};
return (
<Panel padding={true}>
<form className="d-flex flex-column">
<div>
<div className="d-flex">
<label className="sub-title flex-grow-1">Select a Voice</label>
</div>
<SelectionBubbles
options={voiceOptions}
onSelect={handleVoiceSelection}
/>
</div>
<div className="d-flex flex-column mt-3">
<div className="d-flex justify-content-center pb-2">
<label className="sub-title flex-grow-1 pb-0">Your Text</label>
<Button
icon={faShuffle}
label="Randomize Text"
onClick={generateRandomText}
variant="link"
className="fs-7 randomize-text-button"
/>
</div>
<TextArea
placeholder={placeholder}
value={textBuffer}
onChange={handleChangeText}
rows={4}
resize={false}
/>
</div>
{maybeError}
<div className="d-flex gap-3 align-items-center mt-4">
<Button
icon={isPlaying ? faPause : faPlay}
square={true}
onClick={handleEnqueueTts}
isLoading={isAudioLoading}
disabled={textBuffer.length === 0}
/>
<DemoTtsAudioPlayer
filename={currentAudioUrl || ""}
play={isPlaying}
onFinish={handleAudioFinish}
/>
</div>
</form>
</Panel>
);
}
@@ -0,0 +1,11 @@
@import "scss/colours.scss";
.randomize-text-button {
color: desaturate($primary, 30%) !important;
opacity: 1;
&:hover {
color: desaturate($primary, 10%) !important;
opacity: 0.85;
}
}
@@ -0,0 +1,88 @@
const RandomTexts = [
"Hello, world!",
"Welcome to the future.",
"The quick brown fox jumps over the lazy dog.",
"In a hole in the ground there lived a hobbit.",
"Once upon a time in a land far, far away.",
"Can't believe you've done this.",
"Sometimes, I wish I was an octopus, so I could slap eight people at once.",
"Explaining why one is right is not arguing, it's just clarifying.",
"Wallets are like onions; opening them brings tears.",
"I'm not lazy, I'm on energy-saving mode.",
"When life gives you melons, dyslexia might be involved.",
"Atoms are untrustworthy; they make up everything.",
"Glue history books are impossible to put down.",
"Time travel jokes are funny in any era, but you might have missed it.",
"I would lose weight, but I hate losing.",
"A messy room is just an open-air museum.",
"Trying to fail and succeeding is a real conundrum.",
"Procrastination is just engaging in numerous side quests.",
"Being early for tomorrow is a new kind of punctuality.",
"Adulthood is the longest test run that never ends.",
"Beauty sleep's advertising is misleading.",
"I'm on a whiskey diet. I've lost three days already.",
"Magical beds remember all forgotten tasks.",
"Pillows are the unsung heroes of hairstyling.",
"I'm not saying I'm Batman, but have you ever seen me and Batman in the same room?",
"Couches are the battlegrounds for avoiding problems.",
"Life's brevity demands self-laughter or outsourcing it.",
"Being a limited edition is far from weird.",
"Office chairs that spin make the job worth it.",
"Wake-up time defines personhood, not the time of day.",
"Math problems multiply in silence.",
"Physical conservatism is the new fitness trend.",
"Extra fries misunderstood as exercise is a common error.",
"I'm fluent in three languages: English, sarcasm, and profanity.",
"Multitasking involves listening, ignoring, and forgetting simultaneously.",
"Indecisiveness is a choice, sometimes.",
"I'm really good at stuff until people watch me do that stuff.",
"Correctness needs no argument, just explanation.",
"Coffee relationships are the most committed ones.",
"Rush hour is the greatest paradox.",
"I'm not bossy. I just have better ideas.",
"Silence and duct tape both have their merits.",
"I'm not procrastinating. I'm under a lot of pressure and very well could be a diamond soon.",
"Battery saver mode is essential for work avoidance.",
"Exploration is just being lost with style.",
"Pressing harder on a remote control seems like a valid effort when batteries are weak.",
"I'm convinced that bugs in software are just features in disguise.",
"Adulthood is getting excited about a new sponge for the kitchen, I've accepted this.",
"Life's too short, I always smile while I still have all my teeth.",
"In video games, I've learned that encountering enemies often means you're going in the right direction.",
"I'm often on the hunt for motivation, but it's like searching for a 404 error.",
"Debating with a computer, I've realized, is as effective as trying to solve algebra by chewing bubblegum.",
"During rush hour, I remind myself that patience is indeed a virtue.",
"I transformed a shy friend into a social butterfly, and now I wonder what I've unleashed.",
"The quest for sleep, sanity, and a peaceful hobbit life is something I'm still on.",
"My bed has a magical ability to remind me of forgotten tasks at night.",
"When the Wi-Fi goes down, I discover family members are actually nice people.",
"A cookie in each hand, I believe, is the true definition of a balanced diet.",
"Why use Google when I'm convinced someone believes they have all the answers?",
"Exploring the great indoors, I've found, is because outside is overrated.",
"I'm fluent in three languages: English, sarcasm, and late-night coding.",
"Why wait for a time machine when I have memories and music?",
"The best way to avoid chores, I've found, is to pretend not to know they exist.",
"If I can't find the sunshine, I've learned to be the person who switches on the light.",
"Realizing too late that adulthood is a trap is a lesson I've learned the hard way.",
"The art of being wise, I've discovered, is knowing what to overlook, like unread emails.",
"Why call it fast food if I have to wait in line for it?",
"I've discovered that my spirit animal is a sloth on weekdays.",
"Making a 'quick' phone call, I've realized, is never quick.",
"The mystery of how one sock always disappears in the laundry remains unsolved, I've noticed.",
"Finding out that my emergency contact is a pizza delivery service was a surprise.",
"The realization that autocorrect has become my worst enema... I mean, enemy, is frustrating.",
"Wondering why my coffee tastes like I should have made it myself is a daily ritual.",
"The silent joy of canceling plans is an underrated pleasure I cherish.",
"Discovering that my daily workout is trying to dodge spoilers online is a modern struggle.",
"Realizing that my phone's battery life lasts longer than my motivation is a harsh reality.",
"The paradox of being a perfectionist procrastinator is a real struggle I face.",
"Finding comfort in the fact that chocolate understands me is a sweet relief.",
"Realizing I'm not late, I'm just on a creative time schedule is how I cope.",
"Discovering that my hobby is collecting dust on my gym membership was an eye-opener.",
"The irony of being a tech expert but unable to fix my sleep schedule is not lost on me.",
"Understanding that my to-do list is more of a suggestion than a requirement helps me sleep at night.",
"The thrill of solving a bug in code is the coder's version of a plot twist, I've found.",
"Realizing that my idea of a balanced diet is having a piece of tech in both hands is a modern dilemma.",
];
export default RandomTexts;
@@ -17,7 +17,9 @@ export type WorkflowValuesType = {
workflowConfig: string;
seed: string;
sdModelToken: string;
sdModelTitle: string;
loraModelToken: string;
loraModelTitle: string;
posPrompt: string;
negPrompt: string;
// Advance Options Values
@@ -57,7 +57,9 @@ export default function PageFilterControls({
workflowConfig: "weight_q8sz47gmfw2zx02snrbz88ns9",
seed: "",
sdModelToken: "",
sdModelTitle:"",
loraModelToken: "",
loraModelTitle: "",
posPrompt: "",
negPrompt: "",
...VideoIvs,
@@ -75,7 +77,9 @@ export default function PageFilterControls({
workflowConfig: "weight_q8sz47gmfw2zx02snrbz88ns9",
seed: curr.seed,
sdModelToken: "",
sdModelTitle: "",
loraModelToken: "",
loraModelTitle: "",
posPrompt: "",
negPrompt: "",
...VideoIvs,
@@ -178,7 +182,7 @@ export default function PageFilterControls({
}}
value={{
token:workflowValues.sdModelToken,
title:""
title:workflowValues.sdModelTitle,
}}
/>
</div>