Skip to content

Commit

Permalink
Merge pull request #228 from BinaryStudioAcademy/task/OV-144-add-scri…
Browse files Browse the repository at this point in the history
…pt-generation-for-videos

OV-144: Add Script Generation For Videos
  • Loading branch information
nikita-remeslov authored Sep 12, 2024
2 parents c94df94 + 8677bcf commit ab60c04
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 36 deletions.
4 changes: 4 additions & 0 deletions frontend/src/bundles/common/components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export {
Spinner,
Stack,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
Tooltip,
VStack,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MessageSender } from '~/bundles/chat/enums/enums.js';
import {
Button,
FormProvider,
Expand All @@ -6,7 +7,12 @@ import {
Textarea,
VStack,
} from '~/bundles/common/components/components.js';
import { useAppForm } from '~/bundles/common/hooks/hooks.js';
import { DataStatus } from '~/bundles/common/enums/enums.js';
import {
useAppForm,
useAppSelector,
useCallback,
} from '~/bundles/common/hooks/hooks.js';
import {
type GenerateVideoScriptRequestDto,
generateVideoScriptValidationSchema,
Expand All @@ -20,6 +26,12 @@ type Properties = {
};

const GenerateScriptForm: React.FC<Properties> = ({ onSubmit }) => {
const { messages, dataStatus } = useAppSelector(({ chat }) => ({
messages: chat.messages.filter(
(message) => message.sender === MessageSender.AI,
),
dataStatus: chat.dataStatus,
}));
const form = useAppForm<GenerateVideoScriptRequestDto>({
initialValues: DEFAULT_GENERATE_SCRIPT_PAYLOAD,
validationSchema: generateVideoScriptValidationSchema,
Expand All @@ -28,6 +40,21 @@ const GenerateScriptForm: React.FC<Properties> = ({ onSubmit }) => {

const { handleSubmit, values } = form;

const getButtonLabel = useCallback(() => {
if (dataStatus === DataStatus.PENDING) {
return 'Stop';
}

return messages?.length > 0 ? 'Re-Generate' : 'Generate script';
}, [dataStatus, messages]);

const isSubmitButtonDisabled = useCallback(() => {
return (
dataStatus === DataStatus.PENDING ||
values.topic.trim().length === 0
);
}, [dataStatus, values]);

return (
<FormProvider value={form}>
<form onSubmit={handleSubmit}>
Expand Down Expand Up @@ -61,9 +88,9 @@ const GenerateScriptForm: React.FC<Properties> = ({ onSubmit }) => {
/>
<Button
type="submit"
label="Generate script"
label={getButtonLabel()}
margin-top="50px"
isDisabled={values.topic.trim().length === 0}
isDisabled={isSubmitButtonDisabled()}
/>
</VStack>
</form>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type ComponentWithAs, type IconProps } from '@chakra-ui/react';

import { Flex, Icon, Text } from '~/bundles/common/components/components.js';

import styles from './styles.module.css';

type Properties = {
message: string;
icon?: ComponentWithAs<'svg', IconProps>;
};

const GenerateScriptPlaceholderContent: React.FC<Properties> = ({
message,
icon,
}) => (
<Flex className={styles['scriptPlaceholderContentContainer']}>
{icon && (
<Icon
as={icon}
className={styles['scriptPlaceholderContentIcon']}
/>
)}
<Text className={styles['scriptPlaceholderContentText']} variant="H3">
{message}
</Text>
</Flex>
);

export { GenerateScriptPlaceholderContent };
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.scriptPlaceholderContentContainer {
gap: 10px;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.scriptPlaceholderContentIcon {
color: var(--chakra-colors-brand-secondary-300);
opacity: 0.5;
font-size: 2rem;
}
.scriptPlaceholderContentText {
color: var(--chakra-colors-gray-400);
width: 40%;
min-width: 175px;
text-align: center;
font-style: italic;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
import { Icon, Text, VStack } from '@chakra-ui/react';

import { Box, Loader, VStack } from '~/bundles/common/components/components.js';
import { DataStatus } from '~/bundles/common/enums/data-status.enum.js';
import { useAppSelector } from '~/bundles/common/hooks/hooks.js';
import { IconName } from '~/bundles/common/icons/icons.js';
import { type VideoScript } from '~/bundles/common/types/types.js';

import { GenerateScriptPlaceholderContent } from '../generate-script-placeholder-content/generate-script-placeholder-content.js';
import { GenerateScriptScene } from '../generate-script-scene/generate-script-scene.js';
import styles from './styles.module.css';

const GenerateScriptPlaceholder: React.FC = () => {
type Properties = {
videoScripts: VideoScript[];
};

const GenerateScriptPlaceholder: React.FC<Properties> = ({ videoScripts }) => {
const { dataStatus } = useAppSelector(({ chat }) => ({
dataStatus: chat.dataStatus,
}));

const renderLoadingState = (): React.ReactNode => (
<Box mt="100px">
<Loader />
</Box>
);

const renderEmptyState = (): React.ReactNode => (
<GenerateScriptPlaceholderContent
message="Here you will see your generated script"
icon={IconName.SCROLL}
/>
);

const renderScripts = (): React.ReactNode => (
<>
{videoScripts.map((videoScript, index) => (
<GenerateScriptScene key={index} videoScript={videoScript} />
))}
</>
);

const getContent = (): React.ReactNode => {
if (dataStatus === DataStatus.PENDING) {
return renderLoadingState();
}
if (videoScripts.length === 0) {
return renderEmptyState();
}
return renderScripts();
};

return (
<VStack className={styles['scriptPlaceholderContainer']}>
<Icon
as={IconName.SCROLL}
className={styles['scriptPlaceholderIcon']}
boxSize={10}
/>
<Text className={styles['scriptPlaceholderText']} variant="H3">
Here you will see your generated script
</Text>
{getContent()}
</VStack>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
.scriptPlaceholderContainer {
width: 100%;
height: 100vh;
min-height: 480px;
max-height: 480px;
overflow-y: auto;
padding: 40px;
gap: 10px;
}

.scriptPlaceholderIcon {
color: var(--chakra-colors-brand-secondary-300);
opacity: 0.5;
font-size: 2rem;
}

.scriptPlaceholderText {
color: var(--chakra-colors-gray-400);
width: 40%;
min-width: 175px;
text-align: center;
font-style: italic;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Box, Heading } from '~/bundles/common/components/components.js';
import { useCallback, useState } from '~/bundles/common/hooks/hooks.js';
import { type VideoScript } from '~/bundles/common/types/types.js';

import styles from './styles.module.css';

type Properties = {
videoScript: VideoScript;
};

const GenerateScriptScene: React.FC<Properties> = ({ videoScript }) => {
const { title, description } = videoScript;
const [animationDone, setAnimationDone] = useState(false);

const handleAnimationEnd = useCallback(() => {
setAnimationDone(true);
}, [setAnimationDone]);

return (
<Box mb="10" width="100%">
<Heading as="h4" variant="H4" color="typography.600">
{title}
</Heading>
<Box
as="p"
className={
animationDone
? styles['typing-effect--done']
: styles['typing-effect']
}
onAnimationEnd={handleAnimationEnd}
>
{description}
</Box>
</Box>
);
};

export { GenerateScriptScene };
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.typing-effect {
overflow: hidden;
border-right: 2px solid black;
width: 0;
animation:
typing 3s steps(30, end) forwards,
blink-caret 0.75s step-end infinite;
}
.typing-effect--done {
white-space: normal;
overflow: visible;
border-right: none;
}

@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}

@keyframes blink-caret {
from,
to {
border-color: transparent;
}
50% {
border-color: black;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { type GenerateTextRequestDto } from '~/bundles/chat/chat.js';
import { MessageSender } from '~/bundles/chat/enums/message-sender.js';
import { actions as chatActions } from '~/bundles/chat/store/chat.js';
import {
Heading,
HStack,
Expand All @@ -6,24 +9,66 @@ import {
TabPanel,
TabPanels,
Tabs,
} from '@chakra-ui/react';

import { useCallback } from '~/bundles/common/hooks/hooks.js';
} from '~/bundles/common/components/components.js';
import {
getVideoScriptMessageFromPayload,
sanitizeJsonString,
} from '~/bundles/common/components/video-modal/components/video-modal-content/helpers/helpers.js';
import {
useAppDispatch,
useAppSelector,
useCallback,
useMemo,
} from '~/bundles/common/hooks/hooks.js';
import { type VideoScript } from '~/bundles/common/types/video-script.type.js';
import { type GenerateVideoScriptRequestDto } from '~/bundles/video-scripts/video-scripts.js';

import { GenerateScriptForm } from '../generate-script-form/generate-script-form.js';
import { GenerateScriptPlaceholder } from '../generate-script-placeholder/generate-script-placeholder.js';
import styles from './styles.module.css';

const GenerateScriptView: React.FC = () => {
const dispatch = useAppDispatch();
const { messages } = useAppSelector(({ chat }) => ({
messages: chat.messages.filter(
(message) => message.sender === MessageSender.AI,
),
}));

const handleGenerateVideoScriptSubmit = useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(payload: GenerateVideoScriptRequestDto): void => {
// TODO dispatch video script generation action
const sendMessageRequest: GenerateTextRequestDto = {
message: getVideoScriptMessageFromPayload(payload, messages),
};
void dispatch(chatActions.sendMessage(sendMessageRequest));
},
[],
[messages, dispatch],
);

const lastGeneratedScript: VideoScript[] = useMemo(() => {
if (!messages || messages.length === 0) {
return [];
}

const lastMessage = messages.at(-1);
if (!lastMessage) {
return [];
}

try {
const sanitizedJson = sanitizeJsonString(lastMessage.text);
const videoScripts: VideoScript[] = JSON.parse(sanitizedJson);
return videoScripts;
} catch {
return [
{
title: 'Scene',
description: lastMessage.text,
},
];
}
}, [messages]);

return (
<>
<Heading className={styles['scriptViewHeading']} variant="H3">
Expand All @@ -48,7 +93,9 @@ const GenerateScriptView: React.FC = () => {
<GenerateScriptForm
onSubmit={handleGenerateVideoScriptSubmit}
/>
<GenerateScriptPlaceholder />
<GenerateScriptPlaceholder
videoScripts={lastGeneratedScript}
/>
</HStack>
</TabPanel>
</TabPanels>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@

.scriptViewHStack {
justify-content: space-between;
align-items: flex-start;
}
Loading

0 comments on commit ab60c04

Please sign in to comment.