diff --git a/package-lock.json b/package-lock.json index f4da796a9..b461ffe9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mui/icons-material": "^5.14.12", "@mui/lab": "^5.0.0-alpha.147", "@mui/material": "^5.14.12", + "@mui/x-data-grid": "^7.9.0", "@mui/x-date-pickers": "^6.16.1", "@reduxjs/toolkit": "^1.9.7", "@types/chrome": "^0.0.266", @@ -1912,9 +1913,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3010,12 +3011,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", - "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.0.tgz", + "integrity": "sha512-sYpubkO1MZOnxNyVOClrPNOTs0MfuRVVnAvCeMaOaXt6GimgQbnUcshYv2pSr6PFj+Mqzdff/FYOBceK8u5QgA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.14", + "@mui/utils": "^5.16.0", "prop-types": "^15.8.1" }, "engines": { @@ -3067,15 +3068,15 @@ } }, "node_modules/@mui/system": { - "version": "5.15.15", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", - "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.0.tgz", + "integrity": "sha512-9YbkC2m3+pNumAvubYv+ijLtog6puJ0fJ6rYfzfLCM47pWrw3m+30nXNM8zMgDaKL6vpfWJcCXm+LPaWBpy7sw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.14", + "@mui/private-theming": "^5.16.0", "@mui/styled-engine": "^5.15.14", "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", + "@mui/utils": "^5.16.0", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -3119,9 +3120,9 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.0.tgz", + "integrity": "sha512-kLLi5J1xY+mwtUlMb8Ubdxf4qFAA1+U7WPBvjM/qQ4CIwLCohNb0sHo1oYPufjSIH/Z9+dhVxD7dJlfGjd1AVA==", "dependencies": { "@babel/runtime": "^7.23.9", "@types/prop-types": "^15.7.11", @@ -3145,6 +3146,32 @@ } } }, + "node_modules/@mui/x-data-grid": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.9.0.tgz", + "integrity": "sha512-RkrVD+tfcR/h3j2p2uqohxA00C5tCJIV5gb5+2ap8XdM0Y8XMF81bB8UADWenU5W83UTErWvtU7n4gCl7hJO9g==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@mui/system": "^5.16.0", + "@mui/utils": "^5.16.0", + "@mui/x-internals": "7.9.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.15.14", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@mui/x-date-pickers": { "version": "6.19.9", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.19.9.tgz", @@ -3210,6 +3237,25 @@ } } }, + "node_modules/@mui/x-internals": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.9.0.tgz", + "integrity": "sha512-RJRrM6moaDZ8S11gDt8OKVclKm2v9khpIyLkpenNze+tT4dQYoU3liW5P2t31hA4Na/T6JQKNosB4qmB2TYfZw==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@mui/utils": "^5.16.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index b58cafab7..1f65a0300 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@mui/icons-material": "^5.14.12", "@mui/lab": "^5.0.0-alpha.147", "@mui/material": "^5.14.12", + "@mui/x-data-grid": "^7.9.0", "@mui/x-date-pickers": "^6.16.1", "@reduxjs/toolkit": "^1.9.7", "@types/chrome": "^0.0.266", diff --git a/src/components/NavItems/Assistant/AssistantApiHandlers/useAssistantApi.jsx b/src/components/NavItems/Assistant/AssistantApiHandlers/useAssistantApi.jsx index b4684f839..1c7917228 100644 --- a/src/components/NavItems/Assistant/AssistantApiHandlers/useAssistantApi.jsx +++ b/src/components/NavItems/Assistant/AssistantApiHandlers/useAssistantApi.jsx @@ -166,6 +166,96 @@ export default function assistantApiCalls() { ); }; + const MAX_NUM_RETRIES = 3; + + /** + * Calls an async function that throws an exception when it fails, will retry for numMaxRetries + * @param numMaxRetries Number of times the function will be retried + * @param asyncFunc The async function to call + * @param errorFunc Called when asyncFunc throws an error when there are additional retries + * @returns {Promise<*>} Output of asyncFunc + */ + async function callAsyncWithNumRetries( + numMaxRetries, + asyncFunc, + errorFunc = null, + ) { + for (let retryCount = 0; retryCount < numMaxRetries; retryCount++) { + try { + return await asyncFunc(); + } catch (e) { + if (retryCount + 1 >= MAX_NUM_RETRIES) { + throw e; + } else { + if (errorFunc) errorFunc(retryCount, e); + } + } + } + } + + const callNewsFramingService = async (text) => { + return await callAsyncWithNumRetries( + MAX_NUM_RETRIES, + async () => { + const result = await axios.post( + assistantEndpoint + "gcloud/news-framing-clfr", + { text: text }, + ); + return result.data; + }, + (numTries) => { + console.log( + "Could not connect to news framing service, tries " + + (numTries + 1) + + "/" + + MAX_NUM_RETRIES, + ); + }, + ); + }; + + const callNewsGenreService = async (text) => { + return await callAsyncWithNumRetries( + MAX_NUM_RETRIES, + async () => { + const result = await axios.post( + assistantEndpoint + "gcloud/news-genre-clfr", + { text: text }, + ); + return result.data; + }, + (numTries) => { + console.log( + "Could not connect to news genre service, tries " + + (numTries + 1) + + "/" + + MAX_NUM_RETRIES, + ); + }, + ); + }; + + const callPersuasionService = async (text) => { + return await callAsyncWithNumRetries( + MAX_NUM_RETRIES, + async () => { + const result = await axios.post( + assistantEndpoint + "gcloud/persuasion-span-clfr", + { text: text }, + ); + return result.data; + }, + (numTries) => { + console.log( + "Could not connect to persuasion service, tries " + + (numTries + 1) + + "/" + + MAX_NUM_RETRIES, + ); + }, + ); + }; + const callOcrScriptService = async () => { const result = await axios.get(assistantEndpoint + "gcloud/ocr-scripts"); return result.data; diff --git a/src/components/NavItems/Assistant/AssistantScrapeResults/assistantUtils.jsx b/src/components/NavItems/Assistant/AssistantScrapeResults/assistantUtils.jsx index 46b23403d..481d82d64 100644 --- a/src/components/NavItems/Assistant/AssistantScrapeResults/assistantUtils.jsx +++ b/src/components/NavItems/Assistant/AssistantScrapeResults/assistantUtils.jsx @@ -85,34 +85,34 @@ function treeMapToElementsRecursive( if ("span" in treeElem) { const span = treeElem.span; if (spanHighlightIndices === null) { - // console.log("No span highlight: ", text.substring(span.start, span.end)); + console.log("No span highlight: ", text.substring(span.start, span.end)); childElems.push(text.substring(span.start, span.end)); } else { - // console.log("Span highlight: ", text.substring(span.start, span.end)); + console.log("Span highlight: ", text.substring(span.start, span.end)); let currentIndex = span.start; for (let i = 0; i < spanHighlightIndices.length; i++) { const hSpan = spanHighlightIndices[i]; - // console.log( - // "Matching span", - // span.start, - // span.end, - // hSpan.indices[0], - // hSpan.indices[1], - // ); + console.log( + "Matching span", + span.start, + span.end, + hSpan.indices[0], + hSpan.indices[1], + ); const hSpanStart = hSpan.indices[0]; const hSpanEnd = hSpan.indices[1]; if ( (span.start <= hSpanStart && hSpanStart <= span.end) || (span.start <= hSpanEnd && hSpanEnd <= span.end) ) { - // //If there's an overlap - // console.log( - // "Found lapping span ", - // span.start, - // span.end, - // hSpanStart, - // hSpanEnd, - // ); + //If there's an overlap + console.log( + "Found lapping span ", + span.start, + span.end, + hSpanStart, + hSpanEnd, + ); // If span doesn't start before the current index if (hSpanStart > currentIndex) { @@ -123,7 +123,7 @@ function treeMapToElementsRecursive( hSpanStart < span.start ? span.start : hSpanStart; const boundedEnd = hSpanEnd > span.end ? span.end : hSpanEnd; if (wrapFunc) { - // console.log("Wrapping: ", text.substring(boundedStart, boundedEnd)); + console.log("Wrapping: ", text.substring(boundedStart, boundedEnd)); childElems.push( wrapFunc( text.substring(boundedStart, boundedEnd), @@ -161,7 +161,6 @@ function treeMapToElementsRecursive( ), ); } - return React.createElement(treeElem.tag, null, childElems); } diff --git a/src/components/NavItems/tools/Alltools/ToolsMenu.jsx b/src/components/NavItems/tools/Alltools/ToolsMenu.jsx index c45e21b59..37da4c8fe 100644 --- a/src/components/NavItems/tools/Alltools/ToolsMenu.jsx +++ b/src/components/NavItems/tools/Alltools/ToolsMenu.jsx @@ -30,7 +30,8 @@ import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import { i18nLoadNamespace } from "components/Shared/Languages/i18nLoadNamespace"; import { Audiotrack } from "@mui/icons-material"; -import { ROLES, tools, TOOLS_CATEGORIES } from "../../../../constants/tools"; +import { tools, TOOLS_CATEGORIES } from "../../../../constants/tools"; +import { ROLES } from "../../../../constants/roles"; function TabPanel(props) { const { children, value, index, ...other } = props; diff --git a/src/components/NavItems/tools/SyntheticImageDetection/NddDatagrid.jsx b/src/components/NavItems/tools/SyntheticImageDetection/NddDatagrid.jsx new file mode 100644 index 000000000..0755567b9 --- /dev/null +++ b/src/components/NavItems/tools/SyntheticImageDetection/NddDatagrid.jsx @@ -0,0 +1,257 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import { DataGrid, GridActionsCellItem } from "@mui/x-data-grid"; +import Link from "@mui/material/Link"; +import { Chip, Grid, Stack, Typography } from "@mui/material"; +import { i18nLoadNamespace } from "../../../Shared/Languages/i18nLoadNamespace"; +import { + getAlertColor, + getAlertLabel, + getPercentageColorCode, +} from "./syntheticImageDetectionResults"; +import { OpenInNew } from "@mui/icons-material"; +import { useSelector } from "react-redux"; +import { createTheme, ThemeProvider } from "@mui/material/styles"; + +import { + arSD, + deDE, + elGR, + enUS, + esES, + frFR, + itIT, +} from "@mui/x-data-grid/locales"; + +const languages = { + en: enUS, + fr: frFR, + es: esES, + el: elGR, + it: itIT, + ar: arSD, + de: deDE, +}; + +const NddDataGrid = ({ rows }) => { + const keyword = i18nLoadNamespace( + "components/NavItems/tools/SyntheticImageDetection", + ); + + const currentLang = useSelector((state) => state.language); + const isCurrentLanguageLeftToRight = currentLang !== "ar"; + + //Retrieves the localization for the Datagrid + const datagridLanguage = languages[currentLang] || languages["en"]; + + const detectionRateStack = (row, index) => { + if (!row.detectionResults[index]) return null; + + const detectionPercentageNdImage = + row.detectionResults[index].predictionScore; + + return ( + + + <> + + {keyword("synthetic_image_detection_probability_text")}{" "} + + + {detectionPercentageNdImage}% + + + + + + ); + }; + + const imageUrlsCell = (urls) => { + if (!urls || !Array.isArray(urls) || urls.length === 0) return <>; + + return ( + + {urls.map((url, index) => ( + + + {`#${index + 1}`} + + + ))} + + ); + }; + + /** + * Computes the JSX element to display for the algorithm name cell + * @param params + * @param index {number} The array position + * @returns {React.JSX.Element|null} The JSX element if applicable else null + */ + const renderAlgorithmName = (params, index) => { + if (!params.row.detectionResults[index]) { + return null; + } + + return ( + + {keyword(params.row.detectionResults[index].name)} + + ); + }; + + /** + * A helper function to compute the detection rows as not all the NDD results have the same number of algorithms for detections + * @returns {*[]} + */ + const detectionDetailsRows = () => { + const maxSizeAlgorithm = Math.max( + ...rows.map((row) => row.detectionResults.length), + ); + + let algorithmsRows = []; + + for (let i = 0; i < maxSizeAlgorithm; i++) { + algorithmsRows.push({ + field: `detectionName${i + 1}`, + headerName: `${keyword( + "synthetic_image_detection_ndd_table_header_3", + )}${i + 1}`, + type: "string", + minWidth: 120, + valueGetter: (value, row) => row?.detectionResults[i]?.name, + renderCell: (params) => renderAlgorithmName(params, i), + }); + + algorithmsRows.push({ + field: `detectionRate${i + 1}`, + headerName: `${keyword( + "synthetic_image_detection_ndd_table_header_4", + )}${i + 1}`, + type: "number", + minWidth: 180, + valueGetter: (value, row) => row?.detectionResults[i]?.predictionScore, + renderCell: (params) => detectionRateStack(params.row, i), + }); + } + + return algorithmsRows; + }; + + const columns = [ + { + field: "id", + headerName: keyword("synthetic_image_detection_ndd_table_header_1"), + width: 10, + }, + { + field: "image", + headerName: keyword("synthetic_image_detection_ndd_table_header_2"), + minWidth: 150, + sortable: false, + renderCell: (params) => , + }, + ...detectionDetailsRows(), + { + field: "archiveUrl", + headerName: keyword("synthetic_image_detection_ndd_table_header_5"), + sortable: false, + renderCell: (params) => ( + + {params.value} + + ), + }, + { + field: "imageUrls", + headerName: keyword("synthetic_image_detection_ndd_table_header_6"), + sortable: false, + renderCell: (params) => imageUrlsCell(params.value), + }, + { + headerName: keyword("synthetic_image_detection_ndd_table_header_7"), + field: "actions", + type: "actions", + width: 120, + getActions: (params) => { + const url = new URL( + window.location.href + "?url=" + params.row.archiveUrl, + ); + return [ + // eslint-disable-next-line react/jsx-key + } + onClick={() => window.open(url, "_blank", "noopener noreferrer")} + label="Open Analysis" + />, + ]; + }, + }, + ]; + + /* This is needed to fix the pagination arrows for RTL languages + * See https://mui.com/x/react-data-grid/localization/#rtl-support + */ + const theme = createTheme( + { + direction: isCurrentLanguageLeftToRight ? "ltr" : "rtl", + palette: { + primary: { + light: "#00926c", + main: "#00926c", + dark: "#00926c", + contrastText: "#fff", + }, + }, + }, + datagridLanguage, + ); + + return ( + + + "auto"} + rows={rows} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 5, + }, + }, + }} + autosizeOnMount={true} + autosizeOptions={{ + includeHeaders: true, + includeOutliers: true, + expand: true, + outliersFactor: 20, + }} + pageSizeOptions={[5]} + disableRowSelectionOnClick + /> + + + ); +}; + +export default NddDataGrid; diff --git a/src/components/NavItems/tools/SyntheticImageDetection/SyntheticImageDetectionAlgorithms.jsx b/src/components/NavItems/tools/SyntheticImageDetection/SyntheticImageDetectionAlgorithms.jsx new file mode 100644 index 000000000..7bd1ec673 --- /dev/null +++ b/src/components/NavItems/tools/SyntheticImageDetection/SyntheticImageDetectionAlgorithms.jsx @@ -0,0 +1,139 @@ +import { ROLES } from "../../../../constants/roles"; + +/** + * @file Provides constants and helper functions used for the synthetic image detection tool + * + */ + +/** + * Thresholds (percentage) for the different analysis categories + * @type {{THRESHOLD_2: number, THRESHOLD_3: number, THRESHOLD_1: number}} + */ +export const DETECTION_THRESHOLDS = { + THRESHOLD_1: 50, + THRESHOLD_2: 70, + THRESHOLD_3: 90, +}; + +export class SyntheticImageDetectionAlgorithm { + /** + * + * @param apiServiceName {string} The service parameter for the API call + * @param name {string} The algorithm name key + * @param description {string} The algorithm description key + * @param roleNeeded {?Roles} Role needed to get the detection results for the algorithm + */ + constructor(apiServiceName, name, description, roleNeeded) { + this.apiServiceName = apiServiceName; + this.name = name; + this.description = description; + this.roleNeeded = roleNeeded; + } +} + +export const ganR50Mever = new SyntheticImageDetectionAlgorithm( + "gan_r50_mever", + "synthetic_image_detection_gan_name", + "synthetic_image_detection_gan_description", + ROLES.BETA_TESTER, +); + +export const proGanR50Grip = new SyntheticImageDetectionAlgorithm( + "progan_r50_grip", + "synthetic_image_detection_progan_name", + "synthetic_image_detection_progan_description", + ROLES.BETA_TESTER, +); + +export const ldmR50Grip = new SyntheticImageDetectionAlgorithm( + "ldm_r50_grip", + "synthetic_image_detection_diffusion_name", + "synthetic_image_detection_diffusion_description", + ROLES.BETA_TESTER, +); + +export const proGanWebpR50Grip = new SyntheticImageDetectionAlgorithm( + "progan-webp_r50_grip", + "synthetic_image_detection_progan-webp_r50_grip_name", + "synthetic_image_detection_progan-webp_r50_grip_description", + ROLES.EXTRA_FEATURE, +); + +export const ldmWebpR50Grip = new SyntheticImageDetectionAlgorithm( + "ldm-webp_r50_grip", + "synthetic_image_detection_ldm-webp_r50_grip_name", + "synthetic_image_detection_ldm-webp_r50_grip_description", + ROLES.EXTRA_FEATURE, +); + +export const gigaGanWebpR50Grip = new SyntheticImageDetectionAlgorithm( + "gigagan-webp_r50_grip", + "synthetic_image_detection_gigagan-webp_r50_grip_name", + "synthetic_image_detection_gigagan-webp_r50_grip_description", + ROLES.EXTRA_FEATURE, +); + +export const admR50Grip = new SyntheticImageDetectionAlgorithm( + "adm_r50_grip", + "synthetic_image_detection_adm_name", + "synthetic_image_detection_adm_description", + ROLES.EXTRA_FEATURE, +); +export const proGanRineMever = new SyntheticImageDetectionAlgorithm( + "progan_rine_mever", + "synthetic_image_detection_progan_rine_mever_name", + "synthetic_image_detection_progan_rine_mever_description", + ROLES.EXTRA_FEATURE, +); +export const ldmRineMever = new SyntheticImageDetectionAlgorithm( + "ldm_rine_mever", + "synthetic_image_detection_ldm_rine_mever_name", + "synthetic_image_detection_ldm_rine_mever_description", + ROLES.EXTRA_FEATURE, +); +export const ldmR50Mever = new SyntheticImageDetectionAlgorithm( + "ldm_r50_mever", + "synthetic_image_detection_ldm_r50_mever_name", + "synthetic_image_detection_ldm_r50_mever_description", + ROLES.EXTRA_FEATURE, +); + +/** + * The list of the synthetic image detection algorithms + * TODO:Use SET + * @type {SyntheticImageDetectionAlgorithm[]} + */ +export const syntheticImageDetectionAlgorithms = [ + proGanR50Grip, + ldmR50Grip, + admR50Grip, + proGanWebpR50Grip, + ldmWebpR50Grip, + gigaGanWebpR50Grip, + ganR50Mever, + ldmR50Mever, + proGanRineMever, + ldmRineMever, +]; + +/** + * Returns a list of algorithms that can be used by a user according to their roles + * @param roles {?Array} + * @returns {SyntheticImageDetectionAlgorithm[]} + */ +export const getSyntheticImageDetectionAlgorithmsForRoles = (roles) => { + return syntheticImageDetectionAlgorithms.filter((algorithm) => + roles.includes(algorithm.roleNeeded), + ); +}; + +/** + * Returns the Synthetic Image Detection Algorithm Object from the given APi Service Name + * @param apiName {string} + * @returns {SyntheticImageDetectionAlgorithm} + */ +export const getSyntheticImageDetectionAlgorithmFromApiName = (apiName) => { + return syntheticImageDetectionAlgorithms.find( + (algorithm) => algorithm.apiServiceName === apiName, + ); +}; diff --git a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx index b5796b00e..a4c922cff 100644 --- a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx +++ b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx @@ -1,8 +1,9 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { resetSyntheticImageDetectionImage, setSyntheticImageDetectionLoading, + setSyntheticImageDetectionNearDuplicates, setSyntheticImageDetectionResult, } from "../../../../redux/actions/tools/syntheticImageDetectionActions"; @@ -28,8 +29,14 @@ import SyntheticImageDetectionResults from "./syntheticImageDetectionResults"; import { setError } from "redux/reducers/errorReducer"; import StringFileUploadField from "../../../Shared/StringFileUploadField"; import { preprocessFileUpload } from "../../../Shared/Utils/fileUtils"; +import { syntheticImageDetectionAlgorithms } from "./SyntheticImageDetectionAlgorithms"; +import { useLocation } from "react-router-dom"; const SyntheticImageDetection = () => { + const location = useLocation(); + const urlParams = new URLSearchParams(location.search); + const urlParam = urlParams.get("url"); + const classes = useMyStyles(); const keyword = i18nLoadNamespace( "components/NavItems/tools/SyntheticImageDetection", @@ -46,6 +53,7 @@ const SyntheticImageDetection = () => { ); const result = useSelector((state) => state.syntheticImageDetection.result); const url = useSelector((state) => state.syntheticImageDetection.url); + const nd = useSelector((state) => state.syntheticImageDetection.duplicates); const [input, setInput] = useState(url ? url : ""); const [imageFile, setImageFile] = useState(undefined); @@ -69,8 +77,10 @@ const SyntheticImageDetection = () => { dispatch(setSyntheticImageDetectionLoading(true)); const modeURL = "images/"; - const services = - "gan_r50_mever,ldm_r50_grip,progan_r50_grip,adm_r50_grip,ldm_r50_mever,progan_rine_mever,ldm_rine_mever"; + + const services = syntheticImageDetectionAlgorithms + .map((algorithm) => algorithm.apiServiceName) + .join(","); const baseURL = process.env.REACT_APP_CAA_DEEPFAKE_URL; @@ -108,7 +118,7 @@ const SyntheticImageDetection = () => { bodyFormData.append("file", image); res = await axios.post(baseURL + modeURL + "jobs", bodyFormData, { method: "post", - params: { services: services }, + params: { services: services, search_similar: true }, headers: { "Content-Type": "multipart/form-data", }, @@ -117,7 +127,7 @@ const SyntheticImageDetection = () => { default: res = await axios.post(baseURL + modeURL + "jobs", null, { - params: { url: url, services: services }, + params: { url: url, services: services, search_similar: true }, }); break; } @@ -136,13 +146,42 @@ const SyntheticImageDetection = () => { handleError("error_" + error.status); } - if (response && response.data != null) + if (response && response.data != null) { dispatch( setSyntheticImageDetectionResult({ url: image ? URL.createObjectURL(image) : url, result: response.data, }), ); + } + + if ( + response && + response.data && + response.data.similar_images && + response.data.similar_images.completed + ) { + let imgSimilarRes; + + try { + imgSimilarRes = await axios.get(baseURL + modeURL + "similar/" + id); + } catch (error) { + handleError("error_" + error.status); + } + + console.log(imgSimilarRes.data); + + if ( + !imgSimilarRes.data || + !imgSimilarRes.data.similar_media || + !Array.isArray(imgSimilarRes.data.similar_media) || + imgSimilarRes.data.similar_media.length === 0 + ) { + dispatch(setSyntheticImageDetectionNearDuplicates(null)); + } + + dispatch(setSyntheticImageDetectionNearDuplicates(imgSimilarRes.data)); + } }; const waitUntilFinish = async (id) => { @@ -202,15 +241,29 @@ const SyntheticImageDetection = () => { ); }; - const handleSubmit = async () => { + /** + * + * @param url {string} + * @returns {Promise} + */ + const handleSubmit = async (url) => { dispatch(resetSyntheticImageDetectionImage()); - + const urlInput = url ? url : input; const type = - input && typeof input === "string" ? IMAGE_FROM.URL : IMAGE_FROM.UPLOAD; + urlInput && typeof urlInput === "string" + ? IMAGE_FROM.URL + : IMAGE_FROM.UPLOAD; - await getSyntheticImageScores(input, true, dispatch, type, imageFile); + await getSyntheticImageScores(urlInput, true, dispatch, type, imageFile); }; + useEffect(() => { + if (urlParam) { + setInput(urlParam); + handleSubmit(urlParam); + } + }, []); + return ( { fileInputTypesAccepted={"image/*"} handleCloseSelectedFile={handleClose} preprocessLocalFile={preprocessImage} + isParentLoading={isLoading} /> @@ -278,9 +332,10 @@ const SyntheticImageDetection = () => { {result && ( )} diff --git a/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx b/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx index 9913a59fb..b68e3f5b9 100644 --- a/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx +++ b/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx @@ -1,71 +1,118 @@ import React, { useEffect, useRef, useState } from "react"; import { + Accordion, + AccordionDetails, + AccordionSummary, Alert, Box, Card, + CardContent, CardHeader, + Chip, Grid, IconButton, Stack, } from "@mui/material"; -import { Close } from "@mui/icons-material"; +import { Close, Download, ExpandMore } from "@mui/icons-material"; import { i18nLoadNamespace } from "components/Shared/Languages/i18nLoadNamespace"; import { useSelector } from "react-redux"; import { useTrackEvent } from "Hooks/useAnalytics"; import { getclientId } from "components/Shared/GoogleAnalytics/MatomoAnalytics"; -import GaugeChartResult from "components/Shared/GaugeChartResults/GaugeChartResult"; +import CustomAlertScore from "../../../Shared/CustomAlertScore"; +import GaugeChart from "react-gauge-chart"; +import Tooltip from "@mui/material/Tooltip"; +import { exportReactElementAsJpg } from "../../../Shared/Utils/htmlUtils"; +import NddDatagrid from "./NddDatagrid"; +import { + DETECTION_THRESHOLDS, + getSyntheticImageDetectionAlgorithmFromApiName, + SyntheticImageDetectionAlgorithm, + syntheticImageDetectionAlgorithms, +} from "./SyntheticImageDetectionAlgorithms"; +import GaugeChartModalExplanation from "../../../Shared/GaugeChartResults/GaugeChartModalExplanation"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; + +/** + * Returns the alert color code for the given percentage n + * @param n {number} + * @returns {"error" | "warning" | "success"} + */ +export const getAlertColor = (n) => { + if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) { + return "error"; + } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) { + return "warning"; + } else { + return "success"; + } +}; + +export const getAlertLabel = (n, keyword) => { + if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) { + return keyword("synthetic_image_detection_alert_label_4"); + } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) { + return keyword("synthetic_image_detection_alert_label_3"); + } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_1) { + return keyword("synthetic_image_detection_alert_label_2"); + } else { + return keyword("synthetic_image_detection_alert_label_1"); + } +}; -const SyntheticImageDetectionResults = (props) => { +export const getPercentageColorCode = (n) => { + if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) { + return "#FF0000"; + } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) { + return "#FFAA00"; + } else { + return "green"; + } +}; + +class NddResult { + /** + * + * @param id {number} + * @param image {string} + * @param archiveUrl {string} + * @param imageUrls {string} + * @param detectionResults {SyntheticImageDetectionAlgorithmResult[]} + */ + constructor(id, image, archiveUrl, imageUrls, detectionResults) { + this.id = id; + this.image = image; + this.archiveUrl = archiveUrl; + this.imageUrls = imageUrls; + this.detectionResults = detectionResults; + } +} + +const SyntheticImageDetectionResults = ({ results, url, handleClose, nd }) => { const keyword = i18nLoadNamespace( "components/NavItems/tools/SyntheticImageDetection", ); - class SyntheticImageDetectionAlgorithmResult { + const role = useSelector((state) => state.userSession.user.roles); + + class SyntheticImageDetectionAlgorithmResult extends SyntheticImageDetectionAlgorithm { /** * - * @param methodName {string} + * @param syntheticImageDetectionAlgorithm {SyntheticImageDetectionAlgorithm} * @param predictionScore {number} * @param isError {boolean} */ - constructor(methodName, predictionScore, isError) { - (this.methodName = methodName), - (this.predictionScore = predictionScore), - (this.isError = isError); + constructor(syntheticImageDetectionAlgorithm, predictionScore, isError) { + super( + syntheticImageDetectionAlgorithm.apiServiceName, + syntheticImageDetectionAlgorithm.name, + syntheticImageDetectionAlgorithm.description, + syntheticImageDetectionAlgorithm.roleNeeded, + ); + (this.predictionScore = predictionScore), (this.isError = isError); } } - const DeepfakeImageDetectionMethodNames = { - gan: { - name: keyword("synthetic_image_detection_gan_name"), - description: keyword("synthetic_image_detection_gan_description"), - }, - diffusion: { - name: keyword("synthetic_image_detection_diffusion_name"), - description: keyword("synthetic_image_detection_diffusion_description"), - }, - progan_r50_grip: { - name: keyword("synthetic_image_detection_progan_name"), - description: keyword("synthetic_image_detection_progan_description"), - }, - adm_r50_grip: { - name: keyword("synthetic_image_detection_adm_name"), - description: keyword("synthetic_image_detection_adm_description"), - }, - progan_rine_mever: { - name: keyword("synthetic_image_detection_progan_rine_mever_name"), - description: keyword( - "synthetic_image_detection_progan_rine_mever_description", - ), - }, - ldm_rine_mever: { - name: keyword("synthetic_image_detection_ldm_rine_mever_name"), - description: keyword( - "synthetic_image_detection_ldm_rine_mever_description", - ), - }, - }; - const results = props.result; - const url = props.url; const imgElement = React.useRef(null); const imgContainerRef = useRef(null); @@ -80,68 +127,32 @@ const SyntheticImageDetectionResults = (props) => { useEffect(() => { setResultsHaveErrors(false); + let res = []; - const diffusionScore = new SyntheticImageDetectionAlgorithmResult( - //previously unina_report - Object.keys(DeepfakeImageDetectionMethodNames)[1], - !results.ldm_r50_grip_report.prediction - ? 0 - : results.ldm_r50_grip_report.prediction * 100, - !results.ldm_r50_grip_report.prediction, - ); - const ganScore = new SyntheticImageDetectionAlgorithmResult( - //previously gan_report - Object.keys(DeepfakeImageDetectionMethodNames)[0], - !results.gan_r50_mever_report.prediction - ? 0 - : results.gan_r50_mever_report.prediction * 100, - !results.gan_r50_mever_report.prediction, - ); - - const proganScore = new SyntheticImageDetectionAlgorithmResult( - Object.keys(DeepfakeImageDetectionMethodNames)[2], - !results.progan_r50_grip_report.prediction - ? 0 - : results.progan_r50_grip_report.prediction * 100, - !results.progan_r50_grip_report.prediction, - ); - - const admScore = new SyntheticImageDetectionAlgorithmResult( - Object.keys(DeepfakeImageDetectionMethodNames)[3], - !results.adm_r50_grip_report.prediction - ? 0 - : results.adm_r50_grip_report.prediction * 100, - !results.adm_r50_grip_report.prediction, - ); + for (const algorithm of syntheticImageDetectionAlgorithms) { + if ( + !role.includes(algorithm.roleNeeded) && + algorithm.roleNeeded.length > 0 + ) { + continue; + } - const proganRineScore = new SyntheticImageDetectionAlgorithmResult( - Object.keys(DeepfakeImageDetectionMethodNames)[4], - !results.progan_rine_mever_report.prediction - ? 0 - : results.progan_rine_mever_report.prediction * 100, - !results.progan_rine_mever_report.prediction, - ); + const algorithmReport = results[algorithm.apiServiceName + "_report"]; - const ldmRineScore = new SyntheticImageDetectionAlgorithmResult( - Object.keys(DeepfakeImageDetectionMethodNames)[5], - !results.ldm_rine_mever_report.prediction - ? 0 - : results.ldm_rine_mever_report.prediction * 100, - !results.ldm_rine_mever_report.prediction, - ); + if (algorithmReport) { + res.push( + new SyntheticImageDetectionAlgorithmResult( + algorithm, + !algorithmReport.prediction ? 0 : algorithmReport.prediction * 100, + algorithmReport.prediction === undefined, + ), + ); + } + } - const res = ( - role.includes("EXTRA_FEATURE") - ? [ - diffusionScore, - ganScore, - proganScore, - admScore, - proganRineScore, - ldmRineScore, - ] - : [diffusionScore, ganScore, proganScore] - ).sort((a, b) => b.predictionScore - a.predictionScore); + res = res + .filter((i) => i !== undefined) + .sort((a, b) => b.predictionScore - a.predictionScore); const hasResultError = () => { for (const algorithm of res) { @@ -166,7 +177,6 @@ const SyntheticImageDetectionResults = (props) => { const client_id = getclientId(); const session = useSelector((state) => state.userSession); - const role = useSelector((state) => state.userSession.user.roles); const uid = session && session.user ? session.user.id : null; useTrackEvent( @@ -179,52 +189,6 @@ const SyntheticImageDetectionResults = (props) => { uid, ); - const handleClose = () => { - props.handleClose(); - }; - const DETECTION_THRESHOLDS = { - THRESHOLD_1: 50, - THRESHOLD_2: 70, - THRESHOLD_3: 90, - }; - - const getPercentageColorCode = (n) => { - if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) { - return "#FF0000"; - } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) { - return "#FFAA00"; - } else { - return "green"; - } - }; - - /** - * Returns the alert color code for the given percentage n - * @param n {number} - * @returns {"error" | "warning" | "success"} - */ - const getAlertColor = (n) => { - if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) { - return "error"; - } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) { - return "warning"; - } else { - return "success"; - } - }; - - const getAlertLabel = (n) => { - if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) { - return keyword("synthetic_image_detection_alert_label_4"); - } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) { - return keyword("synthetic_image_detection_alert_label_3"); - } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_1) { - return keyword("synthetic_image_detection_alert_label_2"); - } else { - return keyword("synthetic_image_detection_alert_label_1"); - } - }; - /** * Returns a percentage between 0 and 99 for display purposes. We exclude 0 and 100 values. * @param percentage {number} @@ -245,6 +209,20 @@ const SyntheticImageDetectionResults = (props) => { ); }; + const [nddDetailsPanelMessage, setNddDetailsPanelMessage] = useState( + "synthetic_image_detection_ndd_additional_results_hide", + ); + const handleNddDetailsChange = () => { + nddDetailsPanelMessage === + "synthetic_image_detection_ndd_additional_results_hide" + ? setNddDetailsPanelMessage( + "synthetic_image_detection_ndd_additional_results", + ) + : setNddDetailsPanelMessage( + "synthetic_image_detection_ndd_additional_results_hide", + ); + }; + const keywords = [ "gauge_scale_modal_explanation_rating_1", "gauge_scale_modal_explanation_rating_2", @@ -253,65 +231,343 @@ const SyntheticImageDetectionResults = (props) => { ]; const colors = ["#00FF00", "#AAFF03", "#FFA903", "#FF0000"]; + /** + * + * @param nddResults + * @returns {NddResult[]} + */ + const getNddRows = (nddResults) => { + let rows = []; + for (let i = 0; i < nddResults.length; i += 1) { + const res = nddResults[i]; + let detectionResults = []; + for (const detection of Object.keys(res.detections)) { + const d = new SyntheticImageDetectionAlgorithmResult( + getSyntheticImageDetectionAlgorithmFromApiName(detection), + sanitizeDetectionPercentage(res.detections[detection] * 100), + false, + ); + + // Display iff the user has the permissions to see the content + if (role.includes(d.roleNeeded) || !d.roleNeeded) + detectionResults.push(d); + } + + if (detectionResults.length === 0) { + continue; + } + + rows.push( + new NddResult( + i + 1, + res.archive_url, + res.archive_url, + res.origin_urls, + detectionResults, + ), + ); + } + return rows; + }; + return ( - - - - - - } - /> + + + + + } + /> + - - - - {"Displays + + + + > + {"Displays + + + + + {nd && nd.similar_media && nd.similar_media.length > 0 && ( + + + + {keyword("synthetic_image_detection_ndd_info")} + + - + )} {syntheticImageScores.length > 0 ? ( - + + + + + {maxScore > DETECTION_THRESHOLDS.THRESHOLD_2 && ( + + {keyword( + "synthetic_image_detection_generic_detection_text", + )} + + )} + + + + + + {keyword( + "synthetic_image_detection_gauge_no_detection", + )} + + + {keyword("synthetic_image_detection_gauge_detection")} + + + + + + + + await exportReactElementAsJpg( + gaugeChartRef, + "gauge_chart", + ) + } + > + + + + + + + + + + + {resultsHaveErrors && ( + + {keyword("synthetic_image_detection_algorithms_errors")} + + )} + + + }> + {keyword(detailsPanelMessage)} + + + + {syntheticImageScores.map((item, key) => { + let predictionScore; + + if (item.predictionScore) { + predictionScore = sanitizeDetectionPercentage( + item.predictionScore, + ); + } + + return ( + + + + + + {keyword(item.name)} + + + + {item.isError ? ( + + {keyword( + "synthetic_image_detection_error_generic", + )} + + ) : ( + <> + + {keyword( + "synthetic_image_detection_probability_text", + )}{" "} + + + {predictionScore}% + + + )} + + {!item.isError && ( + + )} + + + + + + + {keyword(item.description)} + + + + {syntheticImageScores.length > key + 1 && ( + + )} + + ); + })} + + + + + ) : ( { )} + + {nd && nd.similar_media && nd.similar_media.length > 0 && ( + + + }> + {keyword(nddDetailsPanelMessage)} + + + + + + + + + )} + - - + + ); }; diff --git a/src/components/Shared/StringFileUploadField/index.jsx b/src/components/Shared/StringFileUploadField/index.jsx index a365539df..a3cf40afd 100644 --- a/src/components/Shared/StringFileUploadField/index.jsx +++ b/src/components/Shared/StringFileUploadField/index.jsx @@ -1,8 +1,9 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Box from "@mui/material/Box"; import { Button, ButtonGroup, Grid, TextField } from "@mui/material"; import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import CloseIcon from "@mui/icons-material/Close"; +import LoadingButton from "@mui/lab/LoadingButton"; /** * A reusable form component with a textfield and a local file with optional processing @@ -20,7 +21,7 @@ import CloseIcon from "@mui/icons-material/Close"; * @param handleSubmit {any} * @param handleCloseSelectedFile {any} An optional handler function to execute when clearing the file selected * @param preprocessLocalFile {any} Optional preprocessing function to process a local file - + * @param isParentLoading {?Boolean | undefined} Optional boolean to change the loading state of the component from a parent component */ const StringFileUploadField = ({ @@ -36,10 +37,19 @@ const StringFileUploadField = ({ fileInputTypesAccepted, handleCloseSelectedFile, preprocessLocalFile, + isParentLoading, }) => { const fileRef = useRef(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState( + isParentLoading !== undefined ? isParentLoading : false, + ); + + useEffect(() => { + if (isParentLoading !== undefined && isLoading !== isParentLoading) { + setIsLoading(isParentLoading); + } + }, [isParentLoading]); return ( @@ -58,7 +68,7 @@ const StringFileUploadField = ({ /> - + diff --git a/src/components/SideMenu/index.jsx b/src/components/SideMenu/index.jsx index 21b31187d..5dc2b8967 100644 --- a/src/components/SideMenu/index.jsx +++ b/src/components/SideMenu/index.jsx @@ -31,11 +31,11 @@ import ImageIcon from "../NavBar/images/SVG/Image/Images.svg"; import SearchIcon from "../NavBar/images/SVG/Search/Search.svg"; import DataIcon from "../NavBar/images/SVG/DataAnalysis/Data_analysis.svg"; import { - ROLES, TOOL_GROUPS, TOOL_STATUS_ICON, TOOLS_CATEGORIES, } from "../../constants/tools"; +import { ROLES } from "../../constants/roles"; import { selectTopMenuItem } from "../../redux/reducers/navReducer"; import { TOP_MENU_ITEMS } from "../../constants/topMenuItems"; import { selectTool } from "../../redux/reducers/tools/toolReducer"; diff --git a/src/constants/roles.jsx b/src/constants/roles.jsx new file mode 100644 index 000000000..c3dc42f18 --- /dev/null +++ b/src/constants/roles.jsx @@ -0,0 +1,11 @@ +/** + * Represents the user roles that can be needed to access a given topMenuItem + * @typedef Roles + * @type {{BETA_TESTER: string, ARCHIVE: string, LOCK: string}} + */ +export const ROLES = { + ARCHIVE: "ARCHIVE", + BETA_TESTER: "BETA_TESTER", + LOCK: "lock", + EXTRA_FEATURE: "EXTRA_FEATURE", +}; diff --git a/src/constants/tools.jsx b/src/constants/tools.jsx index 19d6ad90f..3a3a6edaa 100644 --- a/src/constants/tools.jsx +++ b/src/constants/tools.jsx @@ -53,6 +53,7 @@ import SemanticSearch from "../components/NavItems/tools/SemanticSearch"; import TwitterSna from "../components/NavItems/tools/TwitterSna/TwitterSna"; import Archive from "../components/NavItems/tools/Archive"; import About from "../components/NavItems/About/About"; +import { ROLES } from "./roles"; /** * Represents the categories to which the tools belong @@ -99,17 +100,6 @@ export const TOOL_GROUPS = { MORE: "more", }; -/** - * Represents the user roles that can be needed to access a given topMenuItem - * @typedef Roles - * @type {{BETA_TESTER: string, ARCHIVE: string, LOCK: string}} - */ -export const ROLES = { - ARCHIVE: "ARCHIVE", - BETA_TESTER: "BETA_TESTER", - LOCK: "lock", -}; - /** * Represents a topMenuItem that can be used by users */ @@ -425,7 +415,7 @@ const imageGif = new Tool(