From a7a93f6391fbaaeabe80378db7ad132778158f9b Mon Sep 17 00:00:00 2001 From: Younes Date: Tue, 17 Dec 2024 17:27:34 +0100 Subject: [PATCH] fix: add emotion auto detection --- apps/frontend/package.json | 4 +- .../components/annotation/AnnotationForm.tsx | 11 +- .../components/annotation/AnnotationHints.tsx | 65 +++- .../components/annotation/AnnotationItem.tsx | 37 +- .../components/annotation/AnnotationPanel.tsx | 69 +++- .../annotation/ContextualAnnotations.tsx | 4 +- .../annotation/useAnnotationEditor.ts | 161 ++++++--- .../components/emotion-detection/dialog.tsx | 337 ++++++++++++++++++ .../src/components/emotion-detection/emoji.ts | 125 +++++++ .../emotion-palette.tsx | 165 ++------- .../src/components/emotion-detection/menu.tsx | 132 +++++++ .../src/components/emotion-detection/store.ts | 30 ++ .../src/components/project/SideBar.tsx | 3 +- .../src/components/project/VideoPlayer.tsx | 18 +- .../components/project/advanced-options.tsx | 39 -- .../src/components/project/rfd-document.tsx | 232 ++++++++---- .../src/components/project/useVideoPlayer.ts | 1 + apps/frontend/src/components/small-switch.tsx | 49 +++ apps/frontend/src/pages/about.tsx | 4 +- apps/frontend/src/server/env.ts | 1 + apps/frontend/src/server/main.ts | 5 +- package.json | 2 +- .../20241217155546_test/migration.sql | 16 + packages/prisma/schema.prisma | 3 + packages/prisma/src/index.ts | 1 - packages/trpc/src/routers/annotation.ts | 18 +- pnpm-lock.yaml | 177 +++++---- turbo.json | 1 + 28 files changed, 1306 insertions(+), 404 deletions(-) create mode 100644 apps/frontend/src/components/emotion-detection/dialog.tsx create mode 100644 apps/frontend/src/components/emotion-detection/emoji.ts rename apps/frontend/src/components/{annotation => emotion-detection}/emotion-palette.tsx (53%) create mode 100644 apps/frontend/src/components/emotion-detection/menu.tsx create mode 100644 apps/frontend/src/components/emotion-detection/store.ts delete mode 100644 apps/frontend/src/components/project/advanced-options.tsx create mode 100644 apps/frontend/src/components/small-switch.tsx create mode 100644 packages/prisma/migrations/20241217155546_test/migration.sql diff --git a/apps/frontend/package.json b/apps/frontend/package.json index a21c541d..2b185a71 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -49,6 +49,7 @@ "@trpc/server": "^10.45.2", "@types/linkify-urls": "^3.1.1", "@uidotdev/usehooks": "^2.4.1", + "@vladmandic/face-api": "^1.7.14", "adminjs": "^7.8.13", "better-auth": "^1.0.21", "change-case": "^4.1.2", @@ -94,7 +95,8 @@ "vite-express": "^0.19.0", "xml2js": "^0.6.2", "yup": "^1.4.0", - "yup-locales": "^1.2.28" + "yup-locales": "^1.2.28", + "zustand": "^5.0.2" }, "browserslist": { "production": [ diff --git a/apps/frontend/src/components/annotation/AnnotationForm.tsx b/apps/frontend/src/components/annotation/AnnotationForm.tsx index 0f2c5511..f0f73d86 100644 --- a/apps/frontend/src/components/annotation/AnnotationForm.tsx +++ b/apps/frontend/src/components/annotation/AnnotationForm.tsx @@ -27,8 +27,9 @@ import { useContextualEditorPosition, useContextualEditorVisibleState, useEditAnnotation, + useEmotionEditor, } from "./useAnnotationEditor"; -import { EmotionsPalette } from "./emotion-palette"; +import { EmotionsPalette } from "../emotion-detection/emotion-palette"; type AnnotationFormProps = { duration: number; @@ -99,7 +100,7 @@ export const AnnotationFormContent: React.FC< stopTime: editedAnnotation.stopTime, pause: editedAnnotation.pause, text: editedAnnotation.text, - emotion: editedAnnotation.extra?.emotion, + emotion: editedAnnotation.emotion, } : { startTime: videoProgress, @@ -136,6 +137,9 @@ export const AnnotationFormContent: React.FC< startTime: values.startTime, stopTime: values.stopTime, pause: values.pause, + emotion: values.emotion, + // mode: values.mode, + // detection: values.detection, extra: contextualEditorPosition ? contextualEditorPosition : {}, }); if (newAnnotation) { @@ -194,7 +198,7 @@ export const AnnotationFormContent: React.FC< value={formik.values.text} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.errors.text} + error={!!formik.errors.text} disabled={formik.isSubmitting} inputProps={{ "aria-label": "Saissez votre annotation", @@ -208,6 +212,7 @@ export const AnnotationFormContent: React.FC< projectId={project.id} semiAutoAnnotation={false} semiAutoAnnotationMe={false} + position={videoProgress} onEmotionChange={(emotion) => { formik.setFieldValue("emotion", emotion); }} diff --git a/apps/frontend/src/components/annotation/AnnotationHints.tsx b/apps/frontend/src/components/annotation/AnnotationHints.tsx index d2a039ee..2d226074 100644 --- a/apps/frontend/src/components/annotation/AnnotationHints.tsx +++ b/apps/frontend/src/components/annotation/AnnotationHints.tsx @@ -2,16 +2,20 @@ import CancelIcon from "@mui/icons-material/Clear"; import { Box, Fade, IconButton, Paper, Stack, Typography } from "@mui/material"; import { grey } from "@mui/material/colors"; import { styled } from "@mui/material/styles"; -import Tooltip, { tooltipClasses, TooltipProps } from "@mui/material/Tooltip"; +import Tooltip, { + tooltipClasses, + type TooltipProps, +} from "@mui/material/Tooltip"; import React from "react"; import { useTranslation } from "react-i18next"; import { Avatar } from "~components/Avatar"; import { MultiLineTypography } from "~components/MultiLineTypography"; -import { AnnotationByProjectId, ProjectById } from "~utils/trpc"; +import type { AnnotationByProjectId, ProjectById } from "~utils/trpc"; import { getUserColor } from "~utils/UserUtils"; import { useAnnotationHintsVisible } from "./useAnnotationEditor"; +import { getEmojiFromName } from "../emotion-detection/emoji"; interface AnnotationHintsProps { project: ProjectById; @@ -55,21 +59,48 @@ const AnnotationHintsItem: React.FC = ({ - - {annotation.user.initial} - - - + + + {annotation.user.initial} + + {annotation.emotion && ( + + {getEmojiFromName(annotation.emotion)} + + )} + + + {annotation.user.username} diff --git a/apps/frontend/src/components/annotation/AnnotationItem.tsx b/apps/frontend/src/components/annotation/AnnotationItem.tsx index 596ba38b..01c1f558 100644 --- a/apps/frontend/src/components/annotation/AnnotationItem.tsx +++ b/apps/frontend/src/components/annotation/AnnotationItem.tsx @@ -32,16 +32,17 @@ import { Avatar } from "~components/Avatar"; import { MultiLineTypography } from "~components/MultiLineTypography"; import { formatDuration } from "~utils/DurationUtils"; import { - AnnotationByProjectId, - AnnotationCommentByProjectId, - ProjectById, + type AnnotationByProjectId, + type AnnotationCommentByProjectId, + type ProjectById, trpc, - UserMe, + type UserMe, } from "~utils/trpc"; import { CommentForm } from "./CommentForm"; import { CommentItem } from "./CommentItem"; import { useEditAnnotation } from "./useAnnotationEditor"; +import { getEmojiFromName } from "../emotion-detection/emoji"; interface AnnotationItemProps { project: ProjectById; @@ -75,9 +76,10 @@ export const AnnotationItem: React.FC = ({ }, }); - const isContextual = Object.keys(annotation.extra || {}).length; + const isContextual = Object.keys(annotation.extra?.x || {}).length; - const canEdit = annotation.user.id == user?.id || project.user.id == user?.id; + const canEdit = + annotation.user.id === user?.id || project.user.id === user?.id; const handleDelete: React.MouseEventHandler = (event) => { event.stopPropagation(); @@ -143,7 +145,7 @@ export const AnnotationItem: React.FC = ({ alignContent: "center", }} > - + = ({ > {annotation.user.initial} + {annotation.emotion && ( + + + {getEmojiFromName(annotation.emotion)} + + + )} (({ theme }) => ({ "& .MuiBadge-badge": { @@ -165,6 +171,7 @@ const AnnotationList: React.FC< export const AnnotationPanel: React.FC = ({ annotationCount, playerIsReady, + annotations, ...props }) => { const [value, setValue] = useState("1"); @@ -180,6 +187,12 @@ export const AnnotationPanel: React.FC = ({ const [hintsVisible, setHintsVisible] = useAnnotationHintsVisible(); + const [onlyMine, setOnlyMine] = useState(false); + + const onlyMyAnnotations = onlyMine + ? annotations.filter((annotation) => annotation.user.id === props.user?.id) + : annotations; + return ( = ({ height: "100%", }} > - + + = ({ ); }; + +function AdvancedControls({ + onlyMine, + onOnlyMineChange, +}: { + onlyMine: boolean; + onOnlyMineChange: (onlyMine: boolean) => void; +}) { + const { mode, setMode } = usePlayerModeStore(); + + return ( + + + + onOnlyMineChange(!onlyMine)} + inputProps={{ "aria-label": "ant design" }} + /> + + Show Only Mine + + + + + setMode(mode === "performance" ? "analysis" : "performance") + } + inputProps={{ "aria-label": "ant design" }} + /> + + Performance Mode + + + + ); +} diff --git a/apps/frontend/src/components/annotation/ContextualAnnotations.tsx b/apps/frontend/src/components/annotation/ContextualAnnotations.tsx index 6e51b514..491f07b4 100644 --- a/apps/frontend/src/components/annotation/ContextualAnnotations.tsx +++ b/apps/frontend/src/components/annotation/ContextualAnnotations.tsx @@ -3,7 +3,7 @@ import { Avatar, Box, hexToRgb, Paper, Stack, Typography } from "@mui/material"; import React, { memo, useMemo, useRef } from "react"; import { MultiLineTypography } from "~components/MultiLineTypography"; -import { AnnotationByProjectId } from "~utils/trpc"; +import type { AnnotationByProjectId } from "~utils/trpc"; import { HtmlTooltip } from "./AnnotationHints"; @@ -152,7 +152,7 @@ export const ContextualAnnotations: React.FC = memo( () => annotations.filter((item) => { return ( - typeof item === "object" && Object.keys(item.extra || {}).length + typeof item === "object" && Object.keys(item.extra?.x || {}).length ); }), [annotations] diff --git a/apps/frontend/src/components/annotation/useAnnotationEditor.ts b/apps/frontend/src/components/annotation/useAnnotationEditor.ts index d14687c1..cc842764 100644 --- a/apps/frontend/src/components/annotation/useAnnotationEditor.ts +++ b/apps/frontend/src/components/annotation/useAnnotationEditor.ts @@ -1,114 +1,179 @@ -import { atom, DefaultValue, selector, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; - -import { AnnotationByProjectId } from "~utils/trpc"; - -type ContextualPosition = - { relativeX: number, relativeY: number, x: number, y: number, parentWidth: number, parentHeight: number } - +import { + atom, + DefaultValue, + selector, + useRecoilState, + useRecoilValue, + useSetRecoilState, +} from "recoil"; + +import type { AnnotationByProjectId } from "~utils/trpc"; + +type ContextualPosition = { + relativeX: number; + relativeY: number; + x: number; + y: number; + parentWidth: number; + parentHeight: number; +}; type AnnotationEditorState = { showHints: boolean; playerIsReady: boolean; contextualEditorVisible: boolean; contextualPosition?: ContextualPosition; + emotion?: string; formVisible: boolean; editedAnnotation?: AnnotationByProjectId; -} +}; const annotationEditorState = atom({ - key: 'contextualEditorState', // unique ID (with respect to other atoms/selectors) + key: "contextualEditorState", // unique ID (with respect to other atoms/selectors) default: { showHints: false, playerIsReady: false, contextualEditorVisible: false, formVisible: false, - editedAnnotation: undefined + editedAnnotation: undefined, }, // default value (aka initial value) }); - - const contextualEditorPosition = selector({ - key: 'contextualPosition', // unique ID (with respect to other atoms/selectors) + key: "contextualPosition", // unique ID (with respect to other atoms/selectors) get: ({ get }) => { const state = get(annotationEditorState); return state.contextualPosition ? state.contextualPosition : undefined; }, - set: ({ set }, newValue) => set(annotationEditorState, (previousState) => { - return { ...previousState, contextualPosition: newValue as ContextualPosition } - }), + set: ({ set }, newValue) => + set(annotationEditorState, (previousState) => { + return { + ...previousState, + contextualPosition: newValue as ContextualPosition, + }; + }), cachePolicy_UNSTABLE: { - eviction: 'most-recent', + eviction: "most-recent", }, }); - const annotationHintsVisible = selector({ - key: 'showHints', // unique ID (with respect to other atoms/selectors) + key: "showHints", // unique ID (with respect to other atoms/selectors) get: ({ get }) => { const state = get(annotationEditorState); return state.showHints; }, - set: ({ set }, newValue) => set(annotationEditorState, (previousState) => { - return { ...previousState, editedAnnotation: undefined, contextualPosition: undefined, contextualEditorVisible: false, formVisible: false, showHints: newValue as boolean } - }) + set: ({ set }, newValue) => + set(annotationEditorState, (previousState) => { + return { + ...previousState, + editedAnnotation: undefined, + contextualPosition: undefined, + contextualEditorVisible: false, + formVisible: false, + showHints: newValue as boolean, + }; + }), }); - const contextualEditorVisible = selector({ - key: 'contextualEditorVisible', // unique ID (with respect to other atoms/selectors) + key: "contextualEditorVisible", // unique ID (with respect to other atoms/selectors) get: ({ get }) => { const state = get(annotationEditorState); return state.contextualEditorVisible; }, - set: ({ set }, newValue) => set(annotationEditorState, (previousState) => { - return { ...previousState, showHints: false, contextualEditorVisible: newValue as boolean, contextualPosition: null } - }) + set: ({ set }, newValue) => + set(annotationEditorState, (previousState) => { + return { + ...previousState, + showHints: false, + contextualEditorVisible: newValue as boolean, + contextualPosition: null, + }; + }), }); - - - const annotationFormVisible = selector({ - key: 'annotationFormVisible', // unique ID (with respect to other atoms/selectors) + key: "annotationFormVisible", // unique ID (with respect to other atoms/selectors) get: ({ get }) => { const state = get(annotationEditorState); return state.formVisible; }, - set: ({ set }, newValue) => set(annotationEditorState, (previousState) => { - return { ...previousState, editedAnnotation: undefined, contextualPosition: undefined, contextualEditorVisible: false, showHints: false, formVisible: newValue as boolean } - }) + set: ({ set }, newValue) => + set(annotationEditorState, (previousState) => { + return { + ...previousState, + editedAnnotation: undefined, + contextualPosition: undefined, + contextualEditorVisible: false, + showHints: false, + formVisible: newValue as boolean, + }; + }), }); const editedAnnotation = selector({ - key: 'editedAnnotation', // unique ID (with respect to other atoms/selectors) + key: "editedAnnotation", // unique ID (with respect to other atoms/selectors) get: ({ get }) => { const state = get(annotationEditorState); return state.editedAnnotation; }, - set: ({ set }, newValue) => set(annotationEditorState, (previousState) => { - return { ...previousState, editedAnnotation: newValue as AnnotationByProjectId, contextualPosition: undefined, showHints: false, contextualEditorVisible: newValue != undefined && Object.keys((newValue as AnnotationByProjectId).extra || {}).length > 0, formVisible: newValue != undefined } - }) + set: ({ set }, newValue) => + set(annotationEditorState, (previousState) => { + return { + ...previousState, + editedAnnotation: newValue as AnnotationByProjectId, + contextualPosition: undefined, + showHints: false, + contextualEditorVisible: + newValue !== undefined && + Object.keys((newValue as AnnotationByProjectId).extra || {}).length > + 0, + formVisible: newValue !== undefined, + }; + }), }); +const emotionEditor = selector({ + key: "emotionEditor", // unique ID (with respect to other atoms/selectors) + get: ({ get }) => { + const state = get(annotationEditorState); + return state.emotion ? state.emotion : undefined; + }, + set: ({ set }, newValue) => + set(annotationEditorState, (previousState) => { + return { + ...previousState, + emotion: newValue as string, + }; + }), + cachePolicy_UNSTABLE: { + eviction: "most-recent", + }, +}); +export const useContextualEditorVisible = () => + useRecoilValue(contextualEditorVisible); +export const useContextualEditorVisibleState = () => + useRecoilState(contextualEditorVisible); -export const useContextualEditorVisible = () => useRecoilValue(contextualEditorVisible); -export const useContextualEditorVisibleState = () => useRecoilState(contextualEditorVisible); - -export const useContextualEditorPosition = () => useRecoilState(contextualEditorPosition); - - +export const useContextualEditorPosition = () => + useRecoilState(contextualEditorPosition); -export const useAnnotationHintsVisible = () => useRecoilState(annotationHintsVisible); +export const useEmotionEditor = () => useRecoilState(emotionEditor); -export const useSetAnnotationEditorState = () => useSetRecoilState(annotationEditorState); -export const useAnnotationEditorState = () => useRecoilValue(annotationEditorState); +export const useAnnotationHintsVisible = () => + useRecoilState(annotationHintsVisible); +export const useSetAnnotationEditorState = () => + useSetRecoilState(annotationEditorState); +export const useAnnotationEditorState = () => + useRecoilValue(annotationEditorState); -export const useAnnotationFormVisible = () => useRecoilState(annotationFormVisible); +export const useAnnotationFormVisible = () => + useRecoilState(annotationFormVisible); export const useEditAnnotation = () => useRecoilState(editedAnnotation); export const useEditAnnotationValue = () => useRecoilValue(editedAnnotation); diff --git a/apps/frontend/src/components/emotion-detection/dialog.tsx b/apps/frontend/src/components/emotion-detection/dialog.tsx new file mode 100644 index 00000000..fac1a580 --- /dev/null +++ b/apps/frontend/src/components/emotion-detection/dialog.tsx @@ -0,0 +1,337 @@ +import { useEffect, useRef, useState } from "react"; +// import * as faceapi from "@vladmandic/face-api"; +import { + Box, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + IconButton, + Link, + Stack, + Typography, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { useAutoDetectionStore } from "./store"; +import * as faceapi from "@vladmandic/face-api"; +import React from "react"; + +const modelPath = "https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/"; + +interface Props { + positionFloored: number; + // position: number; + playing: boolean; + projectId: string; + onEmotionDetectedChange(emotion: string): void; +} + +export const AutoDetectionDialog = React.memo( + ({ positionFloored, playing, projectId, onEmotionDetectedChange }: Props) => { + const videoRef = useRef(null); + const captureIntervalRef = useRef(null); + const startPositionRef = useRef(0); + // const annotations = useRef([]); + const [error, setError] = useState(null); + const videoRefTmp = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [isDetection, setIsDetection] = useState(false); + const [isOpen, setIsOpen] = useState(true); + + const setDetectedEmotion = useAutoDetectionStore( + (state) => state.setDetectedEmotion + ); + + const handleClose = () => setIsOpen(false); + + useEffect(() => { + startPositionRef.current = positionFloored; + }, [positionFloored]); + + function delay(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); + } + + const captureFrame = async () => { + if (videoRef.current) { + const video = videoRef.current; + + // Detect faces in the captured frame + const detections = await faceapi + .detectAllFaces( + video as faceapi.TNetInput, + new faceapi.TinyFaceDetectorOptions() + ) + // .detectAllFaces(video as faceapi.TNetInput, new faceapi.SsdMobilenetv1Options()) + .withFaceLandmarks() + .withFaceExpressions(); + + if (detections.length) { + setIsDetection(true); + + const emotion = Object.entries(detections[0]?.expressions).sort( + (a, b) => b[1] - a[1] + )[0][0]; + + setDetectedEmotion(emotion); + + // if ( + // annotations.current.length !== 0 && + // annotations.current[annotations.current.length - 1].emotion === + // emotion + // ) + // annotations.current[annotations.current.length - 1].stopTime = + // startPositionRef.current; + // else { + // if (annotations.current.length !== 0) { + // try { + // await AnnotationService.create( + // projectId, + // annotations.current[annotations.current.length - 1] + // ); + // } catch (e) { + // setError(e); + // } + + // annotations.current.pop(); + // } + + // const annotation: AnnotationData = { + // text: "", + // startTime: startPositionRef.current, + // stopTime: startPositionRef.current, + // pause: !playing, + // autoDetect: true, + // semiAutoAnnotation: false, + // semiAutoAnnotationMe: false, + // emotion, + // ontology: [], + // }; + + // annotations.current.push(annotation); + // } + } else { + setIsDetection(false); + onEmotionDetectedChange?.(""); + } + + await delay(100); + + requestAnimationFrame(() => captureFrame()); + } + }; + + useEffect(() => { + const loadModels = async () => { + setIsLoading(true); + + try { + await Promise.all([ + await faceapi.nets.tinyFaceDetector.loadFromUri(modelPath), + // await faceapi.nets.tinyYolov2.loadFromUri(modelPath), + // await faceapi.nets.ssdMobilenetv1.loadFromUri(modelPath), + await faceapi.nets.faceLandmark68Net.loadFromUri(modelPath), + await faceapi.nets.faceExpressionNet.loadFromUri(modelPath), + ]); + } catch (e) { + setError("Failed to load the models ..."); + } finally { + setIsLoading(false); + } + }; + + let stream: MediaStream | null = null; + + const startStream = async () => { + await loadModels(); + try { + stream = await navigator.mediaDevices.getUserMedia({ video: true }); + + if (stream && videoRef.current && videoRefTmp.current) { + videoRef.current.srcObject = stream; + videoRefTmp.current.srcObject = stream; + await videoRefTmp.current.play(); + videoRef.current.play().then(() => { + // if (videoRef.current) + // videoRef.current.currentTime = startPositionRef.current; + // captureIntervalRef.current = window.setInterval(captureFrame, 500); + captureFrame(); + }); + } + } catch (err) { + if ( + err.name === "PermissionDeniedError" || + err.name === "NotAllowedError" + ) + setError( + `Camera Error: camera permission denied: ${err.message || err}` + ); + if (err.name === "SourceUnavailableError") + setError( + `Camera Error: camera not available: ${err.message || err}` + ); + return null; + } + }; + + startStream(); + + return () => { + // console.log(annotations.current); + clearInterval(captureIntervalRef.current as number); + + // const pushLastAnnotation = async () => { + // if (annotations.current.length !== 0) { + // try { + // await AnnotationService.create( + // projectId, + // annotations.current[annotations.current.length - 1] + // ); + // } catch (e) { + // setError(e); + // } + + // annotations.current.pop(); + // } + // }; + + // pushLastAnnotation(); + + if (stream) { + const tracks = stream.getTracks(); + for (const track of tracks) { + track.stop(); + } + } + }; + }, []); + + return ( + <> + + {/* */} + {/* */} + {/*
{positionFloored} -- {position} +
*/} + + + + Auto Detection Mode + + ({ + position: "absolute", + right: 8, + top: 8, + color: theme.palette.grey[500], + })} + > + + + + + + + {isLoading && ( + + + + + + )} + + {/* + +
+ +
+
+ + + {isLoading && Loading ...} + + {!isLoading && isDetection && ( + + Detection OK + + )} + + + + + + + Click here + + + +
*/} + {!isLoading && !isDetection && ( + + + Detection failed, please follow the recommendations + + + + )} + + {error && ( + + {error} + + )} +
+ + + + +
+ + ); + } +); diff --git a/apps/frontend/src/components/emotion-detection/emoji.ts b/apps/frontend/src/components/emotion-detection/emoji.ts new file mode 100644 index 00000000..4a556120 --- /dev/null +++ b/apps/frontend/src/components/emotion-detection/emoji.ts @@ -0,0 +1,125 @@ +export interface Emoji { + label: string; + value: string; +} + +export interface EmotionRecommended { + emotion: string; + score: number; +} + +export const emojisArray: Emoji[] = [ + { + label: "👍", + value: "iLike", + }, + { + label: "👎", + value: "iDontLike", + }, + { + label: "😐", + value: "neutral", + }, + { + label: "😮", + value: "surprised", + }, + { + label: "😄", + value: "smile", + }, + { + label: "😂", + value: "laugh", + }, + { + label: "😠", + value: "angry", + }, + { + label: "☚ī¸", + value: "sad", + }, + { + label: "đŸĨš", + value: "empathy", + }, + { + label: "😨", + value: "fearful", + }, + { + label: "🤮", + value: "disgusted", + }, + { + label: "🤔", + value: "itsStrange", + }, +]; + +export const getEmojiFromName = (value: string): string => { + const emoji = emojisArray.find((emoji) => emoji.value === value); + return emoji?.label ?? ''; +}; + + + +export const mapEmotionToEmojis = (emotionDetected: string): Emoji[] => { + const emojis = (() => { + switch (emotionDetected) { + case "neutral": + return [emojisArray.find((emoji) => emoji.value === "neutral")]; + case "happy": + return [ + emojisArray.find((emoji) => emoji.value === "laugh"), + emojisArray.find((emoji) => emoji.value === "smile"), + ]; + case "surprised": + return [ + emojisArray.find((emoji) => emoji.value === "surprised"), + emojisArray.find((emoji) => emoji.value === "fearful"), + ]; + case "fearful": + return [ + emojisArray.find((emoji) => emoji.value === "surprised"), + emojisArray.find((emoji) => emoji.value === "fearful"), + ]; + case "angry": + return [ + emojisArray.find((emoji) => emoji.value === "angry"), + emojisArray.find((emoji) => emoji.value === "sad"), + emojisArray.find((emoji) => emoji.value === "disgusted"), + ]; + case "disgusted": + return [ + emojisArray.find((emoji) => emoji.value === "angry"), + emojisArray.find((emoji) => emoji.value === "sad"), + emojisArray.find((emoji) => emoji.value === "disgusted"), + ]; + case "sad": + return [ + emojisArray.find((emoji) => emoji.value === "angry"), + emojisArray.find((emoji) => emoji.value === "sad"), + emojisArray.find((emoji) => emoji.value === "disgusted"), + ]; + case "iLike": + return [emojisArray.find((emoji) => emoji.value === "neutral")]; + case "iDontLike": + return [emojisArray.find((emoji) => emoji.value === "iDontLike")]; + case "laugh": + return [emojisArray.find((emoji) => emoji.value === "laugh")]; + case "smile": + return [emojisArray.find((emoji) => emoji.value === "smile")]; + case "empathy": + return [emojisArray.find((emoji) => emoji.value === "empathy")]; + case "itsStrange": + return [emojisArray.find((emoji) => emoji.value === "itsStrange")]; + default: + return [emojisArray.find((emoji) => emoji.value === "neutral")]; + } + })().filter((item): item is Emoji => item !== undefined); + + return emojis; +}; diff --git a/apps/frontend/src/components/annotation/emotion-palette.tsx b/apps/frontend/src/components/emotion-detection/emotion-palette.tsx similarity index 53% rename from apps/frontend/src/components/annotation/emotion-palette.tsx rename to apps/frontend/src/components/emotion-detection/emotion-palette.tsx index cc69ae4b..fd7424c7 100644 --- a/apps/frontend/src/components/annotation/emotion-palette.tsx +++ b/apps/frontend/src/components/emotion-detection/emotion-palette.tsx @@ -1,69 +1,11 @@ -import { Box } from "@mui/material"; +import { Box, Typography } from "@mui/material"; import { useEffect, useState, useRef, type CSSProperties } from "react"; +import { emojisArray, type EmotionRecommended, type Emoji } from "./emoji"; +import { mapEmotionToEmojis } from "./emoji"; +import { useAutoDetectionStore } from "./store"; // import AnnotationService from 'services/AnnotationService'; -interface Emoji { - label: string; - value: string; -} - -interface EmotionRecommended { - emotion: string; - score: number; -} - -const emojisArray: Emoji[] = [ - { - label: "👍", - value: "iLike", - }, - { - label: "👎", - value: "iDontLike", - }, - { - label: "😐", - value: "neutral", - }, - { - label: "😮", - value: "surprised", - }, - { - label: "😄", - value: "smile", - }, - { - label: "😂", - value: "laugh", - }, - { - label: "😠", - value: "angry", - }, - { - label: "☚ī¸", - value: "sad", - }, - { - label: "đŸĨš", - value: "empathy", - }, - { - label: "😨", - value: "fearful", - }, - { - label: "🤮", - value: "disgusted", - }, - { - label: "🤔", - value: "itsStrange", - }, -]; - const OFFSET = 10; interface EmotionsPaletteProps { @@ -91,6 +33,10 @@ export function EmotionsPalette({ const captureIntervalRef = useRef(null); const startPositionRef = useRef(0); + const detectedEmotion = useAutoDetectionStore( + (state) => state.detectedEmotion + ); + // Update position ref useEffect(() => { startPositionRef.current = position; @@ -98,64 +44,6 @@ export function EmotionsPalette({ // Others Detection/No Detection useEffect(() => { - const mapEmotionToEmojis = (emotionDetected: string): Emoji[] => { - const emojis = (() => { - switch (emotionDetected) { - case "neutral": - return [emojisArray.find((emoji) => emoji.value === "neutral")]; - case "happy": - return [ - emojisArray.find((emoji) => emoji.value === "laugh"), - emojisArray.find((emoji) => emoji.value === "smile"), - ]; - case "surprised": - return [ - emojisArray.find((emoji) => emoji.value === "surprised"), - emojisArray.find((emoji) => emoji.value === "fearful"), - ]; - case "fearful": - return [ - emojisArray.find((emoji) => emoji.value === "surprised"), - emojisArray.find((emoji) => emoji.value === "fearful"), - ]; - case "angry": - return [ - emojisArray.find((emoji) => emoji.value === "angry"), - emojisArray.find((emoji) => emoji.value === "sad"), - emojisArray.find((emoji) => emoji.value === "disgusted"), - ]; - case "disgusted": - return [ - emojisArray.find((emoji) => emoji.value === "angry"), - emojisArray.find((emoji) => emoji.value === "sad"), - emojisArray.find((emoji) => emoji.value === "disgusted"), - ]; - case "sad": - return [ - emojisArray.find((emoji) => emoji.value === "angry"), - emojisArray.find((emoji) => emoji.value === "sad"), - emojisArray.find((emoji) => emoji.value === "disgusted"), - ]; - case "iLike": - return [emojisArray.find((emoji) => emoji.value === "neutral")]; - case "iDontLike": - return [emojisArray.find((emoji) => emoji.value === "iDontLike")]; - case "laugh": - return [emojisArray.find((emoji) => emoji.value === "laugh")]; - case "smile": - return [emojisArray.find((emoji) => emoji.value === "smile")]; - case "empathy": - return [emojisArray.find((emoji) => emoji.value === "empathy")]; - case "itsStrange": - return [emojisArray.find((emoji) => emoji.value === "itsStrange")]; - default: - return [emojisArray.find((emoji) => emoji.value === "neutral")]; - } - })().filter((item): item is Emoji => item !== undefined); - - return emojis; - }; - const generatePalette = (suggestions: EmotionRecommended[]): Emoji[] => { const emojisFlattened: Emoji[] = suggestions.flatMap( (emotionRecommended) => mapEmotionToEmojis(emotionRecommended.emotion) @@ -182,7 +70,14 @@ export function EmotionsPalette({ else startTimeParam = 0; } - const suggestions: EmotionRecommended[] = []; + const suggestions: EmotionRecommended[] = detectedEmotion + ? [ + { + emotion: detectedEmotion, + score: 1, + }, + ] + : []; // await AnnotationService.getRecommendedEmotions(projectId, { // onlyMe: semiAutoAnnotationMe, // startTime: startTimeParam, @@ -192,6 +87,8 @@ export function EmotionsPalette({ const palette: Emoji[] = generatePalette(suggestions); + console.log(palette); + if (!palette.length) { const neutralEmoji = emojisArray.find( (emoji) => emoji.value === "neutral" @@ -224,7 +121,7 @@ export function EmotionsPalette({ return () => { clearInterval(captureIntervalRef.current as number); }; - }, [semiAutoAnnotation, semiAutoAnnotationMe, projectId]); + }, [semiAutoAnnotation, semiAutoAnnotationMe, projectId, detectedEmotion]); // UI Code const handleHover = (index: number) => { @@ -258,25 +155,37 @@ export function EmotionsPalette({ height="2.5rem" > {emojis.map((emoji, index) => ( - // biome-ignore lint/a11y/useKeyWithClickEvents: -
handleHover(index)} onMouseLeave={handleHoverLeave} title={emoji.value} style={{ ...elementStyle, - transform: - hoveredComponent === index ? "translateY(-20%) scale(2)" : "", - backgroundColor: emotion === emoji.value ? "black" : "transparent", }} + sx={[ + { + transform: + hoveredComponent === index ? "translateY(-20%) scale(2)" : "", + }, + emotion === emoji.value + ? { + backgroundColor: "black", + borderColor: "primary.main", + borderWidth: 1, + borderStyle: "solid", + } + : { + backgroundColor: "transparent", + }, + ]} onClick={(_e) => { if (emoji.value !== emotion) onEmotionChange(emoji.value); else onEmotionChange(undefined); }} > {emoji.label} -
+ ))} ); diff --git a/apps/frontend/src/components/emotion-detection/menu.tsx b/apps/frontend/src/components/emotion-detection/menu.tsx new file mode 100644 index 00000000..8f17d977 --- /dev/null +++ b/apps/frontend/src/components/emotion-detection/menu.tsx @@ -0,0 +1,132 @@ +import * as React from "react"; +import { styled, alpha } from "@mui/material/styles"; +import Button from "@mui/material/Button"; +import Menu, { type MenuProps } from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Divider from "@mui/material/Divider"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import CameraAltIcon from "@mui/icons-material/CameraAlt"; +import StopCircleIcon from "@mui/icons-material/StopCircle"; +import PlayCircleFilledWhiteIcon from "@mui/icons-material/PlayCircleFilledWhite"; +import { + CircularProgress, + ListItemText, + MenuList, + Typography, +} from "@mui/material"; +import Check from "@mui/icons-material/Check"; +import { useAutoDetectionStore } from "../emotion-detection/store"; +import { AutoDetectionDialog } from "./dialog"; +import { grey } from "@mui/material/colors"; + +const StyledMenu = styled((props: MenuProps) => ( + +))(({ theme }) => ({ + "& .MuiPaper-root": { + borderRadius: 6, + marginTop: theme.spacing(1), + minWidth: 180, + borderColor: theme.palette.grey[800], + color: theme.palette.grey[300], + "& .MuiMenu-list": { + padding: "4px 0", + backgroundColor: theme.palette.background.dark, + }, + "& .MuiMenuItem-root": { + "& .MuiSvgIcon-root": { + fontSize: 16, + color: theme.palette.grey[300], + marginRight: theme.spacing(1.5), + }, + "&:active": { + backgroundColor: alpha( + theme.palette.primary.main, + theme.palette.action.selectedOpacity + ), + }, + }, + }, +})); + +export default function AutoDetectionMenu() { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + const { autoDetection, setAutoDetection } = useAutoDetectionStore(); + + const handleToggleAutoDetection = () => { + setAutoDetection(!autoDetection); + handleClose(); + }; + + return ( +
+ {autoDetection ? : null} + + + + + {autoDetection ? : } + + {autoDetection ? "Stop" : "Start"} + + + + Emoji Recommendation + + + From All + + + + Only Me + + + +
+ ); +} diff --git a/apps/frontend/src/components/emotion-detection/store.ts b/apps/frontend/src/components/emotion-detection/store.ts new file mode 100644 index 00000000..1821f6d2 --- /dev/null +++ b/apps/frontend/src/components/emotion-detection/store.ts @@ -0,0 +1,30 @@ +import { create } from 'zustand' + +interface AutoDetectionState { + isEnabled: boolean + setIsEnabled: (enabled: boolean) => void + autoDetection: boolean + setAutoDetection: (autoDetection: boolean) => void + detectedEmotion: string | null + setDetectedEmotion: (detectedEmotion: string | null) => void +} + +export const useAutoDetectionStore = create((set) => ({ + isEnabled: true, + setIsEnabled: (enabled) => set({ isEnabled: enabled }), + autoDetection: false, + setAutoDetection: (autoDetection) => set({ autoDetection, detectedEmotion: null }), + detectedEmotion: null, + setDetectedEmotion: (detectedEmotion) => set({ detectedEmotion }), +})) + + +interface PlayerModeState { + mode: "performance" | "analysis"; + setMode: (mode: "performance" | "analysis") => void; +} + +export const usePlayerModeStore = create((set) => ({ + mode: "analysis", + setMode: (mode) => set({ mode }), +})) diff --git a/apps/frontend/src/components/project/SideBar.tsx b/apps/frontend/src/components/project/SideBar.tsx index e2e8d4a4..a57e6bee 100644 --- a/apps/frontend/src/components/project/SideBar.tsx +++ b/apps/frontend/src/components/project/SideBar.tsx @@ -8,7 +8,7 @@ import { MemberListPanel } from "./MemberListPanel"; import { PlaylistSideBar } from "./PlaylistSideBar"; import { ProjectEditPanel } from "./ProjectEditPanel"; import { SharePanel } from "./SharePanel"; -import { AdvancedOptions } from "./advanced-options"; + interface SideBarProps { project: ProjectById; user?: UserMe; @@ -20,7 +20,6 @@ export const SideBar: React.FC = ({ project, user }) => { {user?.id === project.userId ? ( <> - ) : null} diff --git a/apps/frontend/src/components/project/VideoPlayer.tsx b/apps/frontend/src/components/project/VideoPlayer.tsx index 3be5a484..b2cd5a90 100644 --- a/apps/frontend/src/components/project/VideoPlayer.tsx +++ b/apps/frontend/src/components/project/VideoPlayer.tsx @@ -1,5 +1,5 @@ import ReactPlayer from "@celluloid/react-player"; -import { OnProgressProps } from "@celluloid/react-player/base"; +import type { OnProgressProps } from "@celluloid/react-player/base"; import * as React from "react"; import { forwardRef, @@ -15,6 +15,7 @@ import { } from "~hooks/use-video-player"; import { useSetVideoPlayerProgress } from "./useVideoPlayer"; +import { usePlayerModeStore } from "../emotion-detection/store"; interface VideoPlayerProps { url: string; @@ -28,6 +29,8 @@ export const VideoPlayer = forwardRef( const setVideoPlayerProgress = useSetVideoPlayerProgress(); + const playerMode = usePlayerModeStore((state) => state.mode); + useImperativeHandle(ref, () => playerRef.current); const dispatcher = useVideoPlayerEvent(); @@ -39,10 +42,10 @@ export const VideoPlayer = forwardRef( }); const handleReady = () => { - // dispatcher({ - // state: "READY", - // progress: 0, - // }); + dispatcher({ + state: "READY", + progress: 0, + }); }; const handlePlay = () => { @@ -99,6 +102,9 @@ export const VideoPlayer = forwardRef( url={url} height={height} width={"100%"} + style={{ + pointerEvents: playerMode === "performance" ? "none" : "auto", + }} config={{ peertube: { controls: 1, @@ -106,7 +112,7 @@ export const VideoPlayer = forwardRef( peertubeLink: 0, title: 0, warningTitle: 0, - p2p: 0, + p2p: 1, autoplay: 0, }, }} diff --git a/apps/frontend/src/components/project/advanced-options.tsx b/apps/frontend/src/components/project/advanced-options.tsx deleted file mode 100644 index 3c7f1f96..00000000 --- a/apps/frontend/src/components/project/advanced-options.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from "react"; -import Radio from "@mui/material/Radio"; -import RadioGroup from "@mui/material/RadioGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import FormControl from "@mui/material/FormControl"; -import FormLabel from "@mui/material/FormLabel"; -import { Paper } from "@mui/material"; - -export function AdvancedOptions() { - return ( - - - Mode - - } - label="Analyze" - /> - } - label="Performance" - /> - - - - ); -} diff --git a/apps/frontend/src/components/project/rfd-document.tsx b/apps/frontend/src/components/project/rfd-document.tsx index 1ae17fe4..d9cf0b75 100644 --- a/apps/frontend/src/components/project/rfd-document.tsx +++ b/apps/frontend/src/components/project/rfd-document.tsx @@ -1,15 +1,28 @@ import type React from "react"; import { useState, useEffect } from "react"; import { - Container, Typography, Box, TextField, Button, - Paper, Grid, Snackbar, Alert, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Select, + MenuItem, + FormControl, + InputLabel, } from "@mui/material"; import { parseString, Builder } from "xml2js"; @@ -56,30 +69,42 @@ export const RDFDocumentEditor: React.FC = () => { const [xmlContent, setXmlContent] = useState(""); const [openSnackbar, setOpenSnackbar] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(""); + const [openDialog, setOpenDialog] = useState(false); + const [newMetadata, setNewMetadata] = useState<{ + field: string; + value: string; + }>({ + field: "", + value: "", + }); + + // Add this constant for available metadata fields + const availableMetadataFields = [ + "title", + "creator", + "subject", + "description", + "publisher", + "contributor", + "date", + "type", + "format", + "identifier", + "source", + "language", + "relation", + "coverage", + "rights", + ]; // Load XML file on component mount useEffect(() => { - // Using the provided XML content const xmlDoc = ` - - - - - - - - - - - - - - - +test `; setXmlContent(xmlDoc); @@ -91,23 +116,23 @@ export const RDFDocumentEditor: React.FC = () => { return; } - const rdfData = result["rdf:RDF"]; + const rdfData = result?.["rdf:RDF"] || {}; const newMetadata: RDFMetadata = { - title: rdfData["dcterms:title"][0] || "", - creator: rdfData["dcterms:creator"][0] || "", - subject: rdfData["dcterms:subject"][0] || "", - description: rdfData["dcterms:description"][0] || "", - publisher: rdfData["dcterms:publisher"][0] || "", - contributor: rdfData["dcterms:contributor"][0] || "", - date: rdfData["dcterms:date"][0] || "", - type: rdfData["dcterms:type"][0] || "", - format: rdfData["dcterms:format"][0] || "", - identifier: rdfData["dcterms:identifier"][0] || "", - source: rdfData["dcterms:source"][0] || "", - language: rdfData["dcterms:language"][0] || "", - relation: rdfData["dcterms:relation"][0] || "", - coverage: rdfData["dcterms:coverage"][0] || "", - rights: rdfData["dcterms:rights"][0] || "", + title: rdfData["dcterms:title"]?.[0] ?? "", + creator: rdfData["dcterms:creator"]?.[0] ?? "", + subject: rdfData["dcterms:subject"]?.[0] ?? "", + description: rdfData["dcterms:description"]?.[0] ?? "", + publisher: rdfData["dcterms:publisher"]?.[0] ?? "", + contributor: rdfData["dcterms:contributor"]?.[0] ?? "", + date: rdfData["dcterms:date"]?.[0] ?? "", + type: rdfData["dcterms:type"]?.[0] ?? "", + format: rdfData["dcterms:format"]?.[0] ?? "", + identifier: rdfData["dcterms:identifier"]?.[0] ?? "", + source: rdfData["dcterms:source"]?.[0] ?? "", + language: rdfData["dcterms:language"]?.[0] ?? "", + relation: rdfData["dcterms:relation"]?.[0] ?? "", + coverage: rdfData["dcterms:coverage"]?.[0] ?? "", + rights: rdfData["dcterms:rights"]?.[0] ?? "", }; setMetadata(newMetadata); @@ -160,57 +185,122 @@ export const RDFDocumentEditor: React.FC = () => { setOpenSnackbar(true); }; - // Render metadata fields in a grid - const renderMetadataFields = () => { - const fields: (keyof RDFMetadata)[] = [ - "title", - "creator", - "subject", - "description", - "publisher", - "contributor", - "date", - "type", - "format", - "identifier", - "source", - "language", - "relation", - "coverage", - "rights", - ]; - - return fields.map((field) => ( - - handleMetadataChange(field, e.target.value)} - margin="normal" - /> - - )); + const handleAddMetadata = () => { + if (newMetadata.field && newMetadata.value) { + setMetadata((prev) => ({ + ...prev, + [newMetadata.field.toLowerCase()]: newMetadata.value, + })); + setNewMetadata({ field: "", value: "" }); + setOpenDialog(false); + } }; return ( - Metadata + Metadata Dublin - - {renderMetadataFields()} - + + + + + + Term + + + Value + + + + + {Object.entries(metadata) + .filter(([_, value]) => value !== "") // Only show non-empty fields + .map(([field, value]) => ( + + + {field.charAt(0).toUpperCase() + field.slice(1)} + + + + handleMetadataChange( + field as keyof RDFMetadata, + e.target.value + ) + } + size="small" + /> + + + ))} + +
+
+ - {/* Snackbar for save confirmation */} + {/* Modified Dialog */} + setOpenDialog(false)}> + Add New Metadata Field + + + Term + + + + setNewMetadata((prev) => ({ ...prev, value: e.target.value })) + } + margin="normal" + /> + + + + + + + + {/* Existing Snackbar */} ({ key: 'videoPlayerState', // unique ID (with respect to other atoms/selectors) diff --git a/apps/frontend/src/components/small-switch.tsx b/apps/frontend/src/components/small-switch.tsx new file mode 100644 index 00000000..eeb39523 --- /dev/null +++ b/apps/frontend/src/components/small-switch.tsx @@ -0,0 +1,49 @@ +import { styled } from "@mui/material/styles"; +import Switch from "@mui/material/Switch"; + +export const SmallSwitch = styled(Switch)(({ theme }) => ({ + width: 28, + height: 16, + padding: 0, + display: "flex", + "&:active": { + "& .MuiSwitch-thumb": { + width: 15, + }, + "& .MuiSwitch-switchBase.Mui-checked": { + transform: "translateX(9px)", + }, + }, + "& .MuiSwitch-switchBase": { + padding: 2, + "&.Mui-checked": { + transform: "translateX(12px)", + color: "#fff", + "& + .MuiSwitch-track": { + opacity: 1, + backgroundColor: "#1890ff", + ...theme.applyStyles("dark", { + backgroundColor: "#177ddc", + }), + }, + }, + }, + "& .MuiSwitch-thumb": { + boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", + width: 12, + height: 12, + borderRadius: 6, + transition: theme.transitions.create(["width"], { + duration: 200, + }), + }, + "& .MuiSwitch-track": { + borderRadius: 16 / 2, + opacity: 1, + backgroundColor: "rgba(0,0,0,.25)", + boxSizing: "border-box", + ...theme.applyStyles("dark", { + backgroundColor: "rgba(255,255,255,.35)", + }), + }, +})); diff --git a/apps/frontend/src/pages/about.tsx b/apps/frontend/src/pages/about.tsx index 8c72d4eb..21e10bd3 100644 --- a/apps/frontend/src/pages/about.tsx +++ b/apps/frontend/src/pages/about.tsx @@ -1,5 +1,5 @@ import { Box, Grid, Link, Typography } from "@mui/material"; -import * as React from "react"; +import type * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { OpenEditionLogo } from "~images/OpenEdition"; @@ -53,7 +53,7 @@ export const About: React.FC = () => { - +
- console.log("Server is listening on port 3000..."), +const server = ViteExpress.listen(app, env.PORT, () => + console.log(`Server is listening on port ${env.PORT}...`), ); diff --git a/package.json b/package.json index a3cb2d9f..e910006c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "packages/*" ], "scripts": { - "dev": "dotenv -- turbo watch dev --continue", + "dev": "dotenv -- turbo dev --continue", "clean": "turbo clean", "build": "dotenv -- turbo run build --no-cache", "eslint": "eslint --ext .js,.jsx,.ts,.tsx", diff --git a/packages/prisma/migrations/20241217155546_test/migration.sql b/packages/prisma/migrations/20241217155546_test/migration.sql new file mode 100644 index 00000000..cf4e29a2 --- /dev/null +++ b/packages/prisma/migrations/20241217155546_test/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - A unique constraint covering the columns `[token]` on the table `session` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "session_token_key"; + +-- AlterTable +ALTER TABLE "Annotation" ADD COLUMN "detection" TEXT, +ADD COLUMN "emotion" TEXT, +ADD COLUMN "mode" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "session_token_key" ON "session"("token"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 175c3233..fa892805 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -25,6 +25,9 @@ model Annotation { comments Comment[] orignalURL String? + emotion String? + detection String? // "auto" | "semi-auto" | "semi-auto-mine" + mode String? // "performance" | "analysis" } model Comment { diff --git a/packages/prisma/src/index.ts b/packages/prisma/src/index.ts index a480c3c8..b2f892cc 100644 --- a/packages/prisma/src/index.ts +++ b/packages/prisma/src/index.ts @@ -51,7 +51,6 @@ const prismaClient = new PrismaClient({ relativeY: extraObject.relativeY, parentWidth: extraObject.parentWidth, parentHeight: extraObject.parentHeight, - emotion: extraObject.emotion, } } return null diff --git a/packages/trpc/src/routers/annotation.ts b/packages/trpc/src/routers/annotation.ts index ccafab37..e9cbb2d2 100644 --- a/packages/trpc/src/routers/annotation.ts +++ b/packages/trpc/src/routers/annotation.ts @@ -93,7 +93,10 @@ export const annotationRouter = router({ stopTime: z.number(), pause: z.boolean(), projectId: z.string(), - extra: z.any() + extra: z.any(), + emotion: z.string().optional(), + mode: z.enum(["performance", "analysis"]).optional(), + detection: z.enum(["auto", "semi-auto", "semi-auto-mine"]).optional() }), ) .mutation(async ({ input, ctx }) => { @@ -106,7 +109,10 @@ export const annotationRouter = router({ stopTime: input.stopTime, pause: input.pause, projectId: input.projectId, - extra: input.extra + extra: input.extra, + emotion: input.emotion, + mode: input.mode, + detection: input.detection } // select: defaultPostSelect, }); @@ -124,7 +130,8 @@ export const annotationRouter = router({ stopTime: z.number().optional(), pause: z.boolean().optional(), projectId: z.string().optional(), - extra: z.any().optional() + extra: z.any().optional(), + emotion: z.string().optional(), }), ) .mutation(async ({ input, ctx }) => { @@ -156,7 +163,8 @@ export const annotationRouter = router({ stopTime: input.stopTime ?? annotation.stopTime, pause: input.pause ?? annotation.pause, projectId: input.projectId ?? annotation.projectId, - extra: input.extra ?? annotation.extra + extra: input.extra ?? annotation.extra, + emotion: input.emotion ?? annotation.emotion, }, }); @@ -234,6 +242,8 @@ export const annotationRouter = router({ comments: a.comments.map((c) => c.text), contextX: a.extra ? a.extra.relativeX : null, contextY: a.extra ? a.extra.relativeY : null, + emotion: a.emotion, + mode: a.mode, })) let content = ""; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39639f79..b37ee513 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,7 +111,7 @@ importers: version: 2.0.8 ts-jest: specifier: ^27.0.1 - version: 27.1.5(@babel/core@7.26.0)(@types/jest@26.0.24)(babel-jest@27.5.1(@babel/core@7.26.0))(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@20.5.1)(typescript@5.6.3)))(typescript@5.6.3) + version: 27.1.5(@babel/core@7.26.0)(@types/jest@26.0.24)(babel-jest@27.5.1(@babel/core@7.26.0))(esbuild@0.24.0)(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@20.5.1)(typescript@5.6.3)))(typescript@5.6.3) tsup: specifier: ^8.3.0 version: 8.3.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) @@ -211,6 +211,9 @@ importers: '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@vladmandic/face-api': + specifier: ^1.7.14 + version: 1.7.14 adminjs: specifier: ^7.8.13 version: 7.8.13(@types/babel__core@7.20.5)(@types/react-dom@18.3.1)(@types/react@18.3.12)(encoding@0.1.13) @@ -309,7 +312,7 @@ importers: version: 6.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13)))(eslint@8.57.1)(react@18.2.0)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3))(type-fest@2.19.0)(typescript@5.6.3) + version: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0))(esbuild@0.24.0)(eslint@8.57.1)(react@18.2.0)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3))(type-fest@2.19.0)(typescript@5.6.3) react-transition-group: specifier: ^2.3.1 version: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -349,6 +352,9 @@ importers: yup-locales: specifier: ^1.2.28 version: 1.2.28 + zustand: + specifier: ^5.0.2 + version: 5.0.2(@types/react@18.3.12)(immer@9.0.21)(react@18.2.0)(use-sync-external-store@1.2.2(react@18.2.0)) devDependencies: '@celluloid/config': specifier: workspace:* @@ -4547,6 +4553,10 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 + '@vladmandic/face-api@1.7.14': + resolution: {integrity: sha512-WTechvIQ+t7JS7ASQ2n1XaTCNSXQiqdTQmtWAuGrpClAIHIP18FVV66dPWDA8/0XIdotbWnzGjuS3WzybxVlJw==} + engines: {node: '>=14.0.0'} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -11583,6 +11593,24 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zustand@5.0.2: + resolution: {integrity: sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zx@8.2.4: resolution: {integrity: sha512-g9wVU+5+M+zVen/3IyAZfsZFmeqb6vDfjqFggakviz5uLK7OAejOirX+jeTOkyvAh/OYRlCgw+SdqzN7F61QVQ==} engines: {node: '>= 12.17.0'} @@ -14245,7 +14273,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@pmmmwh/react-refresh-webpack-plugin@0.5.15(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13)))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.15(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0))': dependencies: ansi-html: 0.0.9 core-js-pure: 3.39.0 @@ -14255,11 +14283,11 @@ snapshots: react-refresh: 0.11.0 schema-utils: 4.2.0 source-map: 0.7.4 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) optionalDependencies: - '@types/webpack': 5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13)) + '@types/webpack': 5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) type-fest: 2.19.0 - webpack-dev-server: 4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + webpack-dev-server: 4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) '@popperjs/core@2.11.8': {} @@ -15546,11 +15574,11 @@ snapshots: '@types/webpack-env@1.18.5': {} - '@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))': + '@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)': dependencies: '@types/node': 18.19.64 tapable: 2.2.1 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) transitivePeerDependencies: - '@swc/core' - esbuild @@ -15724,6 +15752,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@vladmandic/face-api@1.7.14': {} + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -16176,14 +16206,14 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@8.4.1(@babel/core@7.26.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + babel-loader@8.4.1(@babel/core@7.26.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@babel/core': 7.26.0 find-cache-dir: 3.3.2 loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) babel-plugin-istanbul@6.1.1: dependencies: @@ -16974,7 +17004,7 @@ snapshots: postcss: 8.4.47 postcss-selector-parser: 6.1.2 - css-loader@6.11.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + css-loader@6.11.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: icss-utils: 5.1.0(postcss@8.4.47) postcss: 8.4.47 @@ -16985,9 +17015,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) - css-minimizer-webpack-plugin@3.4.1(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + css-minimizer-webpack-plugin@3.4.1(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: cssnano: 5.1.15(postcss@8.4.47) jest-worker: 27.5.1 @@ -16995,7 +17025,9 @@ snapshots: schema-utils: 4.2.0 serialize-javascript: 6.0.2 source-map: 0.6.1 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) + optionalDependencies: + esbuild: 0.24.0 css-prefers-color-scheme@6.0.3(postcss@8.4.47): dependencies: @@ -17961,7 +17993,7 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-webpack-plugin@3.2.0(eslint@8.57.1)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + eslint-webpack-plugin@3.2.0(eslint@8.57.1)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@types/eslint': 8.56.12 eslint: 8.57.1 @@ -17969,7 +18001,7 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 schema-utils: 4.2.0 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) eslint@8.57.1: dependencies: @@ -18228,11 +18260,11 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + file-loader@6.2.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) file-saver@2.0.5: {} @@ -18316,7 +18348,7 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@babel/code-frame': 7.26.2 '@types/json-schema': 7.0.15 @@ -18332,7 +18364,7 @@ snapshots: semver: 7.6.3 tapable: 1.1.3 typescript: 5.6.3 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) optionalDependencies: eslint: 8.57.1 @@ -18700,7 +18732,7 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 - html-webpack-plugin@5.6.3(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + html-webpack-plugin@5.6.3(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -18708,7 +18740,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) htmlparser2@6.1.0: dependencies: @@ -20205,11 +20237,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + mini-css-extract-plugin@2.9.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: schema-utils: 4.2.0 tapable: 2.2.1 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) minimalistic-assert@1.0.1: {} @@ -21131,13 +21163,13 @@ snapshots: tsx: 4.19.2 yaml: 2.6.0 - postcss-loader@6.2.1(postcss@8.4.47)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + postcss-loader@6.2.1(postcss@8.4.47)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: cosmiconfig: 7.1.0 klona: 2.0.6 postcss: 8.4.47 semver: 7.6.3 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) postcss-logical@5.0.4(postcss@8.4.47): dependencies: @@ -21735,7 +21767,7 @@ snapshots: react-onclickoutside: 6.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dev-utils@12.0.1(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + react-dev-utils@12.0.1(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@babel/code-frame': 7.26.2 address: 1.2.2 @@ -21746,7 +21778,7 @@ snapshots: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -21761,7 +21793,7 @@ snapshots: shell-quote: 1.8.1 strip-ansi: 6.0.1 text-table: 0.2.0 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: @@ -21936,56 +21968,56 @@ snapshots: '@remix-run/router': 1.20.0 react: 18.3.1 - react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13)))(eslint@8.57.1)(react@18.2.0)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3))(type-fest@2.19.0)(typescript@5.6.3): + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0))(esbuild@0.24.0)(eslint@8.57.1)(react@18.2.0)(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3))(type-fest@2.19.0)(typescript@5.6.3): dependencies: '@babel/core': 7.26.0 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13)))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(@types/webpack@5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) '@svgr/webpack': 5.5.0 babel-jest: 27.5.1(@babel/core@7.26.0) - babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) babel-plugin-named-asset-import: 0.3.8(@babel/core@7.26.0) babel-preset-react-app: 10.0.1 bfj: 7.1.0 browserslist: 4.24.2 camelcase: 6.3.0 case-sensitive-paths-webpack-plugin: 2.4.0 - css-loader: 6.11.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) - css-minimizer-webpack-plugin: 3.4.1(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + css-loader: 6.11.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) + css-minimizer-webpack-plugin: 3.4.1(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 8.57.1 eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3)))(typescript@5.6.3) - eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) - file-loader: 6.2.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) + file-loader: 6.2.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) fs-extra: 10.1.0 - html-webpack-plugin: 5.6.3(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + html-webpack-plugin: 5.6.3(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) identity-obj-proxy: 3.0.0 jest: 27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3)) jest-resolve: 27.5.1 jest-watch-typeahead: 1.1.0(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3))) - mini-css-extract-plugin: 2.9.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + mini-css-extract-plugin: 2.9.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) postcss: 8.4.47 postcss-flexbugs-fixes: 5.0.2(postcss@8.4.47) - postcss-loader: 6.2.1(postcss@8.4.47)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + postcss-loader: 6.2.1(postcss@8.4.47)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) postcss-normalize: 10.0.1(browserslist@4.24.2)(postcss@8.4.47) postcss-preset-env: 7.8.3(postcss@8.4.47) prompts: 2.4.2 react: 18.2.0 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + react-dev-utils: 12.0.1(eslint@8.57.1)(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) react-refresh: 0.11.0 resolve: 1.22.8 resolve-url-loader: 4.0.0 - sass-loader: 12.6.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + sass-loader: 12.6.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) semver: 7.6.3 - source-map-loader: 3.0.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) - style-loader: 3.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + source-map-loader: 3.0.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) + style-loader: 3.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) tailwindcss: 3.4.14(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3)) - terser-webpack-plugin: 5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.13))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) - webpack-dev-server: 4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) - webpack-manifest-plugin: 4.1.1(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) - workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + terser-webpack-plugin: 5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) + webpack-dev-server: 4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) + webpack-manifest-plugin: 4.1.1(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) + workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) optionalDependencies: fsevents: 2.3.3 typescript: 5.6.3 @@ -22427,11 +22459,11 @@ snapshots: sanitize.css@13.0.0: {} - sass-loader@12.6.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + sass-loader@12.6.0(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: klona: 2.0.6 neo-async: 2.6.2 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) sax@1.2.4: {} @@ -22743,12 +22775,12 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@3.0.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + source-map-loader@3.0.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) source-map-support@0.5.21: dependencies: @@ -22989,9 +23021,9 @@ snapshots: strnum@1.0.5: {} - style-loader@3.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + style-loader@3.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) styled-components@5.3.9(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0): dependencies: @@ -23160,16 +23192,17 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.13))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + terser-webpack-plugin@5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) optionalDependencies: '@swc/core': 1.3.101(@swc/helpers@0.5.13) + esbuild: 0.24.0 terser@5.36.0: dependencies: @@ -23290,7 +23323,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@27.1.5(@babel/core@7.26.0)(@types/jest@26.0.24)(babel-jest@27.5.1(@babel/core@7.26.0))(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@20.5.1)(typescript@5.6.3)))(typescript@5.6.3): + ts-jest@27.1.5(@babel/core@7.26.0)(@types/jest@26.0.24)(babel-jest@27.5.1(@babel/core@7.26.0))(esbuild@0.24.0)(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@20.5.1)(typescript@5.6.3)))(typescript@5.6.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -23306,6 +23339,7 @@ snapshots: '@babel/core': 7.26.0 '@types/jest': 26.0.24 babel-jest: 27.5.1(@babel/core@7.26.0) + esbuild: 0.24.0 ts-node@10.9.2(@swc/core@1.3.101(@swc/helpers@0.5.13))(@types/node@16.18.119)(typescript@5.6.3): dependencies: @@ -23770,16 +23804,16 @@ snapshots: webidl-conversions@6.1.0: {} - webpack-dev-middleware@5.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + webpack-dev-middleware@5.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: colorette: 2.0.20 memfs: 3.5.3 mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.2.0 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) - webpack-dev-server@4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + webpack-dev-server@4.15.2(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -23809,20 +23843,20 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 5.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + webpack-dev-middleware: 5.3.4(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) ws: 8.18.0 optionalDependencies: - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) transitivePeerDependencies: - bufferutil - debug - supports-color - utf-8-validate - webpack-manifest-plugin@4.1.1(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + webpack-manifest-plugin@4.1.1(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: tapable: 2.2.1 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) webpack-sources: 2.3.1 webpack-sources@1.4.3: @@ -23837,7 +23871,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)): + webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -23859,7 +23893,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.13))(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))) + terser-webpack-plugin: 5.3.10(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)) watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -24070,12 +24104,12 @@ snapshots: workbox-sw@6.6.0: {} - workbox-webpack-plugin@6.6.0(@types/babel__core@7.20.5)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))): + workbox-webpack-plugin@6.6.0(@types/babel__core@7.20.5)(webpack@5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0)): dependencies: fast-json-stable-stringify: 2.1.0 pretty-bytes: 5.6.0 upath: 1.2.0 - webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13)) + webpack: 5.96.1(@swc/core@1.3.101(@swc/helpers@0.5.13))(esbuild@0.24.0) webpack-sources: 1.4.3 workbox-build: 6.6.0(@types/babel__core@7.20.5) transitivePeerDependencies: @@ -24192,6 +24226,13 @@ snapshots: zod@3.24.1: {} + zustand@5.0.2(@types/react@18.3.12)(immer@9.0.21)(react@18.2.0)(use-sync-external-store@1.2.2(react@18.2.0)): + optionalDependencies: + '@types/react': 18.3.12 + immer: 9.0.21 + react: 18.2.0 + use-sync-external-store: 1.2.2(react@18.2.0) + zx@8.2.4: optionalDependencies: '@types/fs-extra': 11.0.4 diff --git a/turbo.json b/turbo.json index 17018efe..501367f5 100644 --- a/turbo.json +++ b/turbo.json @@ -28,6 +28,7 @@ }, "globalEnv": [ "BASE_URL", + "PORT", "NODE_ENV", "DATABASE_URL", "REDIS_URL",