Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OV-205: Add voices modal #240

Merged
merged 20 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5cc42b6
OV-205: * move modal logic to separate component
Alblupynos Sep 9, 2024
dfdbd79
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 9, 2024
60b3b85
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 9, 2024
8e49f94
OV-205: + add voices modal
Alblupynos Sep 10, 2024
a02d3ae
OV-205: * format structure, extract styles
Alblupynos Sep 10, 2024
f854069
OV-205: + add type for voice
Alblupynos Sep 10, 2024
7372855
OV-205: + implement voice selection
Alblupynos Sep 10, 2024
e3bed03
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 10, 2024
44ed24f
OV-205: * fix formatting
Alblupynos Sep 10, 2024
79a720e
OV-205: * fix formatting
Alblupynos Sep 10, 2024
2cc710e
OV-205: * fix styling
Alblupynos Sep 11, 2024
29fde77
OV-205: * fix review suggestions
Alblupynos Sep 11, 2024
0874e63
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 11, 2024
cec15c9
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 11, 2024
d45f7d0
OV-205: * merge script properties
Alblupynos Sep 11, 2024
a4cc7a7
OV-205: * fix styling
Alblupynos Sep 11, 2024
2590daa
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 13, 2024
8fa71bf
OV-205: * refactor voice type
Alblupynos Sep 13, 2024
532b10c
OV-205: * fix imports
Alblupynos Sep 13, 2024
5fb62af
Merge remote-tracking branch 'origin/next' into task/OV-205-Add-voice…
Alblupynos Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/bundles/common/components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export { VideoModal } from './video-modal/video-modal.js';
export { VideoPlayer } from './video-player/video-player.js';
export {
Badge,
Modal as BaseModal,
Box,
Card,
CardBody,
Center,
Checkbox,
Circle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { VideoModalContent } from './components/components.js';

type Properties = {
isOpen: boolean;
onModalClose: () => void;
onClose: () => void;
};

const VideoModal: React.FC<Properties> = ({ isOpen, onModalClose }) => {
const VideoModal: React.FC<Properties> = ({ isOpen, onClose }) => {
const { handleCloseChat } = useChatCleanup({
onModalChatClose: onModalClose,
onModalChatClose: onClose,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Properties = {
label: string;
size: IconSizeT;
icon: ElementType;
onClick?: () => void;
onClick?: (event: React.MouseEvent) => void;
width?: string;
height?: string;
isRound?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Script } from './script.js';
export { VoicesModal } from './voices-modal/voices-modal.js';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Icon,
IconButton,
Spinner,
Text,
Tooltip,
VStack,
} from '~/bundles/common/components/components.js';
Expand All @@ -21,14 +22,15 @@ import { PlayIconNames } from '~/bundles/studio/enums/play-icon-names.enum.js';
import { actions as studioActions } from '~/bundles/studio/store/studio.js';
import { type Script as ScriptT } from '~/bundles/studio/types/types.js';

type Properties = ScriptT;
type Properties = ScriptT & { handleChangeVoice: (scriptId: string) => void };

const Script: React.FC<Properties> = ({
id,
text,
voiceName,
voice,
url,
iconName,
handleChangeVoice,
}) => {
const dispatch = useAppDispatch();

Expand Down Expand Up @@ -64,14 +66,18 @@ const Script: React.FC<Properties> = ({
return;
}

if (!voice) {
return;
}

void dispatch(
studioActions.generateScriptSpeech({
scriptId: id,
text,
voiceName,
voiceName: voice.shortName,
}),
);
}, [dispatch, id, text, url, voiceName]);
}, [dispatch, id, text, url, voice]);

const handleAudioEnd = useCallback((): void => {
setIsPlaying(false);
Expand All @@ -85,32 +91,45 @@ const Script: React.FC<Properties> = ({
return isPlaying ? IconName.STOP : IconName.PLAY;
}, [iconName, isPlaying]);

const handleChangeVoiceId = useCallback((): void => {
handleChangeVoice(id);
}, [handleChangeVoice, id]);

return (
<VStack w="full">
<HStack justify="end" w="full" gap={0}>
<Tooltip
isDisabled={Boolean(url)}
label="Click to update audio"
placement="top"
hasArrow
<HStack justify="space-between" w="full">
<Text
onClick={handleChangeVoiceId}
cursor="pointer"
variant="link"
>
{voice?.name || 'No voice'}
</Text>
<HStack gap={0}>
<Tooltip
isDisabled={Boolean(url)}
label="Click to update audio"
placement="top"
hasArrow
>
<IconButton
icon={<Icon as={iconComponent} />}
size="sm"
variant="ghostIconDark"
aria-label="Play script"
onClick={toggleIsPlaying}
borderRadius="100%"
border={url ? '' : '1px dotted'}
/>
</Tooltip>
<IconButton
icon={<Icon as={iconComponent} />}
icon={<Icon as={IconName.CLOSE} />}
size="sm"
variant="ghostIconDark"
aria-label="Play script"
onClick={toggleIsPlaying}
borderRadius="100%"
border={url ? '' : '1px dotted'}
aria-label="Delete script"
onClick={handleDeleteScript}
/>
</Tooltip>
<IconButton
icon={<Icon as={IconName.CLOSE} />}
size="sm"
variant="ghostIconDark"
aria-label="Delete script"
onClick={handleDeleteScript}
/>
</HStack>
</HStack>
<Editable
defaultValue={text}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.modal-header {
width: 290px;
padding: 33px 44px 0px;
align-self: start;
}
.modal-content {
gap: 20px;
overflow-y: auto;
padding: 20px;
align-content: space-between;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
Card,
CardBody,
HStack,
Text,
} from '~/bundles/common/components/components.js';
import { useCallback, 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 { type Voice } from '~/bundles/studio/types/types.js';

type Properties = {
voice: Voice;
isChecked: boolean;
onClick: (voice: Voice) => void;
};

const VoiceCard: React.FC<Properties> = ({ voice, isChecked, onClick }) => {
const [isPlaying, setIsPlaying] = useState(false);

const handlePlayClick = useCallback((event: React.MouseEvent): void => {
setIsPlaying((previous) => !previous);
event.stopPropagation();
}, []);
const handleCardClick = useCallback((): void => {
onClick(voice);
}, [onClick, voice]);
return (
<Card
variant={isChecked ? 'outline' : 'elevated'}
cursor="pointer"
onClick={handleCardClick}
>
<CardBody>
<HStack>
<Control
label={isPlaying ? 'Pause' : 'Play voice'}
size={IconSize.SMALL}
icon={isPlaying ? IconName.PAUSE : IconName.PLAY}
onClick={handlePlayClick}
/>
<Text variant="body1" color={'text.default'}>
{voice.name}
</Text>
</HStack>
</CardBody>
</Card>
);
};

export { VoiceCard };
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Heading,
SimpleGrid,
VStack,
} from '~/bundles/common/components/components.js';
import {
useAppDispatch,
useAppSelector,
useCallback,
} from '~/bundles/common/hooks/hooks.js';
import { mockVoices } from '~/bundles/studio/components/video-menu/components/mock/voices-mock.js';
import { actions as studioActions } from '~/bundles/studio/store/studio.js';
import { type Voice } from '~/bundles/studio/types/types.js';

import styles from './styles.module.css';
import { VoiceCard } from './voice-card.js';

type Properties = {
scriptId: string;
onModalClose: () => void;
};
const VoicesModalContent: React.FC<Properties> = ({
scriptId,
onModalClose,
}) => {
const dispatch = useAppDispatch();
const script = useAppSelector(({ studio }) =>
studio.scripts.find((s) => s.id === scriptId),
);
const handleCardClick = useCallback(
(voice: Voice): void => {
dispatch(studioActions.editScript({ id: scriptId, voice }));
onModalClose();
},
[dispatch, scriptId, onModalClose],
);
return (
<VStack>
<Heading
className={styles['modal-header']}
variant="H3"
color="typography.900"
>
AI Voice
</Heading>
<SimpleGrid
className={styles['modal-content']}
w="full"
columns={[2, null, 3]}
>
{mockVoices.map((card) => (
<VoiceCard
voice={card}
key={card.shortName}
isChecked={script?.voice?.shortName === card.shortName}
onClick={handleCardClick}
/>
))}
</SimpleGrid>
</VStack>
);
};

export { VoicesModalContent };
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Modal } from '~/bundles/common/components/components.js';

import { VoicesModalContent } from './components/voices-modal-content.js';

type Properties = {
isOpen: boolean;
onClose: () => void;
scriptId: string | null;
};
const VoicesModal: React.FC<Properties> = ({ isOpen, onClose, scriptId }) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
{scriptId && (
<VoicesModalContent
scriptId={scriptId}
onModalClose={onClose}
/>
)}
</Modal>
);
};

export { VoicesModal };
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,63 @@ import {
useAppDispatch,
useAppSelector,
useCallback,
useState,
} from '~/bundles/common/hooks/hooks.js';
import { IconName } from '~/bundles/common/icons/icons.js';
import { NEW_SCRIPT_TEXT } from '~/bundles/studio/components/constants/constants.js';
import { actions as studioActions } from '~/bundles/studio/store/studio.js';

import { Script } from './components/script.js';
import { Script, VoicesModal } from './components/components.js';

const ScriptContent: React.FC = () => {
const dispatch = useAppDispatch();
const scripts = useAppSelector(({ studio }) => studio.scripts);
const [changeVoiceScriptId, setChangeVoiceScriptId] = useState<
string | null
>(null);

const handleAddScript = useCallback((): void => {
void dispatch(studioActions.addScript(NEW_SCRIPT_TEXT));
}, [dispatch]);

const handleChangeVoiceClick = useCallback((scriptId: string): void => {
setChangeVoiceScriptId(scriptId);
}, []);

const handleCloseVoicesModal = useCallback((): void => {
setChangeVoiceScriptId(null);
}, []);

return (
<VStack w="full" spacing="20px" p="20px 0">
{scripts.length === 0 ? (
<Text variant="body1" width="60%" textAlign="center">
To add a script press a button below.
</Text>
) : (
scripts.map(({ id, ...script }) => (
<Script key={id} id={id} {...script} />
))
)}
<IconButton
icon={<Icon as={IconName.ADD} />}
aria-label="Add script"
borderRadius="100%"
onClick={handleAddScript}
<>
<VStack w="full" spacing="20px" p="20px 0">
{scripts.length === 0 ? (
<Text variant="body1" width="60%" textAlign="center">
To add a script press a button below.
</Text>
) : (
scripts.map(({ id, ...script }) => (
<Script
key={id}
id={id}
{...script}
handleChangeVoice={handleChangeVoiceClick}
/>
))
)}
<IconButton
icon={<Icon as={IconName.ADD} />}
aria-label="Add script"
borderRadius="100%"
onClick={handleAddScript}
/>
</VStack>
<VoicesModal
isOpen={changeVoiceScriptId !== null}
onClose={handleCloseVoicesModal}
scriptId={changeVoiceScriptId}
/>
</VStack>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type Voice } from '~/bundles/studio/types/types.js';

// TODO: remove when we will have voices in store
const defaultVoiceName = 'en-US-BrianMultilingualNeural';

const mockVoices: Voice[] = Array.from({ length: 10 }, (_, index) => ({
Alblupynos marked this conversation as resolved.
Show resolved Hide resolved
name: `Voice ${index + 1}`,
shortName: index === 0 ? defaultVoiceName : defaultVoiceName + index,
locale: '',
localeName: '',
voiceType: '',
}));

export { defaultVoiceName, mockVoices };
Loading
Loading