mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 02:00:46 +00:00
Merge branch 'feature/mvp' of https://github.com/storytold/storyteller-frontend into feature/mvp
This commit is contained in:
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 (
|
||||
<>
|
||||
|
||||
+1
-1
@@ -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} />
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+36
@@ -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>
|
||||
);
|
||||
}
|
||||
+277
@@ -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;
|
||||
+2
@@ -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
|
||||
|
||||
+5
-1
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user