diff --git a/_redirects b/_redirects index cb8399f97f..81385faefe 100644 --- a/_redirects +++ b/_redirects @@ -43,6 +43,7 @@ /voice-conversion* /index.html 200 /w2l* /index.html 200 /voice-designer* /index.html 200 +/weight* /index.html 200 # Test paths /dev-tts* /index.html 200 diff --git a/packages/components/src/api/_common/enums/WeightCategory.ts b/packages/components/src/api/_common/enums/WeightCategory.ts new file mode 100644 index 0000000000..e0c256eb67 --- /dev/null +++ b/packages/components/src/api/_common/enums/WeightCategory.ts @@ -0,0 +1,7 @@ +export enum WeightCategory { + TTS = "text_to_speech", + VOCODER = "vocoder", + VC = "voice_conversion", + ZS = "zero_shot", + SD = "image_generation", +} diff --git a/packages/components/src/api/_common/enums/WeightType.ts b/packages/components/src/api/_common/enums/WeightType.ts new file mode 100644 index 0000000000..f4e51c6846 --- /dev/null +++ b/packages/components/src/api/_common/enums/WeightType.ts @@ -0,0 +1,10 @@ +export enum WeightType { + TT2 = "tt2", + HIFIGAN_TT2 = "hifigan_tt2", + RVCv2 = "rvc_v2", + SD_15 = "sd_1.5", + SDXL = "sdxl", + SVC = "so_vits_svc", + LORA = "loRA", + VALL_E = "vall_e", +} diff --git a/packages/components/src/api/weights/GetWeight.ts b/packages/components/src/api/weights/GetWeight.ts new file mode 100644 index 0000000000..b574448113 --- /dev/null +++ b/packages/components/src/api/weights/GetWeight.ts @@ -0,0 +1,28 @@ +// import MakeRequest from "../MakeRequest"; +import { UserDetailsLight } from "../_common/UserDetailsLight"; +import { WeightCategory } from "../_common/enums/WeightCategory"; +import { WeightType } from "../_common/enums/WeightType"; + +export interface Weight { + weight_token: string; + weight_type: WeightType; + weight_category: WeightCategory; + maybe_creator_user: UserDetailsLight | null; + creator_set_visibility: string; + created_at: Date; + updated_at: Date; + title: string; + description_markdown: string; +} + +// export interface GetMediaRequest {} + +// export interface GetMediaResponse { +// success: boolean, +// media_file?: MediaFile, +// } + +// export const GetMedia = MakeRequest({ +// method: "GET", +// routingFunction: (mediaFileToken: string) => `/v1/media_files/file/${mediaFileToken}`, +// }); diff --git a/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx b/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx index 4eceaeee37..206b841044 100644 --- a/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx +++ b/packages/fakeyou.com/src/components/common/Accordion/Accordion.tsx @@ -39,17 +39,25 @@ function AccordionItem({ ) as AccordionContextType; const isOpen = openItems.includes(title); - const [contentHeight, setContentHeight] = useState( - isOpen ? undefined : 0 + const [contentHeight, setContentHeight] = useState( + defaultOpen ? 0 : 0 ); const contentRef = useRef(null); useEffect(() => { + // Update content height whenever the accordion item is opened or closed if (contentRef.current) { setContentHeight(isOpen ? contentRef.current.scrollHeight : 0); } }, [isOpen]); + useEffect(() => { + // Adjust height when the content changes, only if the item is open + if (isOpen && contentRef.current) { + setContentHeight(contentRef.current.scrollHeight); + } + }, [children, isOpen]); + const heightProps = useSpring({ height: isOpen ? `${contentHeight}px` : "0px", config: { tension: 300, friction: 25, clamp: true }, @@ -89,24 +97,22 @@ function AccordionItem({ } function Accordion({ children }: AccordionProps) { - const [openItems, setOpenItems] = useState([]); + // Extract titles of items that should be initially open + const defaultOpenTitles = React.Children.toArray(children) + .filter((child: any) => child.props.defaultOpen) + .map((child: any) => child.props.title); + + // Initialize openItems with the titles of items that should be open + const [openItems, setOpenItems] = useState(defaultOpenTitles); const toggleItem = (title: string) => { if (openItems.includes(title)) { - setOpenItems(openItems.filter((item) => item !== title)); + setOpenItems(openItems.filter(item => item !== title)); } else { setOpenItems([...openItems, title]); } }; - // Initialize openItems based on defaultOpen props of AccordionItem children - useEffect(() => { - const defaultOpenItems = React.Children.toArray(children) - .filter((child: any) => child.props.defaultOpen) - .map((child: any) => child.props.title); - setOpenItems(defaultOpenItems); - }, [children]); - return (
{children}
diff --git a/packages/fakeyou.com/src/components/common/Alert/Alert.scss b/packages/fakeyou.com/src/components/common/Alert/Alert.scss index 315c3dd026..1d175fdc46 100644 --- a/packages/fakeyou.com/src/components/common/Alert/Alert.scss +++ b/packages/fakeyou.com/src/components/common/Alert/Alert.scss @@ -2,7 +2,6 @@ border-radius: 0.5rem !important; font-weight: 400; font-size: 14px; - margin-bottom: 1rem !important; padding: 0.75rem 0.75rem !important; &.alert-dismissible { diff --git a/packages/fakeyou.com/src/components/common/Badge/Badge.scss b/packages/fakeyou.com/src/components/common/Badge/Badge.scss index 0346f788f3..81c7b48b3d 100644 --- a/packages/fakeyou.com/src/components/common/Badge/Badge.scss +++ b/packages/fakeyou.com/src/components/common/Badge/Badge.scss @@ -1,8 +1,9 @@ @import "scss/colours.scss"; .fy-badge { + display: flex; font-weight: 600; - border-radius: 0.35rem; + border-radius: 0.5rem; font-size: 13px; padding: 4px 8px; transition: 0.2s; @@ -11,8 +12,8 @@ -webkit-user-select: none; -ms-user-select: none; user-select: none; - height: 100%; margin-bottom: 10px; + padding: 4px 8px; &.shadow { box-shadow: 0 0 12px rgba($bg, 0.4); diff --git a/packages/fakeyou.com/src/components/common/Card/AudioCard/AudioCard.tsx b/packages/fakeyou.com/src/components/common/Card/AudioCard/AudioCard.tsx index 8a992fd650..4323584c40 100644 --- a/packages/fakeyou.com/src/components/common/Card/AudioCard/AudioCard.tsx +++ b/packages/fakeyou.com/src/components/common/Card/AudioCard/AudioCard.tsx @@ -46,7 +46,7 @@ export default function AudioCard({ data, type }: AudioCardProps) { <>
-
+
@@ -71,7 +71,7 @@ export default function AudioCard({ data, type }: AudioCardProps) { <>
-
+
diff --git a/packages/fakeyou.com/src/components/common/Card/ImageCard/ImageCard.tsx b/packages/fakeyou.com/src/components/common/Card/ImageCard/ImageCard.tsx index 9c1b14d01e..4bda2d687c 100644 --- a/packages/fakeyou.com/src/components/common/Card/ImageCard/ImageCard.tsx +++ b/packages/fakeyou.com/src/components/common/Card/ImageCard/ImageCard.tsx @@ -46,7 +46,7 @@ export default function ImageCard({ data, type }: ImageCardProps) {
-
+
@@ -78,7 +78,7 @@ export default function ImageCard({ data, type }: ImageCardProps) {
-
+
diff --git a/packages/fakeyou.com/src/components/common/Card/VideoCard/VideoCard.tsx b/packages/fakeyou.com/src/components/common/Card/VideoCard/VideoCard.tsx index e478fe27c2..b48ab49d0f 100644 --- a/packages/fakeyou.com/src/components/common/Card/VideoCard/VideoCard.tsx +++ b/packages/fakeyou.com/src/components/common/Card/VideoCard/VideoCard.tsx @@ -42,7 +42,7 @@ export default function VideoCard({ data, type }: VideoCardProps) {
-
+
@@ -77,7 +77,7 @@ export default function VideoCard({ data, type }: VideoCardProps) {
-
+
diff --git a/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.scss b/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.scss index 3dcd92c1d4..4994731213 100644 --- a/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.scss +++ b/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.scss @@ -3,6 +3,7 @@ .favorite-button { display: flex; align-items: center; + justify-content: center; background-color: rgba(lighten($secondary, 5%), 0.9); color: inherit; border: none; @@ -13,6 +14,7 @@ font-size: 16px; transition: 0.2s; padding: 3px 8px; + min-width: 56px; border-radius: 0.5rem; &.shadow { @@ -46,10 +48,14 @@ ); } } + + &.large { + padding: 6px 10px; + } } .favorite-button .icon-default { - color: $white; + color: map-get($badge-colors, "yellow"); opacity: 0.7; transition: 0.2s; } @@ -64,9 +70,10 @@ } .favorite-number { + color: $white; font-size: 14px; font-weight: 400 !important; - opacity: 0.8; + opacity: 0.9; transition: 0.2s; } diff --git a/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.tsx b/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.tsx index 53ff39e4d4..1a16470ada 100644 --- a/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.tsx +++ b/packages/fakeyou.com/src/components/common/FavoriteButton/FavoriteButton.tsx @@ -5,12 +5,14 @@ import { faBookmark as faBookmarkOutline } from "@fortawesome/pro-regular-svg-ic import Tippy from "@tippyjs/react"; import "tippy.js/dist/tippy.css"; import "./FavoriteButton.scss"; +import useShortenNumber from "hooks/useShortenNumber"; interface FavoriteButtonProps { initialToggled?: boolean; onToggle: (toggled: boolean) => Promise; - favoriteCount?: number; + favoriteCount: number; overlay?: boolean; + large?: boolean; } export default function FavoriteButton({ @@ -18,6 +20,7 @@ export default function FavoriteButton({ onToggle, favoriteCount, overlay, + large, }: FavoriteButtonProps) { const [isToggled, setIsToggled] = useState(initialToggled); const [isLoading, setIsLoading] = useState(false); @@ -37,7 +40,8 @@ export default function FavoriteButton({ const buttonClass = isToggled ? "favorite-button toggled" : "favorite-button"; const buttonShadow = overlay ? "shadow" : ""; const iconClass = isToggled ? "icon-toggled" : "icon-default"; - const toolTip = isToggled ? "Unfavorite" : "Favorite"; + const toolTip = isToggled ? "Unbookmark" : "Bookmark"; + let favoriteCountShort = useShortenNumber(favoriteCount); return (
@@ -52,13 +56,15 @@ export default function FavoriteButton({
diff --git a/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.scss b/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.scss index d5649adddd..f6505a1b20 100644 --- a/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.scss +++ b/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.scss @@ -3,6 +3,7 @@ .like-button { display: flex; align-items: center; + justify-content: center; background-color: rgba(lighten($secondary, 5%), 0.9); color: inherit; border: none; @@ -40,13 +41,17 @@ background-color: rgba(#593a48, 0.9); &:hover { - background-color: darken(rgba(#593a48, 0.9), 2%); + background-color: lighten(rgba(#593a48, 0.9), 10%); } } + + &.large { + padding: 6px 10px; + } } .like-button .icon-default { - color: $white; + color: $primary; opacity: 0.7; transition: 0.2s; } @@ -61,9 +66,10 @@ } .like-number { + color: $white; font-size: 14px; font-weight: 400 !important; - opacity: 0.8; + opacity: 0.9; transition: 0.2s; } diff --git a/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.tsx b/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.tsx index 10ffb1cc0f..ba1bd25761 100644 --- a/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.tsx +++ b/packages/fakeyou.com/src/components/common/LikeButton/LikeButton.tsx @@ -5,12 +5,14 @@ import { faHeart as faHeartOutline } from "@fortawesome/pro-regular-svg-icons"; import Tippy from "@tippyjs/react"; import "tippy.js/dist/tippy.css"; import "./LikeButton.scss"; +import useShortenNumber from "hooks/useShortenNumber"; interface LikeButtonProps { initialToggled?: boolean; onToggle: (toggled: boolean) => Promise; - likeCount?: number; + likeCount: number; overlay?: boolean; + large?: boolean; } export default function LikeButton({ @@ -18,6 +20,7 @@ export default function LikeButton({ onToggle, likeCount, overlay, + large, }: LikeButtonProps) { const [isToggled, setIsToggled] = useState(initialToggled); const [isLoading, setIsLoading] = useState(false); @@ -38,6 +41,7 @@ export default function LikeButton({ const buttonShadow = overlay ? "shadow" : ""; const iconClass = isToggled ? "icon-toggled" : "icon-default"; const toolTip = isToggled ? "Unlike" : "Like"; + let likeCountShort = useShortenNumber(likeCount); return (
@@ -53,13 +57,13 @@ export default function LikeButton({
diff --git a/packages/fakeyou.com/src/components/common/Modal/Modal.scss b/packages/fakeyou.com/src/components/common/Modal/Modal.scss new file mode 100644 index 0000000000..31248ab370 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Modal/Modal.scss @@ -0,0 +1,45 @@ +@import "scss/colours.scss"; + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + background-color: rgba(darken($panel, 8%), 0.7) !important; + display: flex; + justify-content: center; + align-items: center; +} + +.modal { + z-index: 1050; + display: block; +} + +.modal-width-auto { + max-width: fit-content; + margin: auto; +} + +.modal-content { + flex: 0 1 auto; + min-width: 0; + background-color: $panel; + border-color: lighten($panel, 8%) !important; + border: 1px solid; + border-radius: 0.5rem; + box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8); + -webkit-box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8); + -moz-box-shadow: 0px 9px 33px 1px rgba(19, 19, 27, 0.8); +} + +.modal-header { + border-bottom: 1px solid lighten($panel, 8%); +} + +.modal-header .btn-close { + filter: brightness(0) invert(1); +} + +.modal-footer { + border-top: 1px solid lighten($panel, 8%); +} diff --git a/packages/fakeyou.com/src/components/common/Modal/Modal.tsx b/packages/fakeyou.com/src/components/common/Modal/Modal.tsx index dd5fbc94cb..0e8ee542c6 100644 --- a/packages/fakeyou.com/src/components/common/Modal/Modal.tsx +++ b/packages/fakeyou.com/src/components/common/Modal/Modal.tsx @@ -1,10 +1,9 @@ -import React, { useRef, useEffect } from "react"; +import React, { useEffect } from "react"; import Button from "../Button"; -import { faTrashAlt } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSpring, a } from "@react-spring/web"; - - +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import "./Modal.scss"; interface ModalProps { show: boolean; @@ -13,19 +12,47 @@ interface ModalProps { onConfirm?: (e: React.MouseEvent) => any; title: string; content: React.ReactNode; + icon?: IconDefinition; + autoWidth?: boolean; + showButtons?: boolean; } -const Modal: React.FC = ({ show, handleClose, onCancel: cancelEvent, onConfirm: confirmEvent, title, content }) => { - const modalRef = useRef(null); - +const Modal: React.FC = ({ + show, + handleClose, + onCancel: cancelEvent, + onConfirm: confirmEvent, + title, + content, + icon, + autoWidth, + showButtons = true, +}) => { const fadeIn = useSpring({ opacity: show ? 1 : 0, - config: { duration: 80, easing: (t) => t }, + config: { duration: 80, easing: t => t }, onRest: () => { if (!show) handleClose(); }, }); + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const modalContent = document.querySelector(".modal-content"); + if (modalContent && !modalContent.contains(event.target as Node)) { + handleClose(); + } + }; + + if (show) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [show, handleClose]); + const onCancel = (e: React.MouseEvent) => { if (cancelEvent) cancelEvent(e); handleClose(); @@ -36,40 +63,22 @@ const Modal: React.FC = ({ show, handleClose, onCancel: cancelEvent, handleClose(); }; - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { - handleClose(); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [handleClose]); - if (!show) { return null; } return ( -
-
+
+
- + {icon && } {title}
{content}
-
-
+ {showButtons && ( +
+
+ )}
diff --git a/packages/fakeyou.com/src/components/common/RatingStars/RatingStars.scss b/packages/fakeyou.com/src/components/common/RatingStars/RatingStars.scss new file mode 100644 index 0000000000..b150c0c661 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/RatingStars/RatingStars.scss @@ -0,0 +1,4 @@ +.rating-stars { + font-size: 16px; + color: #e88b00; +} diff --git a/packages/fakeyou.com/src/components/common/RatingStars/RatingStars.tsx b/packages/fakeyou.com/src/components/common/RatingStars/RatingStars.tsx new file mode 100644 index 0000000000..c80fac2050 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/RatingStars/RatingStars.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faStar, faStarHalfAlt } from "@fortawesome/pro-solid-svg-icons"; +import { faStar as faStarOutline } from "@fortawesome/pro-regular-svg-icons"; +import "./RatingStars.scss"; + +type StarRatingProps = { + rating: number; +}; + +const RatingStars: React.FC = ({ rating }) => { + const roundToNearestHalf = (num: number) => Math.round(num * 2) / 2; + + const roundedRating = roundToNearestHalf(rating); + const fullStars = Math.floor(roundedRating); + const hasHalfStar = roundedRating % 1 !== 0; + + const stars = []; + + for (let i = 0; i < fullStars; i++) { + stars.push(); + } + + if (hasHalfStar) { + stars.push(); + } + + while (stars.length < 5) { + stars.push( + + ); + } + + return ( +
+
{stars}
+

{rating}

+
+ ); +}; + +export default RatingStars; diff --git a/packages/fakeyou.com/src/components/common/RatingStars/index.ts b/packages/fakeyou.com/src/components/common/RatingStars/index.ts new file mode 100644 index 0000000000..e736c38cd3 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/RatingStars/index.ts @@ -0,0 +1 @@ +export { default } from "./RatingStars"; diff --git a/packages/fakeyou.com/src/components/common/SocialButton/SocialButton.scss b/packages/fakeyou.com/src/components/common/SocialButton/SocialButton.scss new file mode 100644 index 0000000000..850703a289 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/SocialButton/SocialButton.scss @@ -0,0 +1,75 @@ +@import "scss/colours.scss"; + +.social-button { + display: flex; + flex-direction: column; + align-items: center; + margin: 0; + padding: 0; + background: none; + border: none; +} + +.social-button-icon { + font-size: 32px !important; + height: 52px; + width: 52px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + border: 0px solid; + margin-bottom: 10px; + transition: 0.2s; + + &:hover { + opacity: 0.8; + } + + &.bg-facebook { + color: $white; + background-color: #4267b2; + } + + &.bg-reddit { + color: $white; + background-color: #ff5700; + } + + &.bg-reddit { + color: $white; + background-color: #ff5700; + } + + &.bg-whatsapp { + color: $white; + background-color: #25d366; + } + + &.bg-x { + color: $white; + background-color: #1da1f2; + } + + &.bg-email { + color: $white; + background-color: #858585; + } +} + +.social-button-text { + font-size: 12px; + font-weight: 600; +} + +@media (min-width: 768px) { + .social-button-icon { + height: 62px; + width: 62px; + } + + .social-button-text { + font-size: 13px; + font-weight: 600; + } +} diff --git a/packages/fakeyou.com/src/components/common/SocialButton/SocialButton.tsx b/packages/fakeyou.com/src/components/common/SocialButton/SocialButton.tsx new file mode 100644 index 0000000000..e46d2d33e7 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/SocialButton/SocialButton.tsx @@ -0,0 +1,76 @@ +import { + faFacebookF, + faRedditAlien, + faWhatsapp, + faTwitter, +} from "@fortawesome/free-brands-svg-icons"; +import { faXmark, faEnvelope } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React from "react"; +import "./SocialButton.scss"; + +interface SocialButtonProps { + social: "x" | "whatsapp" | "facebook" | "reddit" | "email"; + shareUrl: string; + shareText: string; +} + +export default function SocialButton({ + social, + shareUrl, + shareText, +}: SocialButtonProps) { + const socialIcons = { + x: faTwitter, + whatsapp: faWhatsapp, + facebook: faFacebookF, + reddit: faRedditAlien, + email: faEnvelope, + }; + + let socialIcon = socialIcons[social] || faXmark; + + const getShareLink = (social: string, url: string, text: string) => { + switch (social) { + case "x": + return `https://twitter.com/intent/tweet?url=${encodeURIComponent( + url + )}&text=${encodeURIComponent(text)}`; + case "facebook": + return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent( + url + )}`; + case "whatsapp": + return `https://wa.me/?text=${encodeURIComponent( + text + )} ${encodeURIComponent(url)}`; + case "reddit": + return `https://www.reddit.com/submit?url=${encodeURIComponent( + url + )}&title=${encodeURIComponent(text)}`; + case "email": + return `mailto:?subject=${encodeURIComponent( + text + )}&body=${encodeURIComponent(url)}`; + default: + return "#"; + } + }; + + const handleClick = () => { + const shareLink = getShareLink(social, shareUrl, shareText); + window.open(shareLink, "_blank"); + }; + + return ( + + ); +} diff --git a/packages/fakeyou.com/src/components/common/SocialButton/index.ts b/packages/fakeyou.com/src/components/common/SocialButton/index.ts new file mode 100644 index 0000000000..2486a0f50d --- /dev/null +++ b/packages/fakeyou.com/src/components/common/SocialButton/index.ts @@ -0,0 +1 @@ +export { default } from "./SocialButton"; diff --git a/packages/fakeyou.com/src/components/common/SplitPanel/SplitPanel.tsx b/packages/fakeyou.com/src/components/common/SplitPanel/SplitPanel.tsx index ae8ffb21f4..15bdca0a38 100644 --- a/packages/fakeyou.com/src/components/common/SplitPanel/SplitPanel.tsx +++ b/packages/fakeyou.com/src/components/common/SplitPanel/SplitPanel.tsx @@ -31,7 +31,8 @@ const Footer: React.FC = ({ children, padding }) => { // Panel Interface interface PanelProps { children: ReactNode; - divider?: boolean; + dividerHeader?: boolean; + dividerFooter?: boolean; clear?: boolean; className?: string; } @@ -41,7 +42,7 @@ const SplitPanel: React.FC & { Header: typeof Header; Body: typeof Body; Footer: typeof Footer; -} = ({ children, divider, clear, className }) => { +} = ({ children, dividerHeader, dividerFooter, clear, className }) => { let header: ReactNode, body: ReactNode, footer: ReactNode; const panelClassName = `${clear ? "panel-clear" : "panel"} ${ @@ -67,9 +68,9 @@ const SplitPanel: React.FC & { return (
{header} - {divider &&
} + {dividerHeader &&
} {body} - {divider &&
} + {dividerFooter &&
} {footer}
); diff --git a/packages/fakeyou.com/src/components/common/Tabs/NonRouteTabs.tsx b/packages/fakeyou.com/src/components/common/Tabs/NonRouteTabs.tsx new file mode 100644 index 0000000000..f9c447f609 --- /dev/null +++ b/packages/fakeyou.com/src/components/common/Tabs/NonRouteTabs.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import { useSpring, a } from "@react-spring/web"; +import "./Tabs.scss"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface NonRouteTabProps { + label: string; + icon?: IconDefinition; + isActive: boolean; + onClick: () => void; +} + +interface NonRouteTabsProps { + tabs: { + label: string; + content: React.ReactNode; + icon?: IconDefinition; + padding?: boolean; + }[]; +} + +function NonRouteTab({ label, icon, isActive, onClick }: NonRouteTabProps) { + return ( +
  • +
    + {icon && } + {label} +
    +
  • + ); +} + +function NonRouteTabs({ tabs }: NonRouteTabsProps) { + const [activeTab, setActiveTab] = useState(tabs[0].label); + const [fade, setFade] = useSpring(() => ({ + opacity: 1, + from: { opacity: 0 }, + config: { duration: 300 }, + })); + + const handleTabClick = (tabLabel: string) => { + if (tabLabel === activeTab) { + return; + } + + setFade({ opacity: 0 }); + setTimeout(() => { + setActiveTab(tabLabel); + setFade({ opacity: 1 }); + }, 50); + }; + + return ( + + ); +} + +export default NonRouteTabs; diff --git a/packages/fakeyou.com/src/components/common/Tabs/Tabs.scss b/packages/fakeyou.com/src/components/common/Tabs/Tabs.scss index 4e6c0a16d2..9e5c2bba9d 100644 --- a/packages/fakeyou.com/src/components/common/Tabs/Tabs.scss +++ b/packages/fakeyou.com/src/components/common/Tabs/Tabs.scss @@ -11,6 +11,7 @@ font-weight: 500; white-space: nowrap; transition: 0.2s; + cursor: pointer; &:focus { border-bottom: 4px solid transparent; diff --git a/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx b/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx index 2f9d25689b..5813a6520b 100644 --- a/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx +++ b/packages/fakeyou.com/src/components/layout/PageHeader/PageHeader.tsx @@ -94,10 +94,10 @@ export default function PageHeader({ if (!panel) { return ( -
    +
    {(back || showBackButton) && ( -
    +
    + {/* + + */} + + + + + @@ -628,7 +647,8 @@ class PageContainer extends React.Component< ; + ttsInferenceJobs: Array; + enqueueInferenceJob: ( + jobToken: string, + frontendInferenceJobType: FrontendInferenceJobType + ) => void; + inferenceJobsByCategory: Map>; + enqueueTtsJob: (jobToken: string) => void; +} + +export default function WeightPage({ + sessionWrapper, + sessionSubscriptionsWrapper, + inferenceJobs, + ttsInferenceJobs, + enqueueInferenceJob, + enqueueTtsJob, + inferenceJobsByCategory, +}: WeightProps) { + const { weight_token } = useParams<{ weight_token: string }>(); + const [weight, setWeight] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(false); + const timeUpdated = useTimeAgo(weight?.updated_at.toISOString() || ""); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + const [buttonLabel, setButtonLabel] = useState("Copy"); + + const getWeight = useCallback(async (weightToken: string) => { + // Dummy data + const dummyData = { + weight_token: "TM:xke15gz3v8pv", + title: "Harry Potter (Daniel Radcliffe)", + created_at: new Date(), + updated_at: new Date(), + weight_type: WeightType.HIFIGAN_TT2, + weight_category: WeightCategory.TTS, + maybe_creator_user: { + user_token: "test", + username: "test", + display_name: "Test", + gravatar_hash: "test", + default_avatar: { + image_index: 1, + color_index: 2, + }, + }, + creator_set_visibility: "Public", + description_markdown: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + }; + + // Simulate an API call delay + await new Promise(resolve => setTimeout(resolve, 1000)); + + if (dummyData) { + setWeight(dummyData); + setIsLoading(false); + } else { + setError(true); + setIsLoading(false); + } + }, []); + + useEffect(() => { + getWeight(weight_token); + }, [weight_token, getWeight]); + + function renderWeightComponent(weight: Weight) { + switch (weight.weight_category) { + case WeightCategory.TTS: + return ( + + ); + case WeightCategory.VC: + return ( + + ); + + case WeightCategory.ZS: + return ( + + ); + default: + return null; + } + } + + //Loading state + if (isLoading) + return ( + <> + +
    +
    +

    + +

    + +
    +

    + +

    +

    + +

    +
    +
    +
    +

    + +

    +

    + +

    +

    + +

    +
    +
    +
    + +
    + +

    + +

    +

    + +

    +
    +
    + + ); + + //Error state + if (error || !weight) + return ( + + +
    + } + /> + + ); + + const weightTypeMap: Record< + WeightType, + { weightType: string; weightTagColor: string } + > = { + [WeightType.TT2]: { + weightType: "Tacotron 2", + weightTagColor: "ultramarine", + }, + [WeightType.HIFIGAN_TT2]: { + weightType: "HiFi-GAN Tacotron 2", + weightTagColor: "blue", + }, + [WeightType.VALL_E]: { weightType: "VALL-E", weightTagColor: "purple" }, + [WeightType.LORA]: { weightType: "LoRA", weightTagColor: "pink" }, + [WeightType.RVCv2]: { + weightType: "RVCv2", + weightTagColor: "orange", + }, + [WeightType.SD_15]: { + weightType: "Stable Diffusion 1.5", + weightTagColor: "lime", + }, + [WeightType.SDXL]: { + weightType: "Stable Diffusion XL", + weightTagColor: "green", + }, + [WeightType.SVC]: { + weightType: "SVC", + weightTagColor: "aqua", + }, + }; + + let { weightType, weightTagColor } = weightTypeMap[weight.weight_type] || { + weightType: "", + weightTagColor: "", + }; + + const weightCategoryMap: Record = + { + [WeightCategory.TTS]: { weightCategory: "Text to Speech" }, + [WeightCategory.VC]: { weightCategory: "Voice to Voice" }, + [WeightCategory.SD]: { weightCategory: "Stable Diffusion" }, + [WeightCategory.ZS]: { weightCategory: "Voice Designer" }, + [WeightCategory.VOCODER]: { weightCategory: "Vocoder" }, + }; + + let { weightCategory } = weightCategoryMap[weight.weight_category] || { + weightCategory: "", + }; + + const voiceDetails = [ + { property: "Type", value: weightType }, + { property: "Category", value: weightCategory }, + { + property: "Visibility", + value: weight.creator_set_visibility.toString(), + }, + { property: "Created at", value: weight.created_at.toString() }, + { property: "Updated at", value: weight.updated_at.toString() }, + ]; + + const imageDetails = [ + { property: "Type", value: weightType }, + { property: "Category", value: weightCategory }, + { + property: "Visibility", + value: weight.creator_set_visibility.toString(), + }, + { property: "Created at", value: weight.created_at.toString() }, + { property: "Updated at", value: weight.updated_at.toString() }, + + //more to add for image/stable diffusion details + ]; + + let weightDetails = undefined; + + switch (weight.weight_category) { + case WeightCategory.TTS: + weightDetails = ; + break; + case WeightCategory.VC: + weightDetails = ; + break; + case WeightCategory.ZS: + weightDetails = ; + break; + case WeightCategory.SD: + weightDetails = ; + break; + default: + } + + let modMediaDetails = undefined; + + //dummy content + const modDetails = [ + { property: "Model creator is banned", value: "good standing" }, + { + property: "Result creator is banned (if user)", + value: "good standing", + }, + { + property: "Result creator IP address", + value: "0.0.0.0", + }, + { + property: "Mod deleted at (UTC)", + value: "not deleted", + }, + { + property: "Result creator deleted at (UTC)", + value: "not deleted", + }, + ]; + + if (sessionWrapper.canBanUsers()) { + modMediaDetails = ( + + + + ); + } + + const handleBookmark = async (data: any) => { + console.log( + `The item is now ${data.isLiked ? "Bookmarked" : "Not Bookmarked"}.` + ); + }; + + const subtitleDivider = |; + + const openShareModal = () => { + setIsShareModalOpen(true); + }; + + const closeShareModal = () => { + setIsShareModalOpen(false); + }; + + const handleCopyLink = () => { + navigator.clipboard.writeText(shareUrl); + setButtonLabel("Copied!"); + setTimeout(() => setButtonLabel("Copy"), 1000); + }; + + const shareUrl = `https://fakeyou.com/weight/${weight.weight_token}`; + const shareText = `Use FakeYou to generate speech as ${ + weight.title || "your favorite characters" + }!`; + + return ( +
    + + + {weight.title} +
    + } + subText={ +
    +
    +
    + +
    + {subtitleDivider} +

    {weightCategory}

    + {subtitleDivider} +
    + + +
    +
    +
    + } + /> + +
    +
    +
    {renderWeightComponent(weight)}
    + + {weight.description_markdown !== "" && ( + +

    Description

    +

    {weight.description_markdown}

    +
    + )} + +
    +

    Comments

    + +
    +
    +
    +
    +
    +
    */} +
    + + +
    + +
    + + {weight.maybe_creator_user?.display_name} + +

    Updated: {timeUpdated}

    +
    +
    +
    + + + + {weightDetails} + + + {modMediaDetails} + + +
    +
    +
    +
    +
    + + +
    + + +

    Comments

    + +
    +
    +
    + + {/* Share Modal */} + +
    + + + + + +
    +
    +
    + +
    + +
    +
    + } + /> +
    + ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/weight/inference_panels/TtsInferencePanel.tsx b/packages/fakeyou.com/src/v2/view/pages/weight/inference_panels/TtsInferencePanel.tsx new file mode 100644 index 0000000000..f66e0a614a --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/weight/inference_panels/TtsInferencePanel.tsx @@ -0,0 +1,216 @@ +import React, { useState } from "react"; +import { faDeleteLeft } from "@fortawesome/pro-solid-svg-icons"; +import Panel from "components/common/Panel/Panel"; +import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; +import TextArea from "components/common/TextArea"; +import { Button } from "components/common"; +import { + FrontendInferenceJobType, + InferenceJob, +} from "@storyteller/components/src/jobs/InferenceJob"; +import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs"; +import { faVolumeUp } from "@fortawesome/free-solid-svg-icons"; +import { v4 as uuidv4 } from "uuid"; +import Accordion from "components/common/Accordion"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + GenerateTtsAudio, + GenerateTtsAudioErrorType, + GenerateTtsAudioIsError, + GenerateTtsAudioIsOk, +} from "@storyteller/components/src/api/tts/GenerateTtsAudio"; +import { Analytics } from "common/Analytics"; +import { Link } from "react-router-dom"; +import { SessionTtsInferenceResultList } from "v2/view/_common/SessionTtsInferenceResultsList"; + +interface TtsInferencePanelProps { + sessionSubscriptionsWrapper: SessionSubscriptionsWrapper; + inferenceJobs: Array; + ttsInferenceJobs: Array; + enqueueInferenceJob: ( + jobToken: string, + frontendInferenceJobType: FrontendInferenceJobType + ) => void; + inferenceJobsByCategory: Map>; + voiceToken: string; + enqueueTtsJob: (jobToken: string) => void; +} + +export default function TtsInferencePanel({ + inferenceJobs, + sessionSubscriptionsWrapper, + ttsInferenceJobs, + enqueueInferenceJob, + enqueueTtsJob, + inferenceJobsByCategory, + voiceToken, +}: TtsInferencePanelProps) { + const [textBuffer, setTextBuffer] = useState(""); + const [isEnqueuing, setIsEnqueuing] = useState(false); + const [maybeTtsError, setMaybeTtsError] = useState< + GenerateTtsAudioErrorType | undefined + >(undefined); + const [isAudioLimitAlertVisible, setAudioLimitAlertVisible] = useState(false); + + const handleChangeText = (ev: React.FormEvent) => { + const textValue = (ev.target as HTMLTextAreaElement).value; + setTextBuffer(textValue); + setAudioLimitAlertVisible(textValue.length > 100); + }; + + const handleClearText = () => { + setTextBuffer(""); + }; + + const handleEnqueueTts = async (ev: React.FormEvent) => { + ev.preventDefault(); + + if (!textBuffer) { + return false; + } + + setIsEnqueuing(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); + } + } else if (GenerateTtsAudioIsError(response)) { + setMaybeTtsError(response.error); + } + + setIsEnqueuing(false); + + return false; + }; + + let maybeError = <>; + if (!!maybeTtsError) { + let hasMessage = false; + let message = <>; + switch (maybeTtsError) { + case GenerateTtsAudioErrorType.TooManyRequests: + hasMessage = true; + message = <>Error: Too many requests. Please try again in a bit.; + break; + case GenerateTtsAudioErrorType.ServerError | + GenerateTtsAudioErrorType.BadRequest | + GenerateTtsAudioErrorType.NotFound: + break; + } + + if (hasMessage) { + maybeError = ( +
    + + {message} +
    + ); + } + } + + let audioLimitAlert = <>; + if ( + isAudioLimitAlertVisible && + !sessionSubscriptionsWrapper.hasPaidFeatures() + ) { + audioLimitAlert = ( + <> +
    + + Note: Non-premium is limited to 12 seconds of audio.{" "} + + Upgrade now + + . + +
    + + ); + } + + return ( + +
    +
    +

    + + Generate TTS +

    +