diff --git a/backend/package.json b/backend/package.json index 0e2f5886d..589e2364e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -53,6 +53,7 @@ "pino": "9.3.2", "pino-pretty": "10.3.1", "shared": "*", + "socket.io": "4.7.5", "swagger-jsdoc": "6.2.8", "tiktoken": "1.0.16" } diff --git a/backend/src/bundles/avatar-videos/avatar-videos.service.ts b/backend/src/bundles/avatar-videos/avatar-videos.service.ts index 8b121a8cf..5e335673f 100644 --- a/backend/src/bundles/avatar-videos/avatar-videos.service.ts +++ b/backend/src/bundles/avatar-videos/avatar-videos.service.ts @@ -1,6 +1,8 @@ import { type VideoGetAllItemResponseDto, HTTPCode, HttpError } from 'shared'; +import { AvatarVideoEvent } from '~/common/enums/enums.js'; import { type RemotionService } from '~/common/services/remotion/remotion.service.js'; +import { socketEvent } from '~/common/socket/socket.js'; import { type VideoService } from '../videos/video.service.js'; import { RenderVideoErrorMessage } from './enums/enums.js'; @@ -129,6 +131,7 @@ class AvatarVideoService { if (url) { // TODO: NOTIFY USER await this.videoService.update(videoRecordId, { url }); + socketEvent.emitNotification(AvatarVideoEvent.RENDER_SUCCESS); } await this.scenesService.clearAvatars(compositionForRender.scenes); diff --git a/backend/src/common/constants/constants.ts b/backend/src/common/constants/constants.ts index 1a5d15784..a707f62d8 100644 --- a/backend/src/common/constants/constants.ts +++ b/backend/src/common/constants/constants.ts @@ -1,2 +1,3 @@ +export { SOCKET_TRANSPORT_WEBSOCKETS } from './socket-trasnport.constants.js'; export { USER_PASSWORD_SALT_ROUNDS } from './user.constants.js'; export { WHITE_ROUTES } from './white-routes.constants.js'; diff --git a/backend/src/common/constants/socket-trasnport.constants.ts b/backend/src/common/constants/socket-trasnport.constants.ts new file mode 100644 index 000000000..4bc632c5d --- /dev/null +++ b/backend/src/common/constants/socket-trasnport.constants.ts @@ -0,0 +1,3 @@ +const SOCKET_TRANSPORT_WEBSOCKETS = 'websocket'; + +export { SOCKET_TRANSPORT_WEBSOCKETS }; diff --git a/backend/src/common/enums/avatar-video-event.enum.ts b/backend/src/common/enums/avatar-video-event.enum.ts new file mode 100644 index 000000000..4e8bae4ba --- /dev/null +++ b/backend/src/common/enums/avatar-video-event.enum.ts @@ -0,0 +1,6 @@ +const AvatarVideoEvent = { + RENDER_SUCCESS: 'render:success', + RENDER_FAILED: 'render:failed', +} as const; + +export { AvatarVideoEvent }; diff --git a/backend/src/common/enums/enums.ts b/backend/src/common/enums/enums.ts index 86a1f50c7..e2f5bf0ee 100644 --- a/backend/src/common/enums/enums.ts +++ b/backend/src/common/enums/enums.ts @@ -1,2 +1,4 @@ +export { AvatarVideoEvent } from './avatar-video-event.enum.js'; export { ParametersValidationMessage } from './parameters-validation-message.enum.js'; +export { SocketEvent } from './socket-event.enum.js'; export { ApiPath, AppEnvironment, ServerErrorType } from 'shared'; diff --git a/backend/src/common/enums/socket-event.enum.ts b/backend/src/common/enums/socket-event.enum.ts new file mode 100644 index 000000000..052bba5fa --- /dev/null +++ b/backend/src/common/enums/socket-event.enum.ts @@ -0,0 +1,6 @@ +const SocketEvent = { + CONNECTION: 'connection', + DISCONNECT: 'disconnect', +} as const; + +export { SocketEvent }; diff --git a/backend/src/common/server-application/base-server-app.ts b/backend/src/common/server-application/base-server-app.ts index 44f8adcf7..6b6e8af37 100644 --- a/backend/src/common/server-application/base-server-app.ts +++ b/backend/src/common/server-application/base-server-app.ts @@ -11,13 +11,19 @@ import Fastify, { type FastifyReply, type FastifyRequest, } from 'fastify'; +import { type Socket, Server } from 'socket.io'; import { type Config } from '~/common/config/config.js'; +import { + SOCKET_TRANSPORT_WEBSOCKETS, + WHITE_ROUTES, +} from '~/common/constants/constants.js'; import { type Database } from '~/common/database/database.js'; -import { ServerErrorType } from '~/common/enums/enums.js'; +import { ServerErrorType, SocketEvent } from '~/common/enums/enums.js'; import { type ValidationError } from '~/common/exceptions/exceptions.js'; -import { HTTPCode, HttpError } from '~/common/http/http.js'; +import { HTTPCode, HttpError, HTTPMethod } from '~/common/http/http.js'; import { type Logger } from '~/common/logger/logger.js'; +import { authenticateJWT } from '~/common/plugins/plugins.js'; import { session } from '~/common/plugins/session/session.plugin.js'; import { type ServerCommonErrorResponse, @@ -25,8 +31,7 @@ import { type ValidationSchema, } from '~/common/types/types.js'; -import { WHITE_ROUTES } from '../constants/constants.js'; -import { authenticateJWT } from '../plugins/plugins.js'; +import { initSocketConnection } from './socket-application.js'; import { type ServerApp, type ServerAppApi, @@ -51,6 +56,8 @@ class BaseServerApp implements ServerApp { private app: ReturnType; + private io: Server; + public constructor({ config, logger, database, apis }: Constructor) { this.config = config; this.logger = logger; @@ -58,6 +65,14 @@ class BaseServerApp implements ServerApp { this.apis = apis; this.app = Fastify(); + this.io = new Server(this.app.server, { + // This is to ensure that it dosent fall back to long polling as it return a 404 if it does + transports: [SOCKET_TRANSPORT_WEBSOCKETS], + cors: { + origin: this.config.ENV.APP.ORIGIN, + methods: [HTTPMethod.GET, HTTPMethod.POST], + }, + }); } public addRoute(parameters: ServerAppRouteParameters): void { @@ -241,6 +256,10 @@ class BaseServerApp implements ServerApp { this.database.connect(); + this.io.on(SocketEvent.CONNECTION, (socket: Socket) => + initSocketConnection(this.io, socket), + ); + await this.app .listen({ port: this.config.ENV.APP.PORT, diff --git a/backend/src/common/server-application/socket-application.ts b/backend/src/common/server-application/socket-application.ts new file mode 100644 index 000000000..09c00ccfa --- /dev/null +++ b/backend/src/common/server-application/socket-application.ts @@ -0,0 +1,12 @@ +import { type Server, type Socket } from 'socket.io'; + +import { SocketEvent } from '../enums/enums.js'; +import { socketEvent } from '../socket/socket.js'; + +const initSocketConnection = (io: Server, socket: Socket): void => { + socketEvent.initSocketConnection(io, socket); + + socket.on(SocketEvent.DISCONNECT, () => {}); +}; + +export { initSocketConnection }; diff --git a/backend/src/common/socket/scoket-event.ts b/backend/src/common/socket/scoket-event.ts new file mode 100644 index 000000000..424e85774 --- /dev/null +++ b/backend/src/common/socket/scoket-event.ts @@ -0,0 +1,31 @@ +import EventTarget from 'node:events'; + +import { type Server, type Socket } from 'socket.io'; + +class SocketEvent extends EventTarget { + private io: Server | null = null; + private socket: Socket | null = null; + + public initSocketConnection(io: Server, socket: Socket): void { + this.io = io; + this.socket = socket; + } + + public getIo(): Server | null { + return this.io; + } + + public getSocket(): Socket | null { + return this.socket; + } + + public emitNotification(event: string): void { + if (this.socket) { + this.socket.emit(event); + } + } +} + +const socketEvent = new SocketEvent(); + +export { socketEvent }; diff --git a/backend/src/common/socket/socket.ts b/backend/src/common/socket/socket.ts new file mode 100644 index 000000000..703956b43 --- /dev/null +++ b/backend/src/common/socket/socket.ts @@ -0,0 +1 @@ +export { socketEvent } from './scoket-event.js'; diff --git a/frontend/package.json b/frontend/package.json index 42273bf54..1c22cc7d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "react-router-dom": "6.26.0", "remotion": "4.0.201", "shared": "*", + "socket.io-client": "4.7.5", "zod-formik-adapter": "1.3.0" }, "overrides": { diff --git a/frontend/src/bundles/common/constants/constants.ts b/frontend/src/bundles/common/constants/constants.ts index 750a282ca..12338b873 100644 --- a/frontend/src/bundles/common/constants/constants.ts +++ b/frontend/src/bundles/common/constants/constants.ts @@ -1,2 +1,3 @@ export { FPS } from './fps.constant.js'; +export { SOCKET_TRANSPORT_WEBSOCKETS } from './socket-trasnport.constants.js'; export { EMPTY_VALUE } from 'shared'; diff --git a/frontend/src/bundles/common/constants/socket-trasnport.constants.ts b/frontend/src/bundles/common/constants/socket-trasnport.constants.ts new file mode 100644 index 000000000..4bc632c5d --- /dev/null +++ b/frontend/src/bundles/common/constants/socket-trasnport.constants.ts @@ -0,0 +1,3 @@ +const SOCKET_TRANSPORT_WEBSOCKETS = 'websocket'; + +export { SOCKET_TRANSPORT_WEBSOCKETS }; diff --git a/frontend/src/bundles/common/context/socket.ts b/frontend/src/bundles/common/context/socket.ts new file mode 100644 index 000000000..2f082acf8 --- /dev/null +++ b/frontend/src/bundles/common/context/socket.ts @@ -0,0 +1,14 @@ +import React from 'react'; +import { type Socket, io } from 'socket.io-client'; + +import { SOCKET_TRANSPORT_WEBSOCKETS } from '~/bundles/common/constants/constants.js'; + +const serverUrl = import.meta.env['VITE_APP_PROXY_SERVER_URL']; + +const socket = io(serverUrl, { + // This is to ensure that it dosent fall back to long polling as it return a 404 if it does + transports: [SOCKET_TRANSPORT_WEBSOCKETS], +}); +const SocketContext = React.createContext(socket); + +export { socket, SocketContext }; diff --git a/frontend/src/bundles/common/enums/avatar-video-event.enum.ts b/frontend/src/bundles/common/enums/avatar-video-event.enum.ts new file mode 100644 index 000000000..4e8bae4ba --- /dev/null +++ b/frontend/src/bundles/common/enums/avatar-video-event.enum.ts @@ -0,0 +1,6 @@ +const AvatarVideoEvent = { + RENDER_SUCCESS: 'render:success', + RENDER_FAILED: 'render:failed', +} as const; + +export { AvatarVideoEvent }; diff --git a/frontend/src/bundles/common/enums/enums.ts b/frontend/src/bundles/common/enums/enums.ts index 4777b1a14..e4d53fdf0 100644 --- a/frontend/src/bundles/common/enums/enums.ts +++ b/frontend/src/bundles/common/enums/enums.ts @@ -1,4 +1,5 @@ export { AppRoute } from './app-route.enum.js'; +export { AvatarVideoEvent } from './avatar-video-event.enum.js'; export { DataStatus } from './data-status.enum.js'; export { DOMEvent } from './dom-event.enum.js'; export { VideoPreview } from './video-preview.enum.js'; diff --git a/frontend/src/bundles/common/hooks/hooks.ts b/frontend/src/bundles/common/hooks/hooks.ts index 75233c6dc..54afa076f 100644 --- a/frontend/src/bundles/common/hooks/hooks.ts +++ b/frontend/src/bundles/common/hooks/hooks.ts @@ -5,6 +5,7 @@ export { useAppSelector } from './use-app-selector/use-app-selector.hook.js'; export { useFormField } from './use-form-field/use-form-field.hook.js'; export { useCallback, + useContext, useEffect, useLayoutEffect, useMemo, diff --git a/frontend/src/bundles/home/components/main-content/main-content.tsx b/frontend/src/bundles/home/components/main-content/main-content.tsx index 99ac4a842..45e9b22ca 100644 --- a/frontend/src/bundles/home/components/main-content/main-content.tsx +++ b/frontend/src/bundles/home/components/main-content/main-content.tsx @@ -4,12 +4,23 @@ import { 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 { SocketContext } from '~/bundles/common/context/socket.js'; +import { AvatarVideoEvent, DataStatus } from '~/bundles/common/enums/enums.js'; import { useAppDispatch, useAppSelector, + useContext, useEffect, } from '~/bundles/common/hooks/hooks.js'; +import { notificationService } from '~/bundles/common/services/services.js'; +import { + VIDEO_RENDER_FAILED_NOTIFICATION_ID, + VIDEO_RENDER_SUCCESS_NOTIFICATION_ID, +} from '~/bundles/home/constants/constants.js'; +import { + NotificationMessage, + NotificationTitle, +} from '~/bundles/home/enums/enums.js'; import { VideoGallery } from '~/bundles/home/enums/video-gallery.js'; import { actions as homeActions } from '~/bundles/home/store/home.js'; @@ -19,14 +30,50 @@ import styles from './styles.module.css'; const MainContent: React.FC = () => { const dispatch = useAppDispatch(); const { isCollapsed } = useCollapse(); + const socket = useContext(SocketContext); const { videos, dataStatus } = useAppSelector(({ home }) => home); // TODO: filter videos to get recent videos - useEffect(() => { - void dispatch(homeActions.loadUserVideos()); - }, [dispatch]); + const handleLoadUserVideos = (): void => { + void dispatch(homeActions.loadUserVideos()); + }; + + const handleRenderedVideoSuccess = (): void => { + notificationService.success({ + id: VIDEO_RENDER_SUCCESS_NOTIFICATION_ID, + title: NotificationTitle.VIDEO_RENDER_SUCCESS, + message: NotificationMessage.VIDEO_RENDER_SUCCESS, + }); + handleLoadUserVideos(); + }; + + const handleRenderedVideoFailed = (): void => { + socket.on(AvatarVideoEvent.RENDER_FAILED, () => { + notificationService.error({ + id: VIDEO_RENDER_FAILED_NOTIFICATION_ID, + title: NotificationTitle.VIDEO_RENDER_FAILED, + message: NotificationMessage.VIDEO_RENDER_FAILED, + }); + }); + }; + + handleLoadUserVideos(); + socket.on(AvatarVideoEvent.RENDER_SUCCESS, handleRenderedVideoSuccess); + socket.on(AvatarVideoEvent.RENDER_FAILED, handleRenderedVideoFailed); + + return () => { + socket.off( + AvatarVideoEvent.RENDER_SUCCESS, + handleRenderedVideoSuccess, + ); + socket.off( + AvatarVideoEvent.RENDER_FAILED, + handleRenderedVideoFailed, + ); + }; + }, [dispatch, socket]); return ( ; + onResizeStart?: () => void; + onResizeEnd?: () => void; }; -const Item: React.FC = ({ id, type, span, children, onClick }) => { +const Item: React.FC = ({ + id, + type, + span, + children, + onClick, + onResizeStart = (): void => {}, + onResizeEnd = (): void => {}, +}) => { const selectedItem = useAppSelector(({ studio }) => studio.ui.selectedItem); const { @@ -29,12 +40,14 @@ const Item: React.FC = ({ id, type, span, children, onClick }) => { id, span, data: { type }, + onResizeEnd, + onResizeStart, }); return ( { + const [isResizing, setIsResizing] = useState(false); const scenes = useAppSelector(({ studio }) => studio.scenes); const scenesWithSpan = useMemo(() => setItemsSpan(scenes), [scenes]); const { pixelsToValue } = useTimelineContext(); @@ -70,6 +72,14 @@ const ScenesRow: React.FC = () => { [dispatch], ); + const handleResizing = useCallback((): void => { + setIsResizing(true); + }, []); + + const handleResizingEnd = useCallback((): void => { + setIsResizing(false); + }, []); + return ( { type={RowNames.SCENE} {...item} onClick={handleItemClick} + onResizeEnd={handleResizingEnd} + onResizeStart={handleResizing} > { ))} - - - + {!isResizing && ( + + + + )} ); }; diff --git a/frontend/src/bundles/studio/components/timeline/timeline.tsx b/frontend/src/bundles/studio/components/timeline/timeline.tsx index 0f5a3628f..4dbf58f55 100644 --- a/frontend/src/bundles/studio/components/timeline/timeline.tsx +++ b/frontend/src/bundles/studio/components/timeline/timeline.tsx @@ -1,10 +1,12 @@ import { PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { type PlayerRef } from '@remotion/player'; +import { secondsToMilliseconds } from 'date-fns'; import { type DragEndEvent, type DragMoveEvent, type Range, type ResizeEndEvent, + type ResizeMoveEvent, TimelineContext, } from 'dnd-timeline'; import { type RefObject } from 'react'; @@ -14,7 +16,10 @@ import { useAppSelector, useCallback, } from '~/bundles/common/hooks/hooks.js'; -import { DND_ACTIVATION_DISTANCE_PIXELS } from '~/bundles/studio/constants/constants.js'; +import { + DND_ACTIVATION_DISTANCE_PIXELS, + MIN_SCENE_DURATION, +} from '~/bundles/studio/constants/constants.js'; import { RowNames } from '~/bundles/studio/enums/row-names.enum.js'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; import { type RowType } from '~/bundles/studio/types/types.js'; @@ -57,6 +62,42 @@ const Timeline: React.FC = ({ playerRef }) => { [dispatch], ); + const handleResizing = useCallback( + (event: ResizeMoveEvent): void => { + const activeItem = event.active.data.current; + const activeItemType = activeItem['type'] as RowType; + + const updatedSpan = activeItem.getSpanFromResizeEvent?.(event); + + if ( + !updatedSpan || + activeItemType === RowNames.SCRIPT || + activeItemType === RowNames.BUTTON + ) { + return; + } + + if ( + updatedSpan.end - updatedSpan.start < + secondsToMilliseconds(MIN_SCENE_DURATION) + ) { + updatedSpan.end = + updatedSpan.start + + secondsToMilliseconds(MIN_SCENE_DURATION); + + const activeItemId = event.active.id as string; + + dispatch( + studioActions.resizeScene({ + id: activeItemId, + span: updatedSpan, + }), + ); + } + }, + [dispatch], + ); + const handleDragMove = useCallback( (event: DragMoveEvent) => { const activeItem = event.active.data.current; @@ -149,6 +190,7 @@ const Timeline: React.FC = ({ playerRef }) => { { + const dispatch = useAppDispatch(); + const handleRemoveBackground = useCallback((): void => { + dispatch(studioActions.removeBackgroundFromScene()); + }, [dispatch]); + return ( <> @@ -24,6 +34,14 @@ const BackgroundsContent: React.FC = () => { + + None + + {backgroundImages.map((imageSource, index) => ( { + + None + + {backgroundColors.map((color, index) => ( ))} diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/color-card.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/color-card.tsx index 5ceaabb4a..f1680fef2 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/color-card.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/color-card.tsx @@ -2,6 +2,8 @@ import { Box } from '~/bundles/common/components/components.js'; import { useAppDispatch, useCallback } from '~/bundles/common/hooks/hooks.js'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; +import styles from './styles.module.css'; + type Properties = { color: string; }; @@ -20,8 +22,8 @@ const ColorCard: React.FC = ({ color }) => { return ( ); diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css index 4416b5f25..add5f9567 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css @@ -7,8 +7,21 @@ transition: all 0.3s ease; } +.color-item { + border-radius: 7px; + height: 80px; + border-width: 1px; + border-color: transparent; + transition: all 0.3s ease; +} + .image-item:hover { background-color: var(--chakra-colors-background-600); border-color: var(--chakra-colors-brand-secondary-300); - cursor: 'pointer'; + cursor: pointer; +} + +.color-item:hover { + border-color: var(--chakra-colors-brand-secondary-300); + cursor: pointer; } diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/styles.module.css b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/styles.module.css new file mode 100644 index 000000000..d20a3d960 --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/styles.module.css @@ -0,0 +1,15 @@ +.none-item { + background-color: var(--chakra-colors-background-700); + border-radius: 7px; + border-width: 1px; + border-color: transparent; + transition: all 0.3s ease; + align-items: center; + justify-content: center; +} + +.none-item:hover { + background-color: var(--chakra-colors-background-600); + border-color: var(--chakra-colors-brand-secondary-300); + cursor: pointer; +} diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx index 27a68719a..95e536bf2 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/script-content/components/script.tsx @@ -22,6 +22,8 @@ import { PlayIconNames } from '~/bundles/studio/enums/play-icon-names.enum.js'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; import { type ScriptWithIcon as ScriptT } from '~/bundles/studio/types/types.js'; +import styles from './styles.module.css'; + type Properties = ScriptT & { handleChangeVoice: (scriptId: string) => void }; const Script: React.FC = ({ @@ -141,6 +143,7 @@ const Script: React.FC = ({ w="full" > = ({ borderColor="background.600" /> ) { const script = { id: uuidv4(), - duration: MIN_SCRIPT_DURATION, + duration: DEFAULT_SCRIPT_DURATION, text: action.payload, voice: DEFAULT_VOICE, iconName: PlayIconNames.READY, @@ -194,7 +195,7 @@ const { reducer, actions, name } = createSlice({ addScene(state) { const scene = { id: uuidv4(), - duration: MIN_SCENE_DURATION, + duration: DEFAULT_SCENE_DURATION, }; state.ui.selectedItem = { id: scene.id, type: RowNames.SCENE }; state.scenes.push(scene); @@ -212,7 +213,11 @@ const { reducer, actions, name } = createSlice({ return item; } - const duration = millisecondsToSeconds(span.end - span.start); + let duration = millisecondsToSeconds(span.end - span.start); + + if (duration < MIN_SCENE_DURATION) { + duration = MIN_SCENE_DURATION; + } return { ...item, @@ -346,6 +351,23 @@ const { reducer, actions, name } = createSlice({ }; }); }, + removeBackgroundFromScene(state) { + const selectedItem = state.ui.selectedItem; + if (!selectedItem || selectedItem.type !== RowNames.SCENE) { + return; + } + + state.scenes = state.scenes.map((scene) => { + if (scene.id !== selectedItem.id) { + return scene; + } + + return { + ...scene, + background: {}, + }; + }); + }, setMenuActiveItem( state, action: PayloadAction | null>, diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index efb013cc0..adf818968 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -9,12 +9,16 @@ import { import { store } from '~/framework/store/store.js'; import { routes } from '~/routes/routes.js'; +import { socket, SocketContext } from './bundles/common/context/socket.js'; + createRoot(document.querySelector('#root') as HTMLElement).render( - - - + + + + + , ); diff --git a/package-lock.json b/package-lock.json index f0e370f0e..4654a6dd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "pino": "9.3.2", "pino-pretty": "10.3.1", "shared": "*", + "socket.io": "4.7.5", "swagger-jsdoc": "6.2.8", "tiktoken": "1.0.16" }, @@ -106,6 +107,7 @@ "react-router-dom": "6.26.0", "remotion": "4.0.201", "shared": "*", + "socket.io-client": "4.7.5", "zod-formik-adapter": "1.3.0" }, "devDependencies": { @@ -12187,6 +12189,11 @@ "node": ">=16.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -12300,6 +12307,19 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -13039,6 +13059,18 @@ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -13701,6 +13733,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -14427,6 +14467,18 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -15110,16 +15162,52 @@ "once": "^1.4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.13.0" + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" } }, "node_modules/env-paths": { @@ -19360,10 +19448,13 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } }, "node_modules/node-abi": { "version": "3.68.0", @@ -21912,6 +22003,78 @@ "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sonic-boom": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz", @@ -24100,6 +24263,14 @@ "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", @@ -24899,6 +25070,14 @@ "integrity": "sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==", "dev": true }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",