diff --git a/README.md b/README.md index 4312b95cd1..e1e24b4c83 100644 --- a/README.md +++ b/README.md @@ -28,66 +28,8 @@ Fast Prototyping with Better Netlify Pipeline: - [Netlify Functions Overview](https://docs.netlify.com/functions/overview) - [Tailwind Docs](https://tailwindcss.com/docs) -## Netlify Setup - -1. Install the [Netlify CLI](https://docs.netlify.com/cli/get-started/): - -```sh -npm i -g netlify-cli +Setup ``` - -If you have previously installed the Netlify CLI, you should update it to the latest version: - -```sh -npm i -g netlify-cli@latest -``` - -2. Sign up and log in to Netlify: - -```sh -netlify login -``` - -3. Create a new site: - -```sh -netlify init -``` - -## Development - -Ensure all packages are installed by running: - -```sh npm install -``` - -Run - -```sh -netlify dev -``` - -Open up [http://localhost:8888](http://localhost:8888), and you're ready to go! - -### Serve your site locally - -To serve your site locally in a production-like environment, run - -```sh -netlify serve -``` - -Your site will be available at [http://localhost:8888](http://localhost:8888). Note that it will not auto-reload when you make changes. - -## Deployment - -There are two ways to deploy your app to Netlify, you can either link your app to your git repo and have it auto deploy changes to Netlify, or you can deploy your app manually. If you've followed the setup instructions already, all you need to do is run this: - -```sh -# preview deployment -netlify deploy --build - -# production deployment -netlify deploy --build --prod +npm run dev ``` diff --git a/app/api.tsx b/app/api.tsx index 0e0bcc77a1..09dd5d1edc 100644 --- a/app/api.tsx +++ b/app/api.tsx @@ -11,4 +11,7 @@ export const inferTts = `${apiHost}/tts/inference`; export const listTts = `${apiHost}/tts/list`; // User Media -export const listMediaByUser = (username:string) => `${apiHost}/v1/media_files/list/user/${username}`; \ No newline at end of file +export const listMediaByUser = (username:string) => `${apiHost}/v1/media_files/list/user/${username}`; + +// Any Media +export const getMediaFileByToken = (fileToken:string) => `${apiHost}/v1/media_files/file/${fileToken}`; \ No newline at end of file diff --git a/app/api/BucketConfig.ts b/app/api/BucketConfig.ts new file mode 100644 index 0000000000..74b821c809 --- /dev/null +++ b/app/api/BucketConfig.ts @@ -0,0 +1,56 @@ +class BucketConfig { + isLocalDev: boolean; + + constructor() { + this.isLocalDev = + document.location.host.includes("localhost") || + document.location.host.includes("jungle.horse") || + document.location.host.startsWith("dev."); + } + + // TODO: Prevent callers with undefined/null paths + getGcsUrl(bucketRelativePath: string | undefined | null): string { + let bucket = this.getBucket(); + let path = bucketRelativePath; + if (path !== undefined && path !== null && !path.startsWith("/")) { + path = "/" + path; + } + return `https://storage.googleapis.com/${bucket}${path}`; + } + + private getBucket(): string { + return this.isLocalDev ? "dev-vocodes-public" : "vocodes-public"; + } + + getCdnUrl( + bucketRelativePath: string, + width?: number, + quality?: number + ): string { + // const basePath = this.isLocalDev + // ? "https://dev-cdn.fakeyou.com" + // : "https://cdn.fakeyou.com"; + const basePath = "https://cdn.fakeyou.com"; + let path = bucketRelativePath?.startsWith("/") + ? bucketRelativePath + : "/" + bucketRelativePath; + let resizeParams = ""; + if (width || quality) { + resizeParams = "cdn-cgi/image/"; + if (width) { + resizeParams += `width=${width},`; + } + if (quality) { + resizeParams += `quality=${quality},`; + } + resizeParams = resizeParams.slice(0, -1); + } + if (resizeParams) { + return `${basePath}/${resizeParams}${path}`; + } else { + return `${basePath}${path}`; + } + } +} + +export { BucketConfig }; diff --git a/app/api/GetApiHost.ts b/app/api/GetApiHost.ts new file mode 100644 index 0000000000..e2a7547b83 --- /dev/null +++ b/app/api/GetApiHost.ts @@ -0,0 +1,34 @@ +// interface ApiDetails { hostname: string, useSsl: boolean, } + +const domainWithoutPort = document.location.host.split(":")[0]; + +const hostConfig = (host: string, useSsl = true) => ({ + formatUrl: (endpoint = "") => `${ useSsl ? "https" : "http" }://${ host + endpoint }`, + host, + useSsl, +}); + +export default function GetApiHost() { + switch (domainWithoutPort) { + case "fakeyou.com": + case "staging.fakeyou.com": + return hostConfig("api.fakeyou.com"); + case "storyteller.ai": + case "staging.storyteller.ai": + //return hostConfig("api.storyteller.ai"); + return hostConfig("api.fakeyou.com"); + case "storyteller.stream": // Storyteller.stream is deprecated and will be decommissioned in the future. + return hostConfig("api.storyteller.stream"); + case "devproxy.fakeyou.com": + return hostConfig("api.fakeyou.com"); + case "devproxy.storyteller.ai": + //return hostConfig("api.storyteller.ai"); + return hostConfig("api.fakeyou.com"); + case "dev.fakeyou.com": + return hostConfig("api.dev.fakeyou.com:12345",false); // false disables SSL + default: + return document.location.host.includes("localhost") ? + hostConfig("localhost:12345",document.location.protocol === "https:") : + hostConfig("api.fakeyou.com"); // default + } +}; \ No newline at end of file diff --git a/app/api/MakeRequest.ts b/app/api/MakeRequest.ts new file mode 100644 index 0000000000..b287051b39 --- /dev/null +++ b/app/api/MakeRequest.ts @@ -0,0 +1,59 @@ +import GetApiHost from "./GetApiHost"; + +// const { formatUrl } = GetApiHost(); + +type UrlRoutingFunction = (urlRouteArgs: UrlRouteArgs) => string; + +interface RequestHeaders { + [name: string]: string; +} + +interface RouteSetup { + method: string; + multipart?: boolean; + routingFunction: UrlRoutingFunction; +} + +const METHOD_OMITS_BODY: { [key: string]: boolean } = { + DELETE: false, + GET: true, + OPTIONS: true, + PATCH: false, + POST: false, + PUT: false, +}; + +const MakeRequest = (routeSetup: RouteSetup) : (urlRouteArgs: UrlRouteArgs, request: Request, queries?: UrlParams) => Promise => { + return async function(urlRouteArgs: UrlRouteArgs, request: Request, queries?: any) : Promise { + const newQueries = queries ? Object.keys(queries).map((key, i) => { + return `${ i ? "&" : "" }${ key }=${ Array.isArray(queries[key]) ? queries[key].join(`&${ key }=`) : queries[key] }` + }).join("") : null; + + const endpoint = `${ routeSetup.routingFunction(urlRouteArgs) }${ newQueries ? "?" + newQueries : "" }`; + const method = routeSetup.method; + const methodOmitsBody = METHOD_OMITS_BODY[method] || false; + + // return fetch(formatUrl(endpoint), { + return fetch(`https://api.fakeyou.com${ endpoint }`, { + method, + headers: { + "Accept": "application/json", + ...methodOmitsBody? {} : { "Content-Type": "application/json" } + }, + credentials: 'include', + ...methodOmitsBody ? {} : { body: JSON.stringify(request) }, + }) + .then(res => res.json()) + .then(res => { + if (!res.success && res && (res.voice_token || res.dataset_token)) { + return res; + } + else if (res && "success" in res) { + return res; + } else Promise.reject(); + }) + .catch(e => ({ success : false })); + } +}; + +export default MakeRequest; \ No newline at end of file diff --git a/app/api/media_files/GetMediaByUser.ts b/app/api/media_files/GetMediaByUser.ts new file mode 100644 index 0000000000..4ea5c95f6d --- /dev/null +++ b/app/api/media_files/GetMediaByUser.ts @@ -0,0 +1,26 @@ +import MakeRequest from "../MakeRequest"; +import { MediaFile } from "./GetMedia"; +import { Pagination } from "../_common/SharedFetchTypes"; + +export interface GetMediaRequest {} + +export interface GetMediaListResponse { + pagination: Pagination; + success: boolean; + results: MediaFile[]; +} + +export interface GetMediaParams { + page_index: number; +} + +export const GetMediaByUser = MakeRequest< + string, + GetMediaRequest, + GetMediaListResponse, + GetMediaParams +>({ + method: "GET", + routingFunction: (userToken: string) => + `/v1/media_files/list/user/${userToken}`, +}); \ No newline at end of file diff --git a/app/components/Button.tsx b/app/components/Button.tsx index 75bd8c00aa..0aa6b15580 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -16,12 +16,8 @@ export const Button = ({ disabled, ...rest }: ButtonPropsI) => { - const variant = disabled ? "disabled" : propsVariant; function getVariantClassNames(variant: string) { switch (variant) { - case "disabled": { - return "bg-brand-secondary-500 text-brand-secondary-300"; - } case "secondary": { return " bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary"; } @@ -30,20 +26,25 @@ export const Button = ({ } case "primary": default: { - return " bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600"; + return "bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600"; } } } + const disabledClass = twMerge( + disabled ? "opacity-40 pointer-events-none" : "", + ); + const className = twMerge( "text-sm font-medium rounded-lg px-3 py-2 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150 flex gap-2 items-center justify-center", - getVariantClassNames(variant), + getVariantClassNames(propsVariant), propsClassName, + disabledClass, ); return ( ); diff --git a/app/components/InputVector.tsx b/app/components/InputVector.tsx index 9fc8464d44..d25b4c0d37 100644 --- a/app/components/InputVector.tsx +++ b/app/components/InputVector.tsx @@ -1,24 +1,35 @@ import { useRef } from "react"; import { twMerge } from "tailwind-merge"; -import { disableHotkeyInput,enableHotkeyInput,DomLevels } from "~/pages/PageEnigma/store"; +import { + disableHotkeyInput, + enableHotkeyInput, + DomLevels, +} from "~/pages/PageEnigma/store"; interface InputVectorProps { x: number; y: number; z: number; onChange: (v: { x: number; y: number; z: number }) => void; + increment?: number; } -export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => { +export const InputVector = ({ + x, + y, + z, + onChange, + increment = 0.1, +}: InputVectorProps) => { const xRef = useRef(null); const yRef = useRef(null); const zRef = useRef(null); const inputCommonClasses = - "relative w-14 h-7 rounded-r-lg bg-brand-secondary p-3 text-sm text-white transition-all duration-150 ease-in-out outline-none -outline-offset-2"; + "relative h-6 rounded-r-lg bg-brand-secondary p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-brand-secondary-900"; const wrapperCommonClasses = - "flex items-center before:inline-block before:w-10 before:h-10 before:bg-brand-primary before:text-white before:rounded-l-lg before:h-7 before:w-5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs"; + "relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs"; function handleOnChange() { const newVector = { @@ -28,14 +39,67 @@ export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => { }; onChange(newVector); } + + const blurAllInputs = () => { + xRef.current?.blur(); + yRef.current?.blur(); + zRef.current?.blur(); + }; + + // For dragging the input value to increment/decrement + const handleMouseDown = ( + e: React.MouseEvent, + ref: React.RefObject, + ) => { + e.preventDefault(); + blurAllInputs(); + let isDragging = false; + let previousMousePosition = e.clientX; + + const mouseMoveHandler = (e: MouseEvent) => { + isDragging = true; + const currentMousePosition = e.clientX; + const direction = currentMousePosition > previousMousePosition ? 1 : -1; + previousMousePosition = currentMousePosition; + + const currentValue = Number(ref.current?.value) || 0; + const newValue = currentValue + direction * increment; + + if (ref.current) { + ref.current.value = newValue.toFixed(1); + ref.current.blur(); + } + + onChange({ + x: Number(xRef.current?.value) || 0, + y: Number(yRef.current?.value) || 0, + z: Number(zRef.current?.value) || 0, + }); + }; + + const mouseUpHandler = () => { + document.removeEventListener("mousemove", mouseMoveHandler); + document.removeEventListener("mouseup", mouseUpHandler); + if (!isDragging) { + ref.current?.focus(); + ref.current?.select(); + } + }; + + document.addEventListener("mousemove", mouseMoveHandler); + document.addEventListener("mouseup", mouseUpHandler); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + (e.currentTarget as HTMLInputElement).blur(); // Unfocus the input field on pressing Enter + } + }; + return ( -
- +
+ +
X
{ value={x} onFocus={() => disableHotkeyInput(DomLevels.INPUT)} onBlur={() => enableHotkeyInput(DomLevels.INPUT)} + onMouseDown={(e) => handleMouseDown(e, xRef)} + onKeyDown={handleKeyDown} />
- + +
Y
{ value={y} onFocus={() => disableHotkeyInput(DomLevels.INPUT)} onBlur={() => enableHotkeyInput(DomLevels.INPUT)} + onMouseDown={(e) => handleMouseDown(e, yRef)} + onKeyDown={handleKeyDown} />
- + +
Z
{ value={z} onFocus={() => disableHotkeyInput(DomLevels.INPUT)} onBlur={() => enableHotkeyInput(DomLevels.INPUT)} + onMouseDown={(e) => handleMouseDown(e, zRef)} + onKeyDown={handleKeyDown} />
diff --git a/app/components/Pill.tsx b/app/components/Pill.tsx new file mode 100644 index 0000000000..66d5eefc6f --- /dev/null +++ b/app/components/Pill.tsx @@ -0,0 +1,18 @@ +import { twMerge } from "tailwind-merge" + +export const Pill = ({ + className, + children, +}:{ + className?: string, + children : React.ReactNode +})=>{ + return ( +
+ {children} +
+ ) +} \ No newline at end of file diff --git a/app/components/TransitionDialogue.tsx b/app/components/TransitionDialogue.tsx index b6101ba65f..122a295546 100644 --- a/app/components/TransitionDialogue.tsx +++ b/app/components/TransitionDialogue.tsx @@ -2,17 +2,21 @@ import { useEffect } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { Fragment, ReactNode } from "react"; import { twMerge } from "tailwind-merge"; -import { disableHotkeyInput, enableHotkeyInput, DomLevels } from "~/pages/PageEnigma/store"; +import { + disableHotkeyInput, + enableHotkeyInput, + DomLevels, +} from "~/pages/PageEnigma/store"; -const DialogBackdrop = ()=>{ - useEffect(()=>{ +const DialogBackdrop = () => { + useEffect(() => { disableHotkeyInput(DomLevels.DIALOGUE); - return()=>{ + return () => { enableHotkeyInput(DomLevels.DIALOGUE); - } + }; }, []); - return( + return ( { enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" - leaveTo="opacity-0" - > + leaveTo="opacity-0">
); }; export const TransitionDialogue = ({ - isOpen, title, onClose, className, children, -}:{ + isOpen, + title, + onClose, + className, + children, +}: { isOpen: boolean; title?: ReactNode; - onClose:()=>void; + onClose: () => void; className?: string; children: ReactNode; - -})=>{ - return( +}) => { + return ( - - -
-
- - - {title && ( - - {title} - - )} - {children} - - -
+ + +
+
+ + + {title && ( + + {title} + + )} + {children} + +
-
- +
+
+
); -}; \ No newline at end of file +}; diff --git a/app/components/index.tsx b/app/components/index.tsx index 28dfc10661..2133a89322 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -12,6 +12,7 @@ import { ListDropdown } from "./ListDropdown" import { ListSearchDropdown } from "./ListSearchDropdown" import { LoadingBar } from "./LoadingBar"; import { LoadingDots, LoadingDotsBricks, LoadingDotsTyping } from "./LoadingDots"; +import { Pill } from "./Pill"; export { Button, @@ -30,4 +31,5 @@ export { LoadingDots, LoadingDotsBricks, LoadingDotsTyping, + Pill }; diff --git a/app/modules/ButtonDialogue/ButtonDialogue.tsx b/app/modules/ButtonDialogue/ButtonDialogue.tsx index e83bf0c149..f6aba3a01a 100644 --- a/app/modules/ButtonDialogue/ButtonDialogue.tsx +++ b/app/modules/ButtonDialogue/ButtonDialogue.tsx @@ -8,11 +8,12 @@ interface ButtonDialoguePropsI { buttonProps?: UnionedButtonProps; confirmButtonProps?: UnionedButtonProps; closeButtonProps?: UnionedButtonProps; - dialogProps?:{ + dialogProps?: { className?: string; }; title?: string; children: React.ReactNode; + showClose?: boolean; } export const ButtonDialogue = ({ @@ -22,6 +23,7 @@ export const ButtonDialogue = ({ closeButtonProps: unionedCloseButtonProps, title, children, + showClose = true, }: ButtonDialoguePropsI) => { const [isOpen, setIsOpen] = useState(false); @@ -43,19 +45,20 @@ export const ButtonDialogue = ({ title={title} isOpen={isOpen} onClose={closeModal} - className={dialogProps.className} - > -
{children}
+ className={dialogProps.className}> +
{children}
- + {showClose && ( + + )} + {confirmButtonProps && ( )}
- + ); }; diff --git a/app/pages/PageComponentLibrary/SectionComponents.tsx b/app/pages/PageComponentLibrary/SectionComponents.tsx new file mode 100644 index 0000000000..79dc8e8f39 --- /dev/null +++ b/app/pages/PageComponentLibrary/SectionComponents.tsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import { + H1, H4, H6, P, + Button, + ButtonLink, + ListDropdown, + ListSearchDropdown, + // LoadingBar, + LoadingDotsTyping, + LoadingDotsBricks, +} from "~/components"; + + +export function SectionComponents () { + + const [valueListDropdown, setValueListDropdown] = useState(undefined); + const [valueListSearchDropdown, setValueListSearchDropdown] = useState(undefined); + + const testdata = [ + { name: 'Wade Cooper' }, + { name: 'Arlene Mccoy' }, + { name: 'Devon Webb' }, + { name: 'Tom Cook' }, + { name: 'Tanya Fox' }, + { name: 'Hellen Schmidt' }, + ] + + // Progress Bar Usage States, Variables and Functions + // const [progress, setProgress] = useState(0); + // const loopRef = useRef(null); + // const pushProgressBar: ()=>void = useCallback(()=>{ + // loopRef.current = setInterval(function timer(){ + // setProgress((curr)=>{ + // if (curr < 100){ + // return curr+10; + // }else{ + // if(loopRef.current) + // clearInterval(loopRef.current); + // return curr; + // } + // })} + // , 3000); + // }, []); + // useEffect(()=>{ + // pushProgressBar(); + // return ()=>{ + // if(loopRef.current) + // clearInterval(loopRef.current) + // } + // },[progress]); + + return( +
+

Components

+

Buttons

+
+ + + + +
+ +
+

ButtonLink

+ Back to / +
+ +
+

List Dropdowns

+ setValueListDropdown(val)} + /> +

Selected Item: {valueListDropdown}

+ setValueListSearchDropdown(val)} + /> +

Selected Item: {valueListSearchDropdown}

+
+ +
+

Loading Dots

+
+
+ +
+
+ +
+
+
+ +
+

Loading Bar

+
Loading Bar is currently Broken and not reusable
+ {/*
+ + +
+ + + */} +
+
+ ); +}; \ No newline at end of file diff --git a/app/pages/PageComponentLibrary/SectionTypography.tsx b/app/pages/PageComponentLibrary/SectionTypography.tsx new file mode 100644 index 0000000000..a7500e0d03 --- /dev/null +++ b/app/pages/PageComponentLibrary/SectionTypography.tsx @@ -0,0 +1,21 @@ +import { + H1,H2,H3,H4,H5,H6,P,Label, +} from "~/components"; + + +export const SectionTypography = () => { + + return( +
+

Typography

+

H1. This is an H1 Heading

+

H2. This is an H2 Heading

+

H3. This is an H3 Heading

+

H4. This is an H4 Heading

+ +

P. Is an normal Paragraph

+
H5. This is an H5 Heading
+
H6. This is an H6 Heading
+
+ ); +}; \ No newline at end of file diff --git a/app/pages/PageComponentLibrary/index.tsx b/app/pages/PageComponentLibrary/index.tsx new file mode 100644 index 0000000000..9ef6f2cc59 --- /dev/null +++ b/app/pages/PageComponentLibrary/index.tsx @@ -0,0 +1,19 @@ +import { SectionTypography } from "./SectionTypography"; +import { SectionComponents } from "./SectionComponents"; + +export const PageComponentLibrary = () => { + return ( +
+
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/pages/PageCube/PageCube.tsx b/app/pages/PageCube/PageCube.tsx deleted file mode 100644 index f6d2062e5a..0000000000 --- a/app/pages/PageCube/PageCube.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons"; - -import { Button } from "~/components/Button"; -import { ButtonLink } from "~/components/ButtonLink"; -import { TopBarHelmet } from "~/modules/TopBarHelmet"; - -import { SidePanel } from "~/modules/SidePanel"; -import { SceneWithCube } from "./SceneWithCube"; - -export const PageCube = () => { - const mountRef = useRef(null); - const sceneRef = useRef(null); - const [{ x: rotationX, y: rotationY }, setRotations] = useState<{ - x: number | undefined; - y: number | undefined; - }>({ x: undefined, y: undefined }); - - const toggleStopped = () => { - if (sceneRef.current !== null) { - sceneRef.current.stopped = !sceneRef.current.stopped; - } - }; - - const listeningXYRotation = useCallback( - ({ x, y }: { x: number; y: number }) => { - setRotations({ - x: Math.round(x * 100) / 100, - y: Math.round(y * 100) / 100, - }); - }, - [], - ); - - useEffect(() => { - //componentDidMount - sceneRef.current = new SceneWithCube(listeningXYRotation); - mountRef.current?.appendChild(sceneRef.current.renderer.domElement); - - sceneRef.current.renderer.setAnimationLoop(() => { - sceneRef.current?.update(); - }); - - // Clean up - return () => { - if (sceneRef.current) - mountRef.current?.removeChild(sceneRef.current.renderer.domElement); - }; - }, []); - - return ( -
- - - Back to Dashboard - - -
-
-
-
- -

Ration X: {rotationX ? rotationX : ""}

-

Ration Y: {rotationY ? rotationY : ""}

-
- -

Side Panel Context

-
-
- ); -}; diff --git a/app/pages/PageCube/SceneWithCube.tsx b/app/pages/PageCube/SceneWithCube.tsx deleted file mode 100644 index 853b04dbb8..0000000000 --- a/app/pages/PageCube/SceneWithCube.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as THREE from 'three'; - -export class SceneWithCube { - scene: THREE.Scene; - camera: THREE.PerspectiveCamera; - renderer: THREE.WebGLRenderer; - cube: THREE.Mesh; - stopped: boolean; - callback: ({x,y}:{x:number;y:number})=>void; - - update(){ - if(!this.stopped){ - this.cube.rotation.x += 0.01; - this.cube.rotation.y += 0.01; - if(this.callback && typeof this.callback === "function") - this.callback({ - x: this.cube.rotation.x, - y: this.cube.rotation.y - }) - } - this.renderer.render(this.scene, this.camera); - } - - constructor(callback: ({x,y}:{x:number;y:number})=>void) { - const width = 1000; - const height = 750; - - this.callback = callback; - this.stopped = false; - - this.scene = new THREE.Scene(); - this.camera = new THREE.PerspectiveCamera(75, width/height, 0.1, 1000); - this.renderer = new THREE.WebGLRenderer(); - this.renderer.setSize(width, height); - - const geometry = new THREE.BoxGeometry(); - const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); - this.cube = new THREE.Mesh(geometry, material); - this.scene.add(this.cube); - - this.camera.position.z = 5; - } - -} - diff --git a/app/pages/PageCube/index.tsx b/app/pages/PageCube/index.tsx deleted file mode 100644 index 1f4e7f3f76..0000000000 --- a/app/pages/PageCube/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ - -export { PageCube } from "./PageCube"; \ No newline at end of file diff --git a/app/pages/PageEnigma/PageEnigma.tsx b/app/pages/PageEnigma/PageEnigma.tsx index e97e66d041..5ea572f76f 100644 --- a/app/pages/PageEnigma/PageEnigma.tsx +++ b/app/pages/PageEnigma/PageEnigma.tsx @@ -20,4 +20,4 @@ export const PageEnigma = () => { /> ); -}; +}; \ No newline at end of file diff --git a/app/pages/PageEnigma/Queue/toEngineActions.ts b/app/pages/PageEnigma/Queue/toEngineActions.ts index 1992d3a51c..871c5f7cf1 100644 --- a/app/pages/PageEnigma/Queue/toEngineActions.ts +++ b/app/pages/PageEnigma/Queue/toEngineActions.ts @@ -7,8 +7,6 @@ export enum toEngineActions { ADD_OBJECT = "add_object", - ADD_SHAPE = "add_shape", - // data= QueueKeyframe DELETE_CLIP = "delete_clip", // data=QueueClip diff --git a/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx b/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx index 47d7a976d0..05a6064983 100644 --- a/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx +++ b/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx @@ -1,7 +1,8 @@ -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useState } from "react"; import { Transition } from "@headlessui/react"; import { - faArrowRightArrowLeft, + faChevronDown, + faChevronUp, faCube, faTrash, } from "@fortawesome/pro-solid-svg-icons"; @@ -19,9 +20,30 @@ import Queue from "~/pages/PageEnigma/Queue/Queue"; import { toTimelineActions } from "../../Queue/toTimelineActions"; import { QueueKeyframe } from "~/pages/PageEnigma/models"; +// TODO this will be useful later to fix the bug on leading zeros +const formatNumber = (input: string): number => { + // Convert the input string to a number to remove leading zeros + const num = parseFloat(input); + // Convert the number back to a string with at least two decimal places + const str = num.toFixed(2); + return parseFloat(str); +} + +// console.log(formatNumber("000123.4567")); // Outputs: "123.46" +// console.log(formatNumber("000123.4")); // Outputs: "123.40" +// console.log(formatNumber("000123")); // Outputs: "123.00" +// console.log(formatNumber("0000.00")); // Outputs: "0.00" +// console.log(formatNumber("0000.000001")); // Outputs: "0.00" + export const ControlPanelSceneObject = () => { const editorEngine = useContext(EngineContext); const [appUiState, dispatchAppUiState] = useContext(AppUiContext); + const [isCollapsed, setIsCollapsed] = useState(false); + + const toggleCollapse = () => { + setIsCollapsed(!isCollapsed); + }; + const position = appUiState.controlPanel.currentSceneObject?.objectVectors?.position; @@ -31,6 +53,7 @@ export const ControlPanelSceneObject = () => { appUiState.controlPanel.currentSceneObject?.objectVectors?.scale; const currentSceneObject = appUiState.controlPanel.currentSceneObject; + useEffect(() => { // TODO this causes a subtle bug because it renders way too many times. if (!appUiState.controlPanel.currentSceneObject) { @@ -169,63 +192,73 @@ export const ControlPanelSceneObject = () => { show={appUiState?.controlPanel.isShowing} className={[ "absolute bottom-0 right-0", - "m-4 flex h-fit w-fit flex-col gap-2", + "m-4 flex h-fit w-56 flex-col gap-2", "rounded-lg", "border border-ui-panel-border", - "bg-ui-panel p-4 text-white", + "bg-ui-panel p-3.5 text-white", ].join(" ")} - enter="transition-opacity duration-100" + enter="transition-opacity duration-150" enterFrom="opacity-0" enterTo="opacity-100" - leave="transition-opacity duration-100" + leave="transition-opacity duration-150" leaveFrom="opacity-100" leaveTo="opacity-0"> -
+
-

+

{appUiState.controlPanel.currentSceneObject.object_name}

-
- -

Swap Object

+ +
+ + +
+
Location
+
-
- -
-
Position
- -
+
+
Rotation
+ +
-
-
Rotation
- -
+
+
Scale
+ +
+ -
-
Scale
- -
- - -
+
- +
); }; diff --git a/app/pages/PageEnigma/comps/ControlsTopButtons/Help/Help.tsx b/app/pages/PageEnigma/comps/ControlsTopButtons/Help/Help.tsx new file mode 100644 index 0000000000..c35bc37854 --- /dev/null +++ b/app/pages/PageEnigma/comps/ControlsTopButtons/Help/Help.tsx @@ -0,0 +1,144 @@ +import { Label } from "~/components"; + +const ShortcutsGroup = (props: { + label: string; + children?: React.ReactNode; +}) => ( +
+ +
+ {props.children} +
+
+
+); + +const Shortcut = (props: { label: string; children: React.ReactNode }) => ( +
+ + {props.label} + + {props.children} +
+); + +const Key = (props: { button: string }) => ( +
+ {props.button} +
+); + +const KeyGroup = (props: { children: React.ReactNode }) => ( +
{props.children}
+); + +const Mouse = (props: { + button: "left" | "middle" | "right"; + label: string; +}) => ( +
+ {`${props.button} + {props.label} +
+); + +const Plus = () =>
+
; + +export const Help = () => { + return ( +
+
+ + + + + + + + + + + {/* + + + + + + + + + */} + + + + + + + {/* + + */} + {/* + + */} + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + {/* + + + + */} + + + + + + + + + + +
+
+ ); +}; diff --git a/app/pages/PageEnigma/comps/ControlsTopButtons/Help/index.tsx b/app/pages/PageEnigma/comps/ControlsTopButtons/Help/index.tsx new file mode 100644 index 0000000000..c6b98f7c78 --- /dev/null +++ b/app/pages/PageEnigma/comps/ControlsTopButtons/Help/index.tsx @@ -0,0 +1 @@ +export { Help } from "./Help"; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/AudioTabComponent.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/AudioTabComponent.tsx index d6d5ebdacb..8c0e7686fb 100644 --- a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/AudioTabComponent.tsx +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/AudioTabComponent.tsx @@ -1,53 +1,40 @@ import { useCallback, useContext, useEffect, useState } from "react"; -import { twMerge } from "tailwind-merge"; import { useSignals, useSignalEffect } from "@preact/signals-react/runtime"; -import { faCirclePlus, faArrowsRotate } from "@fortawesome/pro-solid-svg-icons"; -import { AppUiContext } from "~/contexts/AppUiContext"; import { AuthenticationContext } from "~/contexts/Authentication"; -import { APPUI_ACTION_TYPES } from "~/reducers"; -import { audioFilter, audioItems } from "~/pages/PageEnigma/store"; -import { - AssetFilterOption, - MediaItem, - AssetType, -} from "~/pages/PageEnigma/models"; - -import { Button, ButtonIcon } from "~/components"; - -import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements"; +import { MediaItem, AssetType } from "~/pages/PageEnigma/models"; import { audioItemsFromServer } from "~/pages/PageEnigma/store/mediaFromServer"; -import { ListAudioByUser } from "./listAudioByUser"; +import { ListAudioByUser } from "./utilities"; import { inferenceJobs } from "~/pages/PageEnigma/store/inferenceJobs"; import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager"; import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle"; +import { PageLibrary } from "./pageLibrary"; +import { PageTTS } from "./pageTTS"; +import { AudioTabPages } from "./types"; + export const AudioTabComponent = () => { useSignals(); const [state, setState] = useState({ firstLoad: false, fetchingUserAudio: false, + page: AudioTabPages.LIBRARY, }); const { authState } = useContext(AuthenticationContext); - const [, dispatchAppUiState] = useContext(AppUiContext); - const handleOpenTtsDialogue = () => { - dispatchAppUiState({ - type: APPUI_ACTION_TYPES.OPEN_DIALOGUE_TTS, - }); - }; - const allAudioItems = [...audioItems.value, ...audioItemsFromServer.value]; - const handleListAudioByUser = useCallback((username: string) => { - setState((curr) => ({ ...curr, fetchingUserAudio: true })); - ListAudioByUser(username).then((res: any[]) => { - setState((curr) => ({ ...curr, fetchingUserAudio: false })); - audioItemsFromServer.value = res.map((item) => { - const morphedItem: MediaItem = { + const handleListAudioByUser = useCallback((username:string, sessionToken:string)=>{ + setState((curr)=>({...curr, fetchingUserAudio:true})); + ListAudioByUser(username, sessionToken).then((res:any[])=>{ + setState((curr)=>({...curr, fetchingUserAudio:false})); + audioItemsFromServer.value = res.map(item=>{ + const morphedItem:MediaItem = { version: 1, type: AssetType.AUDIO, media_id: item.token, object_uuid: item.token, name: item.maybe_title || item.origin.maybe_model.title, + description: item.maybe_text_transcript, + publicBucketPath: item.public_bucket_path, length: 25, thumbnail: "/resources/placeholders/audio_placeholder.png", isMine: true, @@ -58,14 +45,11 @@ export const AudioTabComponent = () => { }); }, []); - useEffect(() => { - if (authState.userInfo) { - if ( - state.firstLoad === false && - audioItemsFromServer.value.length === 0 - ) { - handleListAudioByUser(authState.userInfo.username); - setState((curr) => ({ ...curr, firstLoad: true })); + useEffect(()=>{ + if (authState.userInfo && authState.sessionToken){ + if(state.firstLoad === false && audioItemsFromServer.value.length === 0){ + handleListAudioByUser(authState.userInfo.username, authState.sessionToken); + setState((curr)=>({...curr, firstLoad:true})); } } }, [authState, state, handleListAudioByUser]); @@ -89,71 +73,30 @@ export const AudioTabComponent = () => { return foundItemOfJob !== undefined; } }); - if (found === undefined) { - handleListAudioByUser(authState.userInfo.username); + if(found === undefined && authState.sessionToken){ + handleListAudioByUser(authState.userInfo.username, authState.sessionToken); } } }); - return ( - <> -
- -
- - - - { - if (authState.userInfo?.username) { - handleListAudioByUser(authState.userInfo?.username); - } - }} - /> -
-
-
- -
-
- -
- - ); + const changePage = (newPage:AudioTabPages)=>{ + setState((curr)=>({ + ...curr, + page: newPage + })) + } + if(state.page === AudioTabPages.LIBRARY){ + return + }else if(state.page === AudioTabPages.TTS && authState.sessionToken){ + return( + + ); + }else{ + return( +

Unknown Error

+ ) + } }; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/WaveformPlayer.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/WaveformPlayer.tsx new file mode 100644 index 0000000000..b4b40d7afe --- /dev/null +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/WaveformPlayer.tsx @@ -0,0 +1,55 @@ +import { useCallback, useEffect, useState, useRef } from "react"; +import WaveSurfer from "wavesurfer.js"; +import { faCirclePlay, faCirclePause } from "@fortawesome/pro-solid-svg-icons"; +import { ButtonIcon } from "~/components"; + +export const WaveformPlayer = ({ audio }: { audio: string }) => { + const waveSurferRef = useRef(undefined); + const [isPlaying, toggleIsPlaying] = useState(false); + + const containerRef = useCallback((node: HTMLDivElement) => { + if (node) { + const waveSurfer = WaveSurfer.create({ + container: node, + barWidth: 2, + height: 24, + cursorWidth: 0, + waveColor: "#D7C8C8", + progressColor: "#FB8381", + }); + waveSurfer.load(audio); + waveSurfer.on("ready", () => { + waveSurferRef.current = waveSurfer; + }); + waveSurfer.on("play", () => { + toggleIsPlaying(true); + }); + waveSurfer.on("pause", () => { + toggleIsPlaying(false); + }); + waveSurfer.on("finish", () => { + waveSurfer.seekTo(0); + }); + } + }, []); + + useEffect(() => { + return () => { + console.log("OK?"); + waveSurferRef.current?.destroy(); + }; + }, []); + + return ( +
+ { + waveSurferRef.current?.playPause(); + }} + /> +
+
+ ); +}; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/audioItemElement.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/audioItemElement.tsx new file mode 100644 index 0000000000..eaad85487b --- /dev/null +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/audioItemElement.tsx @@ -0,0 +1,101 @@ +import { useCallback, useContext, useEffect } from "react"; +import { useSignals } from "@preact/signals-react/runtime"; + +import { WaveformPlayer } from "./WaveformPlayer"; +import { MediaItem } from "~/pages/PageEnigma/models"; +import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; +import { + canDrop, + currPosition, + dragItem, + initPosition, +} from "~/pages/PageEnigma/store"; + +import { H5, H6, Pill } from "~/components"; + +function getGcsUrl(bucketRelativePath: string | undefined | null): string { + let bucket = "vocodes-public"; + let path = bucketRelativePath; + if (path !== undefined && path !== null && !path.startsWith("/")) { + path = "/" + path; + } + return `https://storage.googleapis.com/${bucket}${path}`; +} + +interface Props { + item: MediaItem; +} + +export const AudioItemElement = ({ item }: Props) => { + useSignals(); + const { startDrag, endDrag } = useContext(TrackContext); + + const { initX, initY } = initPosition.value; + + useEffect(() => { + const onPointerUp = () => { + if (dragItem.value) { + endDrag(); + } + }; + + const onMouseMove = (event: MouseEvent) => { + if (dragItem.value) { + event.stopPropagation(); + event.preventDefault(); + const deltaX = event.pageX - initX; + const deltaY = event.pageY - initY; + currPosition.value = { currX: initX + deltaX, currY: initY + deltaY }; + } + }; + + window.addEventListener("pointerup", onPointerUp); + window.addEventListener("pointermove", onMouseMove); + + return () => { + window.removeEventListener("pointerup", onPointerUp); + window.removeEventListener("pointermove", onMouseMove); + }; + }, [startDrag, endDrag, initX, initY]); + + const onPointerDown = useCallback( + (event: React.PointerEvent) => { + if (event.button === 0) { + startDrag(item); + currPosition.value = { + currX: event.pageX, + currY: event.pageY, + }; + initPosition.value = { + initX: event.pageX, + initY: event.pageY, + }; + canDrop.value = false; + } + }, + [item, startDrag], + ); + + return ( +
+
+
+ Audio +
+ + {item.publicBucketPath && ( + + )} + +
{item.name}
+ {item.description && ( +
+ {item.description} +
+ )} +
+
+ ); +}; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/audioItemElements.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/audioItemElements.tsx new file mode 100644 index 0000000000..2dfeaa1144 --- /dev/null +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/audioItemElements.tsx @@ -0,0 +1,27 @@ +import { AudioItemElement } from "./audioItemElement"; +import { AssetFilterOption, MediaItem } from "~/pages/PageEnigma/models"; + +interface Props { + items: MediaItem[]; + assetFilter: AssetFilterOption; +} + +export const AudioItemElements = ({ items, assetFilter }: Props) => { + const displayItems = items.filter((item) => { + if (assetFilter === AssetFilterOption.ALL) { + return true; + } + if (assetFilter === AssetFilterOption.MINE) { + return item.isMine; + } + return item.isBookmarked; + }); + + return ( +
+ {displayItems.map((item) => ( + + ))} +
+ ); +}; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/listAudioByUser.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/listAudioByUser.tsx deleted file mode 100644 index 19e0d23c3e..0000000000 --- a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/listAudioByUser.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { listMediaByUser } from "~/api"; -import { - MediaFileType, - MediaFileClass, - MediaFileSubtype, - UserDetailsLight, - WeightCategory, - WeightType -} from "./types"; - - -export interface MediaFile { - token: string; - media_type: MediaFileType; - media_class: MediaFileClass | null; - maybe_media_subtype: MediaFileSubtype | null; - public_bucket_path: string; - maybe_engine_extension: string | null; - maybe_batch_token: string; - maybe_title: string | null; - maybe_original_filename: string | null; - maybe_creator_user: UserDetailsLight | null; - maybe_prompt_token: string | null; - creator_set_visibility: string; - created_at: Date; - updated_at: Date; - maybe_model_weight_info: { - title: string; - weight_token: string; - weight_category: WeightCategory; - weight_type: WeightType; - maybe_weight_creator: UserDetailsLight; - maybe_cover_image_public_bucket_path: string; - }; -} - -export const ListAudioByUser = async(username:string) => { - return await fetch(listMediaByUser(username),{ - method: 'GET', - headers: { - "Accept": "application/json", - }, - // credentials: 'include' - }) - .then(res => res.json()) - .then(res => { - if(res.success && res.results){ - return res.results.filter((item:MediaFile)=>item['media_type']==='audio'); - }else{ - Promise.reject(); - } - }) - .catch(e => ({ success : false })); -} - diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/pageLibrary.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/pageLibrary.tsx new file mode 100644 index 0000000000..43c897163c --- /dev/null +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/pageLibrary.tsx @@ -0,0 +1,75 @@ +import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons"; +import { twMerge } from "tailwind-merge"; +import { useSignals } from "@preact/signals-react/runtime"; +import { audioFilter, audioItems } from "~/pages/PageEnigma/store"; +import { AssetFilterOption } from "~/pages/PageEnigma/models"; +import { audioItemsFromServer } from "~/pages/PageEnigma/store/mediaFromServer"; + +import { Button } from "~/components"; +import { AudioItemElements } from "./audioItemElements"; +import { AudioTabPages } from "./types"; + +export const PageLibrary = ({ + changePage, +}: { + changePage: (newPage: AudioTabPages) => void; +}) => { + useSignals(); + const allAudioItems = [...audioItems.value, ...audioItemsFromServer.value]; + + return ( + <> +
+
+ + + +
+
+
+ +
+ +
+ +
+ + ); +}; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/pageTTS.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/pageTTS.tsx new file mode 100644 index 0000000000..b7e1455f80 --- /dev/null +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTabComponent/pageTTS.tsx @@ -0,0 +1,253 @@ +import { useCallback, useEffect, useState } from "react"; +import { useSignalEffect } from "@preact/signals-react/runtime"; +import { v4 as uuidv4 } from "uuid"; + +import { + faChevronLeft, + faBrainCircuit, + faVolumeHigh, +} from "@fortawesome/pro-solid-svg-icons"; + +import { + Button, + ButtonIcon, + H2, + Label, + LoadingDotsTyping, + ListSearchDropdown, + Textarea, +} from "~/components"; + +import { + TtsModelListItem, + GenerateTtsAudioResponse, +} from "~/pages/PageEnigma/models/tts"; + +import { addInferenceJob, inferenceJobs } from "../../../store/inferenceJobs"; +import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager"; +import { + ListTtsModels, + GenerateTtsAudio, + GetMediaFileByToken, +} from "./utilities"; +import { AudioTabPages } from "./types"; +import { AudioItemElement } from "./audioItemElement"; +import { MediaItem, AssetType } from "~/pages/PageEnigma/models"; + +type TtsState = { + voice: TtsModelListItem | undefined; + text: string; + hasEnqueued: boolean; + inferenceToken?: string; + inferenceJobType?: string; + hasTtsResult: boolean; +}; +const initialState: TtsState = { + voice: undefined, + text: "", + hasEnqueued: false, + inferenceJobType: undefined, + inferenceToken: undefined, + hasTtsResult: false, +}; + +export const PageTTS = ({ + changePage, + sessionToken, +}: { + changePage: (newPage: AudioTabPages) => void; + sessionToken: string; +}) => { + const [ttsState, setTtsState] = useState(initialState); + + const [ttsResultFile, setTtsResultFile] = useState(); + + useSignalEffect(() => { + console.log(inferenceJobs.value); + if (ttsState.hasEnqueued && ttsState.inferenceToken) { + const found = inferenceJobs.value.find( + (job) => job.job_id === ttsState.inferenceToken, + ); + console.log(`finding: ${ttsState.inferenceToken}`); + console.log(found); + if (found?.job_status === JobState.COMPLETE_SUCCESS) { + setTtsState((curr) => ({ + ...curr, + hasTtsResult: true, + })); + GetMediaFileByToken(found.result.entity_token, sessionToken).then( + (res) => { + console.log(res); + const morphedItem: MediaItem = { + version: 1, + type: AssetType.AUDIO, + media_id: res.media_file.token, + object_uuid: res.media_file.token, + name: ttsState.voice?.title || "", + description: ttsState.text, + publicBucketPath: res.media_file.public_bucket_path, + length: 25, + thumbnail: "/resources/placeholders/audio_placeholder.png", + isMine: true, + // isBookmarked?: boolean; + }; + setTtsResultFile(morphedItem); + }, + ); + } + } + }); + + const [ttsModels, setTtsModels] = useState>([]); + + const listModels = useCallback(async () => { + const ttsModelsLoaded = ttsModels.length > 0; + if (ttsModelsLoaded) { + return; // Already queried. + } + const models = await ListTtsModels(sessionToken); + if (models) { + setTtsModels(models); + } + }, []); + + useEffect(() => { + listModels(); + }, [listModels]); + + useEffect(() => { + if ( + ttsState.hasEnqueued && + ttsState.inferenceToken && + ttsState.inferenceJobType + ) { + console.log(`tts has Enqueued`); + addInferenceJob({ + version: 1, + job_id: ttsState.inferenceToken, + job_type: ttsState.inferenceJobType, + job_status: JobState.PENDING, + }); + } + }, [ttsState]); + + const requestTts = useCallback( + (sessionToken: string) => { + const modelToken = ttsState.voice + ? ttsState.voice.model_token + : undefined; + + if (modelToken) { + setTtsState((curr) => ({ + ...curr, + hasEnqueued: true, + inferenceToken: undefined, + inferenceJobType: undefined, + hasAudio: false, + result: undefined, + })); + + const request = { + uuid_idempotency_token: uuidv4(), + tts_model_token: modelToken, + inference_text: ttsState.text, + }; + + GenerateTtsAudio(request, sessionToken).then( + (res: GenerateTtsAudioResponse) => { + if (res && res.inference_job_token) { + setTtsState((curr) => ({ + ...curr, + inferenceToken: res.inference_job_token, + inferenceJobType: res.inference_job_token_type, + })); + } + }, + ); + } else { + console.log("no voice model selected"); + } + }, + [ttsState], + ); + + const handleTextInput = (e: React.ChangeEvent) => { + setTtsState((curr) => ({ + ...curr, + text: e.target.value, + })); + }; + + const handleOnSelect = (val: string) => { + console.log(val); + const voiceModel = ttsModels.find((item) => { + if (item.title === val) return item; + }); + setTtsState((curr) => ({ + ...curr, + voice: voiceModel, + })); + }; + + return ( +
+
+ changePage(AudioTabPages.LIBRARY)} + /> +

Generate TTS

+
+ + {ttsModels.length > 0 && ( + + )} +
+ +
+
+