From f7fd563dd0d218a82c8020d75b065db48b6256ec Mon Sep 17 00:00:00 2001 From: Albina Date: Wed, 25 Sep 2024 15:27:20 +0300 Subject: [PATCH 1/5] OV-403: + add voices page --- .../common/components/sidebar/sidebar.tsx | 18 +++ .../bundles/common/enums/app-route.enum.ts | 1 + .../common/icons/custom-icons/custom-icons.ts | 1 + .../common/icons/custom-icons/voice.tsx | 14 ++ .../src/bundles/common/icons/icon-name.ts | 3 +- frontend/src/bundles/home/store/actions.ts | 17 ++- frontend/src/bundles/home/store/slice.ts | 20 ++- frontend/src/bundles/home/types/types.ts | 2 + .../bundles/voices/components/components.ts | 3 + .../components/main-content/main-content.tsx | 44 +++++++ .../components/main-content/styles.module.css | 6 + .../components/voice-card/voice-card.tsx | 120 ++++++++++++++++++ .../voice-section/styles.module.css | 6 + .../voice-section/voice-section.tsx | 70 ++++++++++ .../bundles/voices/enums/voices-sections.ts | 6 + frontend/src/bundles/voices/pages/voices.tsx | 19 +++ frontend/src/routes/routes.tsx | 9 ++ 17 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 frontend/src/bundles/common/icons/custom-icons/voice.tsx create mode 100644 frontend/src/bundles/voices/components/components.ts create mode 100644 frontend/src/bundles/voices/components/main-content/main-content.tsx create mode 100644 frontend/src/bundles/voices/components/main-content/styles.module.css create mode 100644 frontend/src/bundles/voices/components/voice-card/voice-card.tsx create mode 100644 frontend/src/bundles/voices/components/voice-section/styles.module.css create mode 100644 frontend/src/bundles/voices/components/voice-section/voice-section.tsx create mode 100644 frontend/src/bundles/voices/enums/voices-sections.ts create mode 100644 frontend/src/bundles/voices/pages/voices.tsx diff --git a/frontend/src/bundles/common/components/sidebar/sidebar.tsx b/frontend/src/bundles/common/components/sidebar/sidebar.tsx index 5b16eceb9..57a09588f 100644 --- a/frontend/src/bundles/common/components/sidebar/sidebar.tsx +++ b/frontend/src/bundles/common/components/sidebar/sidebar.tsx @@ -6,6 +6,7 @@ import { IconButton, Link, Spacer, + Text, } from '~/bundles/common/components/components.js'; import { AppRoute } from '~/bundles/common/enums/enums.js'; import { @@ -105,6 +106,23 @@ const Sidebar: React.FC = ({ children }) => { label="My Avatar" /> + + Assets + + + + } + isCollapsed={isCollapsed} + label="AI Voices" + /> + + ), +}); + +export { Voice }; diff --git a/frontend/src/bundles/common/icons/icon-name.ts b/frontend/src/bundles/common/icons/icon-name.ts index 729172418..de222a192 100644 --- a/frontend/src/bundles/common/icons/icon-name.ts +++ b/frontend/src/bundles/common/icons/icon-name.ts @@ -12,7 +12,7 @@ import { WarningIcon, } from '@chakra-ui/icons'; -import { Logo, LogoText, OpenAi } from './custom-icons/custom-icons.js'; +import { Logo, LogoText, OpenAi, Voice } from './custom-icons/custom-icons.js'; import { BackwardStep, CircleUser, @@ -73,6 +73,7 @@ const IconName = { WARNING: WarningIcon, VIDEO_CAMERA: VideoCamera, IMAGE: Image, + VOICE: Voice, } as const; export { IconName }; diff --git a/frontend/src/bundles/home/store/actions.ts b/frontend/src/bundles/home/store/actions.ts index 56bd864c7..f31618ba2 100644 --- a/frontend/src/bundles/home/store/actions.ts +++ b/frontend/src/bundles/home/store/actions.ts @@ -1,7 +1,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; -import { type VideoGetAllResponseDto } from '~/bundles/home/types/types.js'; +import { + type GetVoicesResponseDto, + type VideoGetAllResponseDto, +} from '~/bundles/home/types/types.js'; import { name as sliceName } from './slice.js'; @@ -24,4 +27,14 @@ const deleteVideo = createAsyncThunk, string, AsyncThunkConfig>( }, ); -export { deleteVideo, loadUserVideos }; +const loadVoices = createAsyncThunk< + GetVoicesResponseDto, + undefined, + AsyncThunkConfig +>(`${sliceName}/load-voices`, (_, { extra }) => { + const { speechApi } = extra; + + return speechApi.loadVoices(); +}); + +export { deleteVideo, loadUserVideos, loadVoices }; diff --git a/frontend/src/bundles/home/store/slice.ts b/frontend/src/bundles/home/store/slice.ts index 3c185a756..a48129c29 100644 --- a/frontend/src/bundles/home/store/slice.ts +++ b/frontend/src/bundles/home/store/slice.ts @@ -2,18 +2,23 @@ import { createSlice } from '@reduxjs/toolkit'; import { DataStatus } from '~/bundles/common/enums/enums.js'; import { type ValueOf } from '~/bundles/common/types/types.js'; -import { type VideoGetAllItemResponseDto } from '~/bundles/home/types/types.js'; +import { + type VideoGetAllItemResponseDto, + type Voice, +} from '~/bundles/home/types/types.js'; -import { deleteVideo, loadUserVideos } from './actions.js'; +import { deleteVideo, loadUserVideos, loadVoices } from './actions.js'; type State = { dataStatus: ValueOf; videos: Array | []; + voices: Voice[]; }; const initialState: State = { dataStatus: DataStatus.IDLE, videos: [], + voices: [], }; const { reducer, actions, name } = createSlice({ @@ -44,6 +49,17 @@ const { reducer, actions, name } = createSlice({ builder.addCase(deleteVideo.rejected, (state) => { state.dataStatus = DataStatus.REJECTED; }); + builder.addCase(loadVoices.pending, (state) => { + state.dataStatus = DataStatus.PENDING; + }); + builder.addCase(loadVoices.fulfilled, (state, action) => { + state.voices = action.payload.items; + state.dataStatus = DataStatus.FULFILLED; + }); + builder.addCase(loadVoices.rejected, (state) => { + state.voices = []; + state.dataStatus = DataStatus.REJECTED; + }); }, }); diff --git a/frontend/src/bundles/home/types/types.ts b/frontend/src/bundles/home/types/types.ts index e79c0f579..766a60825 100644 --- a/frontend/src/bundles/home/types/types.ts +++ b/frontend/src/bundles/home/types/types.ts @@ -1,4 +1,6 @@ export { + type GetVoicesResponseDto, type VideoGetAllItemResponseDto, type VideoGetAllResponseDto, + type Voice, } from 'shared'; diff --git a/frontend/src/bundles/voices/components/components.ts b/frontend/src/bundles/voices/components/components.ts new file mode 100644 index 000000000..e4a123065 --- /dev/null +++ b/frontend/src/bundles/voices/components/components.ts @@ -0,0 +1,3 @@ +export { MainContent } from './main-content/main-content.js'; +export { VoiceCard } from './voice-card/voice-card.js'; +export { VoiceSection } from './voice-section/voice-section.js'; diff --git a/frontend/src/bundles/voices/components/main-content/main-content.tsx b/frontend/src/bundles/voices/components/main-content/main-content.tsx new file mode 100644 index 000000000..dd34215eb --- /dev/null +++ b/frontend/src/bundles/voices/components/main-content/main-content.tsx @@ -0,0 +1,44 @@ +import { + Box, + Loader, + Overlay, +} from '~/bundles/common/components/components.js'; +import { useCollapse } from '~/bundles/common/components/sidebar/hooks/use-collapse.hook.js'; +import { DataStatus } from '~/bundles/common/enums/enums.js'; +import { + useAppDispatch, + useAppSelector, + useEffect, +} from '~/bundles/common/hooks/hooks.js'; +import { loadVoices } from '~/bundles/home/store/actions.js'; +import { VoiceSection } from '~/bundles/voices/components/components.js'; +import { VoicesSections } from '~/bundles/voices/enums/voices-sections.js'; + +import styles from './styles.module.css'; + +const MainContent: React.FC = () => { + const dispatch = useAppDispatch(); + const { isCollapsed } = useCollapse(); + + const { voices, dataStatus } = useAppSelector(({ home }) => home); + + useEffect(() => { + void dispatch(loadVoices()); + }, [dispatch]); + + return ( + + + + + + + + + ); +}; + +export { MainContent }; diff --git a/frontend/src/bundles/voices/components/main-content/styles.module.css b/frontend/src/bundles/voices/components/main-content/styles.module.css new file mode 100644 index 000000000..c0d57c638 --- /dev/null +++ b/frontend/src/bundles/voices/components/main-content/styles.module.css @@ -0,0 +1,6 @@ +.main-content { + background-color: var(--chakra-colors-background-50); + border-radius: var(--chakra-radii-lg); + height: calc(100vh - 75px); + overflow: auto; +} diff --git a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx new file mode 100644 index 000000000..5374b0a97 --- /dev/null +++ b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx @@ -0,0 +1,120 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { + Card, + CardBody, + HStack, + Spinner, + Text, +} from '~/bundles/common/components/components.js'; +import { + useAppDispatch, + useAppSelector, + useCallback, + useMemo, + useState, +} from '~/bundles/common/hooks/hooks.js'; +import { IconName, IconSize } from '~/bundles/common/icons/icons.js'; +import { Control } from '~/bundles/studio/components/control/control.js'; +import { actions as studioActions } from '~/bundles/studio/store/studio.js'; +import { type Voice } from '~/bundles/studio/types/types.js'; + +type Properties = { + voice: Voice; +}; + +const VoiceCard: React.FC = ({ voice }) => { + const [url, setUrl] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const dispatch = useAppDispatch(); + + const text = 'Sample text'; + + const { isPlaying: playerIsPlaying, url: playerUrl } = useAppSelector( + ({ studio }) => studio.scriptPlayer, + ); + + const isPlaying = useMemo( + () => playerIsPlaying && playerUrl === url, + [playerIsPlaying, playerUrl, url], + ); + const handlePlayClick = useCallback( + (event: React.MouseEvent): void => { + event.stopPropagation(); + if (isLoading) { + return; + } + if (url) { + dispatch( + studioActions.playScript({ isPlaying: !isPlaying, url }), + ); + return; + } + setIsLoading(true); + void dispatch( + studioActions.generateScriptSpeechPreview({ + scriptId: uuidv4(), + text, + voiceName: voice.shortName, + }), + ) + .unwrap() + .then(({ audioUrl }) => { + setUrl(audioUrl); + setIsLoading(false); + dispatch( + studioActions.playScript({ + isPlaying: true, + url: audioUrl, + }), + ); + }); + }, + [ + dispatch, + text, + url, + voice, + isPlaying, + setUrl, + setIsLoading, + isLoading, + ], + ); + // const handleCardClick = useCallback((): void => { + // onClick(voice, url); + // }, [onClick, voice, url]); + + const iconComponent = useMemo(() => { + if (isLoading) { + return Spinner; + } + if (!url) { + return IconName.DOWNLOAD; + } + return isPlaying ? IconName.STOP : IconName.PLAY; + }, [isPlaying, isLoading, url]); + + return ( + + + + + + {voice.name} + + + + + ); +}; + +export { VoiceCard }; diff --git a/frontend/src/bundles/voices/components/voice-section/styles.module.css b/frontend/src/bundles/voices/components/voice-section/styles.module.css new file mode 100644 index 000000000..64d222997 --- /dev/null +++ b/frontend/src/bundles/voices/components/voice-section/styles.module.css @@ -0,0 +1,6 @@ +.horizontal { + display: flex; + overflow-x: auto; + scrollbar-width: thin; + scrollbar-color: #c1c1c1 #f1f1f1; +} diff --git a/frontend/src/bundles/voices/components/voice-section/voice-section.tsx b/frontend/src/bundles/voices/components/voice-section/voice-section.tsx new file mode 100644 index 000000000..76d67b5dc --- /dev/null +++ b/frontend/src/bundles/voices/components/voice-section/voice-section.tsx @@ -0,0 +1,70 @@ +import { + Badge, + Box, + Flex, + Heading, + SimpleGrid, + Text, +} from '~/bundles/common/components/components.js'; +import { type Voice } from '~/bundles/home/types/types.js'; +import { VoiceCard } from '~/bundles/voices/components/components.js'; +import { VoicesSections } from '~/bundles/voices/enums/voices-sections.js'; + +import styles from './styles.module.css'; + +type Properties = { + voices: Voice[]; + title: string; +}; + +const VoiceSection: React.FC = ({ voices, title }) => { + return ( + + + + {title} + + + {voices.length} + + + + {voices.length > 0 ? ( + title === VoicesSections.MY_VOICES ? ( + + {voices.map((voice) => ( + + + + ))} + + ) : ( + + {voices.map((voice) => ( + + ))} + + ) + ) : ( + + You have no voices right now. + + )} + + ); +}; + +export { VoiceSection }; diff --git a/frontend/src/bundles/voices/enums/voices-sections.ts b/frontend/src/bundles/voices/enums/voices-sections.ts new file mode 100644 index 000000000..261d97dfc --- /dev/null +++ b/frontend/src/bundles/voices/enums/voices-sections.ts @@ -0,0 +1,6 @@ +const VoicesSections = { + VOICES: 'OutreachVids Library', + MY_VOICES: 'My Voices', +} as const; + +export { VoicesSections }; diff --git a/frontend/src/bundles/voices/pages/voices.tsx b/frontend/src/bundles/voices/pages/voices.tsx new file mode 100644 index 000000000..1347b4561 --- /dev/null +++ b/frontend/src/bundles/voices/pages/voices.tsx @@ -0,0 +1,19 @@ +import { + Box, + Header, + Sidebar, +} from '~/bundles/common/components/components.js'; +import { MainContent } from '~/bundles/voices/components/components.js'; + +const Voices: React.FC = () => { + return ( + +
+ + + + + ); +}; + +export { Voices }; diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx index 1a6e2e18d..7e22c679b 100644 --- a/frontend/src/routes/routes.tsx +++ b/frontend/src/routes/routes.tsx @@ -7,6 +7,7 @@ import { CreateAvatar } from '~/bundles/create-avatar/pages/create-avatar.js'; import { Home } from '~/bundles/home/pages/home.js'; import { MyAvatar } from '~/bundles/my-avatar/pages/my-avatar.js'; import { Studio } from '~/bundles/studio/pages/studio.js'; +import { Voices } from '~/bundles/voices/pages/voices.js'; const routes = [ { @@ -45,6 +46,14 @@ const routes = [ ), }, + { + path: AppRoute.VOICES, + element: ( + + + + ), + }, { path: AppRoute.CREATE_AVATAR, element: ( From ecee636a197efa1574235f7f385d7b614137b6eb Mon Sep 17 00:00:00 2001 From: Albina Date: Wed, 25 Sep 2024 22:10:10 +0300 Subject: [PATCH 2/5] OV-403: + add liked voices --- frontend/package.json | 1 + .../icons/helper/icon-conversion.helper.ts | 6 ++++ .../src/bundles/common/icons/icon-name.ts | 4 +++ frontend/src/bundles/home/store/slice.ts | 17 +++++++++-- frontend/src/bundles/home/types/types.ts | 2 +- frontend/src/bundles/home/types/voice.type.ts | 7 +++++ .../studio/components/control/control.tsx | 6 +++- .../components/main-content/main-content.tsx | 8 +++++- .../components/voice-card/voice-card.tsx | 28 +++++++++++++------ package-lock.json | 12 ++++++++ 10 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 frontend/src/bundles/home/types/voice.type.ts diff --git a/frontend/package.json b/frontend/package.json index 42273bf54..f88ffd746 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "@dnd-kit/core": "6.1.0", "@emotion/react": "11.13.0", "@emotion/styled": "11.13.0", + "@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", "@reduxjs/toolkit": "2.2.7", diff --git a/frontend/src/bundles/common/icons/helper/icon-conversion.helper.ts b/frontend/src/bundles/common/icons/helper/icon-conversion.helper.ts index d4345bd27..4c3d5ae56 100644 --- a/frontend/src/bundles/common/icons/helper/icon-conversion.helper.ts +++ b/frontend/src/bundles/common/icons/helper/icon-conversion.helper.ts @@ -1,3 +1,4 @@ +import { faHeart as faHeartRegular } from '@fortawesome/free-regular-svg-icons'; import { faBackwardStep, faCircleUser, @@ -7,6 +8,7 @@ import { faFileLines, faFont, faForwardStep, + faHeart, faHouse, faImage, faPause, @@ -45,6 +47,8 @@ const VolumeOff = convertIcon(faVolumeOff); const Stop = convertIcon(faStop); const VideoCamera = convertIcon(faVideoCamera); const Image = convertIcon(faImage); +const HeartFill = convertIcon(faHeart); +const HeartOutline = convertIcon(faHeartRegular); export { BackwardStep, @@ -55,6 +59,8 @@ export { FileLines, Font, ForwardStep, + HeartFill, + HeartOutline, House, Image, Pause, diff --git a/frontend/src/bundles/common/icons/icon-name.ts b/frontend/src/bundles/common/icons/icon-name.ts index de222a192..c9fbd2524 100644 --- a/frontend/src/bundles/common/icons/icon-name.ts +++ b/frontend/src/bundles/common/icons/icon-name.ts @@ -22,6 +22,8 @@ import { FileLines, Font, ForwardStep, + HeartFill, + HeartOutline, House, Image, Pause, @@ -74,6 +76,8 @@ const IconName = { VIDEO_CAMERA: VideoCamera, IMAGE: Image, VOICE: Voice, + HEART_FILL: HeartFill, + HEART_OUTLINE: HeartOutline, } as const; export { IconName }; diff --git a/frontend/src/bundles/home/store/slice.ts b/frontend/src/bundles/home/store/slice.ts index a48129c29..afcdde106 100644 --- a/frontend/src/bundles/home/store/slice.ts +++ b/frontend/src/bundles/home/store/slice.ts @@ -1,3 +1,4 @@ +import { type PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { DataStatus } from '~/bundles/common/enums/enums.js'; @@ -24,7 +25,16 @@ const initialState: State = { const { reducer, actions, name } = createSlice({ initialState, name: 'home', - reducers: {}, + reducers: { + toogleVoiceLike(state, action: PayloadAction) { + state.voices = state.voices.map((voice) => { + const { shortName, isLiked } = voice; + return shortName === action.payload + ? { ...voice, isLiked: !isLiked } + : voice; + }); + }, + }, extraReducers(builder) { builder.addCase(loadUserVideos.pending, (state) => { state.dataStatus = DataStatus.PENDING; @@ -53,7 +63,10 @@ const { reducer, actions, name } = createSlice({ state.dataStatus = DataStatus.PENDING; }); builder.addCase(loadVoices.fulfilled, (state, action) => { - state.voices = action.payload.items; + state.voices = action.payload.items.map((voice) => ({ + ...voice, + isLiked: false, + })); state.dataStatus = DataStatus.FULFILLED; }); builder.addCase(loadVoices.rejected, (state) => { diff --git a/frontend/src/bundles/home/types/types.ts b/frontend/src/bundles/home/types/types.ts index 766a60825..ea63aee9d 100644 --- a/frontend/src/bundles/home/types/types.ts +++ b/frontend/src/bundles/home/types/types.ts @@ -1,6 +1,6 @@ +export { type Voice } from '~/bundles/home/types/voice.type.js'; export { type GetVoicesResponseDto, type VideoGetAllItemResponseDto, type VideoGetAllResponseDto, - type Voice, } from 'shared'; diff --git a/frontend/src/bundles/home/types/voice.type.ts b/frontend/src/bundles/home/types/voice.type.ts new file mode 100644 index 000000000..c735f838c --- /dev/null +++ b/frontend/src/bundles/home/types/voice.type.ts @@ -0,0 +1,7 @@ +import { type Voice as SharedVoice } from 'shared'; + +type Voice = SharedVoice & { + isLiked: boolean; +}; + +export { type Voice }; diff --git a/frontend/src/bundles/studio/components/control/control.tsx b/frontend/src/bundles/studio/components/control/control.tsx index 93c1f7e57..3130c0024 100644 --- a/frontend/src/bundles/studio/components/control/control.tsx +++ b/frontend/src/bundles/studio/components/control/control.tsx @@ -11,6 +11,7 @@ type Properties = { label: string; size: IconSizeT; icon: ElementType; + iconColor?: string; onClick?: (event: React.MouseEvent) => void; width?: string; height?: string; @@ -22,6 +23,7 @@ const Control: React.FC = ({ label, size, icon, + iconColor, onClick = (): void => {}, width, height, @@ -37,7 +39,9 @@ const Control: React.FC = ({ {...(height && { height })} size={size} variant={variant} - icon={} + icon={ + + } onClick={onClick} /> diff --git a/frontend/src/bundles/voices/components/main-content/main-content.tsx b/frontend/src/bundles/voices/components/main-content/main-content.tsx index dd34215eb..884113766 100644 --- a/frontend/src/bundles/voices/components/main-content/main-content.tsx +++ b/frontend/src/bundles/voices/components/main-content/main-content.tsx @@ -9,6 +9,7 @@ import { useAppDispatch, useAppSelector, useEffect, + useMemo, } from '~/bundles/common/hooks/hooks.js'; import { loadVoices } from '~/bundles/home/store/actions.js'; import { VoiceSection } from '~/bundles/voices/components/components.js'; @@ -22,6 +23,11 @@ const MainContent: React.FC = () => { const { voices, dataStatus } = useAppSelector(({ home }) => home); + const myVoices = useMemo( + () => voices.filter((voice) => voice.isLiked), + [voices], + ); + useEffect(() => { void dispatch(loadVoices()); }, [dispatch]); @@ -35,7 +41,7 @@ const MainContent: React.FC = () => { - + ); diff --git a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx index 5374b0a97..613514cdb 100644 --- a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx +++ b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx @@ -15,9 +15,10 @@ import { useState, } from '~/bundles/common/hooks/hooks.js'; import { IconName, IconSize } from '~/bundles/common/icons/icons.js'; +import { actions as homeActions } from '~/bundles/home/store/home.js'; +import { type Voice } from '~/bundles/home/types/types.js'; import { Control } from '~/bundles/studio/components/control/control.js'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; -import { type Voice } from '~/bundles/studio/types/types.js'; type Properties = { voice: Voice; @@ -81,9 +82,9 @@ const VoiceCard: React.FC = ({ voice }) => { isLoading, ], ); - // const handleCardClick = useCallback((): void => { - // onClick(voice, url); - // }, [onClick, voice, url]); + const handleLikeClick = useCallback((): void => { + dispatch(homeActions.toogleVoiceLike(voice.shortName)); + }, [voice, dispatch]); const iconComponent = useMemo(() => { if (isLoading) { @@ -96,10 +97,7 @@ const VoiceCard: React.FC = ({ voice }) => { }, [isPlaying, isLoading, url]); return ( - + = ({ voice }) => { icon={iconComponent} onClick={handlePlayClick} /> - + {voice.name} + diff --git a/package-lock.json b/package-lock.json index f0e370f0e..9dac61d39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "@dnd-kit/core": "6.1.0", "@emotion/react": "11.13.0", "@emotion/styled": "11.13.0", + "@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", "@reduxjs/toolkit": "2.2.7", @@ -9008,6 +9009,17 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", From 596c47fd38e89f41819471001e2bdb1c0862291b Mon Sep 17 00:00:00 2001 From: Albina Date: Wed, 25 Sep 2024 22:25:56 +0300 Subject: [PATCH 3/5] OV-403: * move player to common components --- .../{studio => common}/components/audio-player/audio-player.tsx | 0 .../components/audio-player/constants/constants.ts | 0 .../components/audio-player/enums/audio-event.ts | 0 .../{studio => common}/components/audio-player/enums/enums.ts | 0 frontend/src/bundles/common/components/components.ts | 1 + .../studio/components/player-controls/player-controls.tsx | 2 +- frontend/src/bundles/studio/pages/studio.tsx | 2 +- 7 files changed, 3 insertions(+), 2 deletions(-) rename frontend/src/bundles/{studio => common}/components/audio-player/audio-player.tsx (100%) rename frontend/src/bundles/{studio => common}/components/audio-player/constants/constants.ts (100%) rename frontend/src/bundles/{studio => common}/components/audio-player/enums/audio-event.ts (100%) rename frontend/src/bundles/{studio => common}/components/audio-player/enums/enums.ts (100%) diff --git a/frontend/src/bundles/studio/components/audio-player/audio-player.tsx b/frontend/src/bundles/common/components/audio-player/audio-player.tsx similarity index 100% rename from frontend/src/bundles/studio/components/audio-player/audio-player.tsx rename to frontend/src/bundles/common/components/audio-player/audio-player.tsx diff --git a/frontend/src/bundles/studio/components/audio-player/constants/constants.ts b/frontend/src/bundles/common/components/audio-player/constants/constants.ts similarity index 100% rename from frontend/src/bundles/studio/components/audio-player/constants/constants.ts rename to frontend/src/bundles/common/components/audio-player/constants/constants.ts diff --git a/frontend/src/bundles/studio/components/audio-player/enums/audio-event.ts b/frontend/src/bundles/common/components/audio-player/enums/audio-event.ts similarity index 100% rename from frontend/src/bundles/studio/components/audio-player/enums/audio-event.ts rename to frontend/src/bundles/common/components/audio-player/enums/audio-event.ts diff --git a/frontend/src/bundles/studio/components/audio-player/enums/enums.ts b/frontend/src/bundles/common/components/audio-player/enums/enums.ts similarity index 100% rename from frontend/src/bundles/studio/components/audio-player/enums/enums.ts rename to frontend/src/bundles/common/components/audio-player/enums/enums.ts diff --git a/frontend/src/bundles/common/components/components.ts b/frontend/src/bundles/common/components/components.ts index 20ba7b934..6ac2d2e24 100644 --- a/frontend/src/bundles/common/components/components.ts +++ b/frontend/src/bundles/common/components/components.ts @@ -1,3 +1,4 @@ +export { AudioPlayer } from './audio-player/audio-player.js'; export { Button } from './button/button.js'; export { ComponentsProvider } from './components-provider/components-provider.js'; export { Header } from './header/header.js'; diff --git a/frontend/src/bundles/studio/components/player-controls/player-controls.tsx b/frontend/src/bundles/studio/components/player-controls/player-controls.tsx index cd14ace55..ab9f1289b 100644 --- a/frontend/src/bundles/studio/components/player-controls/player-controls.tsx +++ b/frontend/src/bundles/studio/components/player-controls/player-controls.tsx @@ -2,6 +2,7 @@ import { type PlayerRef } from '@remotion/player'; import { secondsToMilliseconds } from 'date-fns'; import { type RefObject } from 'react'; +import { FPS } from '~/bundles/common/components/audio-player/constants/constants.js'; import { Flex, Spinner } from '~/bundles/common/components/components.js'; import { useAppDispatch, @@ -16,7 +17,6 @@ import { setItemsSpan } from '~/bundles/studio/helpers/set-items-span.js'; import { selectTotalDuration } from '~/bundles/studio/store/selectors.js'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; -import { FPS } from '../audio-player/constants/constants.js'; import { Control } from '../components.js'; import { TimeDisplay } from './components/components.js'; diff --git a/frontend/src/bundles/studio/pages/studio.tsx b/frontend/src/bundles/studio/pages/studio.tsx index 3aabb1eb8..779bc4b54 100644 --- a/frontend/src/bundles/studio/pages/studio.tsx +++ b/frontend/src/bundles/studio/pages/studio.tsx @@ -1,5 +1,6 @@ import { type PlayerRef } from '@remotion/player'; +import { AudioPlayer } from '~/bundles/common/components/audio-player/audio-player.js'; import { Box, Button, @@ -28,7 +29,6 @@ import { import { IconName } from '~/bundles/common/icons/icons.js'; import { notificationService } from '~/bundles/common/services/services.js'; -import { AudioPlayer } from '../components/audio-player/audio-player.js'; import { PlayerControls, Timeline, From dfa87029433bca6d596a6648264f0704834af36c Mon Sep 17 00:00:00 2001 From: Albina Date: Wed, 25 Sep 2024 23:05:08 +0300 Subject: [PATCH 4/5] OV-403: + add audio player for voices --- .../components/audio-player/audio-player.tsx | 4 +-- frontend/src/bundles/home/store/actions.ts | 14 +++++++- frontend/src/bundles/home/store/home.ts | 7 +++- frontend/src/bundles/home/store/slice.ts | 13 +++++++ frontend/src/bundles/home/types/types.ts | 2 ++ .../components/voice-card/voice-card.tsx | 14 ++++---- frontend/src/bundles/voices/pages/voices.tsx | 34 +++++++++++++++---- 7 files changed, 70 insertions(+), 18 deletions(-) diff --git a/frontend/src/bundles/common/components/audio-player/audio-player.tsx b/frontend/src/bundles/common/components/audio-player/audio-player.tsx index 314dc5300..c2655bfca 100644 --- a/frontend/src/bundles/common/components/audio-player/audio-player.tsx +++ b/frontend/src/bundles/common/components/audio-player/audio-player.tsx @@ -14,7 +14,7 @@ type Properties = { isPlaying: boolean; audioUrl: string; onAudioEnd: () => void; - onSetDuration: (duration: number) => void; + onSetDuration?: (duration: number) => void; }; const AudioPlayer: React.FC = ({ @@ -41,7 +41,7 @@ const AudioPlayer: React.FC = ({ getAudioData(audioUrl) .then(({ durationInSeconds }) => { setDurationInFrames(Math.round(durationInSeconds * FPS)); - onSetDuration(durationInSeconds); + onSetDuration && onSetDuration(durationInSeconds); }) .catch(() => { setDurationInFrames(1); diff --git a/frontend/src/bundles/home/store/actions.ts b/frontend/src/bundles/home/store/actions.ts index f31618ba2..77de9acb8 100644 --- a/frontend/src/bundles/home/store/actions.ts +++ b/frontend/src/bundles/home/store/actions.ts @@ -2,6 +2,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; import { + type GenerateSpeechRequestDto, + type GenerateSpeechResponseDto, type GetVoicesResponseDto, type VideoGetAllResponseDto, } from '~/bundles/home/types/types.js'; @@ -37,4 +39,14 @@ const loadVoices = createAsyncThunk< return speechApi.loadVoices(); }); -export { deleteVideo, loadUserVideos, loadVoices }; +const generateScriptSpeechPreview = createAsyncThunk< + GenerateSpeechResponseDto, + GenerateSpeechRequestDto, + AsyncThunkConfig +>(`${sliceName}/generate-script-speech-preview`, (payload, { extra }) => { + const { speechApi } = extra; + + return speechApi.generateScriptSpeech(payload); +}); + +export { deleteVideo, generateScriptSpeechPreview, loadUserVideos, loadVoices }; diff --git a/frontend/src/bundles/home/store/home.ts b/frontend/src/bundles/home/store/home.ts index 9b26c21f9..b7ad69cfe 100644 --- a/frontend/src/bundles/home/store/home.ts +++ b/frontend/src/bundles/home/store/home.ts @@ -1,10 +1,15 @@ -import { deleteVideo, loadUserVideos } from './actions.js'; +import { + deleteVideo, + generateScriptSpeechPreview, + loadUserVideos, +} from './actions.js'; import { actions } from './slice.js'; const allActions = { ...actions, deleteVideo, loadUserVideos, + generateScriptSpeechPreview, }; export { reducer } from './slice.js'; diff --git a/frontend/src/bundles/home/store/slice.ts b/frontend/src/bundles/home/store/slice.ts index afcdde106..caa5e1eab 100644 --- a/frontend/src/bundles/home/store/slice.ts +++ b/frontend/src/bundles/home/store/slice.ts @@ -10,16 +10,26 @@ import { import { deleteVideo, loadUserVideos, loadVoices } from './actions.js'; +type VoicePlayer = { + isPlaying: boolean; + url: string | null; +}; + type State = { dataStatus: ValueOf; videos: Array | []; voices: Voice[]; + voicePlayer: VoicePlayer; }; const initialState: State = { dataStatus: DataStatus.IDLE, videos: [], voices: [], + voicePlayer: { + isPlaying: false, + url: null, + }, }; const { reducer, actions, name } = createSlice({ @@ -34,6 +44,9 @@ const { reducer, actions, name } = createSlice({ : voice; }); }, + playVoice(state, action: PayloadAction>) { + state.voicePlayer = { ...state.voicePlayer, ...action.payload }; + }, }, extraReducers(builder) { builder.addCase(loadUserVideos.pending, (state) => { diff --git a/frontend/src/bundles/home/types/types.ts b/frontend/src/bundles/home/types/types.ts index ea63aee9d..3f0c442ff 100644 --- a/frontend/src/bundles/home/types/types.ts +++ b/frontend/src/bundles/home/types/types.ts @@ -1,5 +1,7 @@ export { type Voice } from '~/bundles/home/types/voice.type.js'; export { + type GenerateSpeechRequestDto, + type GenerateSpeechResponseDto, type GetVoicesResponseDto, type VideoGetAllItemResponseDto, type VideoGetAllResponseDto, diff --git a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx index 613514cdb..0c084c507 100644 --- a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx +++ b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx @@ -18,7 +18,6 @@ import { IconName, IconSize } from '~/bundles/common/icons/icons.js'; import { actions as homeActions } from '~/bundles/home/store/home.js'; import { type Voice } from '~/bundles/home/types/types.js'; import { Control } from '~/bundles/studio/components/control/control.js'; -import { actions as studioActions } from '~/bundles/studio/store/studio.js'; type Properties = { voice: Voice; @@ -29,10 +28,11 @@ const VoiceCard: React.FC = ({ voice }) => { const [isLoading, setIsLoading] = useState(false); const dispatch = useAppDispatch(); - const text = 'Sample text'; + const text = + 'Hello, I can handle video speech for you, choose me if you like it!'; const { isPlaying: playerIsPlaying, url: playerUrl } = useAppSelector( - ({ studio }) => studio.scriptPlayer, + ({ home }) => home.voicePlayer, ); const isPlaying = useMemo( @@ -46,14 +46,12 @@ const VoiceCard: React.FC = ({ voice }) => { return; } if (url) { - dispatch( - studioActions.playScript({ isPlaying: !isPlaying, url }), - ); + dispatch(homeActions.playVoice({ isPlaying: !isPlaying, url })); return; } setIsLoading(true); void dispatch( - studioActions.generateScriptSpeechPreview({ + homeActions.generateScriptSpeechPreview({ scriptId: uuidv4(), text, voiceName: voice.shortName, @@ -64,7 +62,7 @@ const VoiceCard: React.FC = ({ voice }) => { setUrl(audioUrl); setIsLoading(false); dispatch( - studioActions.playScript({ + homeActions.playVoice({ isPlaying: true, url: audioUrl, }), diff --git a/frontend/src/bundles/voices/pages/voices.tsx b/frontend/src/bundles/voices/pages/voices.tsx index 1347b4561..1d5db4ea5 100644 --- a/frontend/src/bundles/voices/pages/voices.tsx +++ b/frontend/src/bundles/voices/pages/voices.tsx @@ -1,18 +1,40 @@ import { + AudioPlayer, Box, Header, Sidebar, } from '~/bundles/common/components/components.js'; +import { + useAppDispatch, + useAppSelector, + useCallback, +} from '~/bundles/common/hooks/hooks.js'; +import { actions as homeActions } from '~/bundles/home/store/home.js'; import { MainContent } from '~/bundles/voices/components/components.js'; const Voices: React.FC = () => { + const dispatch = useAppDispatch(); + const { isPlaying, url } = useAppSelector(({ home }) => home.voicePlayer); + const handleAudioEnd = useCallback((): void => { + dispatch(homeActions.playVoice({ isPlaying: false })); + }, [dispatch]); + return ( - -
- - - - + <> + +
+ + + + + {url && ( + + )} + ); }; From 6b9d07418218b24b6e1cc1976360077ccef7b0b7 Mon Sep 17 00:00:00 2001 From: Albina Date: Thu, 26 Sep 2024 19:00:36 +0300 Subject: [PATCH 5/5] OV-403: * implement review suggestion --- .../components/audio-player/audio-player.tsx | 4 +++- .../voices/components/voice-card/voice-card.tsx | 17 +++-------------- .../components/voice-section/voice-section.tsx | 3 ++- .../src/bundles/voices/constants/constants.ts | 2 ++ .../constants/text-for-voices.constant.ts | 4 ++++ 5 files changed, 14 insertions(+), 16 deletions(-) create mode 100644 frontend/src/bundles/voices/constants/constants.ts create mode 100644 frontend/src/bundles/voices/constants/text-for-voices.constant.ts diff --git a/frontend/src/bundles/common/components/audio-player/audio-player.tsx b/frontend/src/bundles/common/components/audio-player/audio-player.tsx index c2655bfca..614209cc3 100644 --- a/frontend/src/bundles/common/components/audio-player/audio-player.tsx +++ b/frontend/src/bundles/common/components/audio-player/audio-player.tsx @@ -41,7 +41,9 @@ const AudioPlayer: React.FC = ({ getAudioData(audioUrl) .then(({ durationInSeconds }) => { setDurationInFrames(Math.round(durationInSeconds * FPS)); - onSetDuration && onSetDuration(durationInSeconds); + if (onSetDuration) { + onSetDuration(durationInSeconds); + } }) .catch(() => { setDurationInFrames(1); diff --git a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx index 0c084c507..a7b766313 100644 --- a/frontend/src/bundles/voices/components/voice-card/voice-card.tsx +++ b/frontend/src/bundles/voices/components/voice-card/voice-card.tsx @@ -18,6 +18,7 @@ import { IconName, IconSize } from '~/bundles/common/icons/icons.js'; import { actions as homeActions } from '~/bundles/home/store/home.js'; import { type Voice } from '~/bundles/home/types/types.js'; import { Control } from '~/bundles/studio/components/control/control.js'; +import { TEXT_FOR_VOICES } from '~/bundles/voices/constants/constants.js'; type Properties = { voice: Voice; @@ -28,9 +29,6 @@ const VoiceCard: React.FC = ({ voice }) => { const [isLoading, setIsLoading] = useState(false); const dispatch = useAppDispatch(); - const text = - 'Hello, I can handle video speech for you, choose me if you like it!'; - const { isPlaying: playerIsPlaying, url: playerUrl } = useAppSelector( ({ home }) => home.voicePlayer, ); @@ -53,7 +51,7 @@ const VoiceCard: React.FC = ({ voice }) => { void dispatch( homeActions.generateScriptSpeechPreview({ scriptId: uuidv4(), - text, + text: TEXT_FOR_VOICES, voiceName: voice.shortName, }), ) @@ -69,16 +67,7 @@ const VoiceCard: React.FC = ({ voice }) => { ); }); }, - [ - dispatch, - text, - url, - voice, - isPlaying, - setUrl, - setIsLoading, - isLoading, - ], + [dispatch, url, voice, isPlaying, setUrl, setIsLoading, isLoading], ); const handleLikeClick = useCallback((): void => { dispatch(homeActions.toogleVoiceLike(voice.shortName)); diff --git a/frontend/src/bundles/voices/components/voice-section/voice-section.tsx b/frontend/src/bundles/voices/components/voice-section/voice-section.tsx index 76d67b5dc..051006891 100644 --- a/frontend/src/bundles/voices/components/voice-section/voice-section.tsx +++ b/frontend/src/bundles/voices/components/voice-section/voice-section.tsx @@ -6,6 +6,7 @@ import { SimpleGrid, Text, } from '~/bundles/common/components/components.js'; +import { EMPTY_VALUE } from '~/bundles/common/constants/constants.js'; import { type Voice } from '~/bundles/home/types/types.js'; import { VoiceCard } from '~/bundles/voices/components/components.js'; import { VoicesSections } from '~/bundles/voices/enums/voices-sections.js'; @@ -34,7 +35,7 @@ const VoiceSection: React.FC = ({ voices, title }) => { - {voices.length > 0 ? ( + {voices.length > EMPTY_VALUE ? ( title === VoicesSections.MY_VOICES ? ( {voices.map((voice) => ( diff --git a/frontend/src/bundles/voices/constants/constants.ts b/frontend/src/bundles/voices/constants/constants.ts new file mode 100644 index 000000000..624b13d5f --- /dev/null +++ b/frontend/src/bundles/voices/constants/constants.ts @@ -0,0 +1,2 @@ +export { TEXT_FOR_VOICES } from './text-for-voices.constant.js'; +export { EMPTY_VALUE } from 'shared'; diff --git a/frontend/src/bundles/voices/constants/text-for-voices.constant.ts b/frontend/src/bundles/voices/constants/text-for-voices.constant.ts new file mode 100644 index 000000000..e6f13b4a6 --- /dev/null +++ b/frontend/src/bundles/voices/constants/text-for-voices.constant.ts @@ -0,0 +1,4 @@ +const TEXT_FOR_VOICES = + 'Hello, I can handle video speech for you, choose me if you like it!'; + +export { TEXT_FOR_VOICES };