diff --git a/.gitignore b/.gitignore index 6a7a2fd6..8ab1795d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ cypress/snapshots .idea *.db + +.vercel diff --git a/package.json b/package.json index 65db0fb9..4f317f7c 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "react-select": "^3.0.8", "react-time-series": "^1.0.20", "recoil": "^0.0.13", + "recoil-persist": "^0.8.0", "rfc6902": "^3.0.4", "seamless-immutable": "^7.1.4", "seamless-immutable-patch": "^1.0.4", @@ -129,6 +130,7 @@ "udt-collaboration-server": "^1.0.7", "udt-dataset-managers": "^1.0.15", "udt-format": "0.0.1", + "udt-review-hooks": "^1.0.1", "use-async-effect": "^2.2.2", "use-event-callback": "^0.1.0", "wavesurfer.js": "^3.3.3", diff --git a/src/AppWithContexts.js b/src/AppWithContexts.js index 3f3b085a..87b1d37f 100644 --- a/src/AppWithContexts.js +++ b/src/AppWithContexts.js @@ -8,15 +8,16 @@ import { AuthProvider } from "./utils/auth-handlers/use-auth.js" import { LabelHelpProvider } from "./components/LabelHelpView" import { HotkeyStorageProvider } from "./components/HotkeyStorage" import "./App.css" - +import recoilPersist from "recoil-persist" import Loading from "./components/Loading" - -// Importing Internalization file import "./i18n" +const { RecoilPersist, updateState } = recoilPersist() + export const AppWithContexts = () => { return ( - + + diff --git a/src/components/DatasetEditor/index.js b/src/components/DatasetEditor/index.js index 61695ead..bb14f93a 100644 --- a/src/components/DatasetEditor/index.js +++ b/src/components/DatasetEditor/index.js @@ -1,6 +1,7 @@ // @flow import React, { useState, useEffect, useMemo } from "react" +import Recoil from "recoil" import { makeStyles } from "@material-ui/core/styles" import { HeaderWithContainer } from "../Header" @@ -19,6 +20,8 @@ import { useHotkeyStorage } from "../HotkeyStorage" import useInterface from "../../hooks/use-interface" import useSummary from "../../hooks/use-summary" import useRemoveSamples from "../../hooks/use-remove-samples" +import ReviewPluginContent from "../ReviewPluginContent" +import { activeDatasetAtom } from "udt-review-hooks" import "brace/mode/javascript" import "brace/theme/github" @@ -57,7 +60,10 @@ export default ({ const labelOnlyMode = useIsLabelOnlyMode() const c = useStyles() const { addToast } = useToasts() - const [mode, changeMode] = useState(labelOnlyMode ? "label" : initialMode) + const isReviewMode = Boolean(Recoil.useRecoilValue(activeDatasetAtom)) + const [mode, changeMode] = useState( + isReviewMode ? "review" : labelOnlyMode ? "label" : initialMode + ) const { ipcRenderer } = useElectron() || {} const posthog = usePosthog() const { iface } = useInterface() @@ -66,7 +72,11 @@ export default ({ const [sampleIndex, setSampleIndex] = useState(null) - const headerTabs = labelOnlyMode ? ["Label"] : ["Setup", "Samples", "Label"] + const headerTabs = isReviewMode + ? ["Review"] + : labelOnlyMode + ? ["Label"] + : ["Setup", "Samples", "Label", "Review"] const [ sampleTimeToComplete, @@ -166,6 +176,7 @@ export default ({ onClickSetup={() => changeMode("setup")} /> )} + {mode === "review" && } diff --git a/src/components/HeaderToolbar/index.js b/src/components/HeaderToolbar/index.js index d545d30a..c7bf82c2 100644 --- a/src/components/HeaderToolbar/index.js +++ b/src/components/HeaderToolbar/index.js @@ -21,6 +21,9 @@ import { colors, Tooltip } from "@material-ui/core" import ExitToAppIcon from "@material-ui/icons/ExitToApp" import DownloadButton from "../DownloadButton" import PowerIcon from "@material-ui/icons/Power" +import RateReviewIcon from "@material-ui/icons/RateReview" +import Recoil from "recoil" +import { activeDatasetAtom } from "udt-review-hooks" const capitalize = (s) => { return s.charAt(0).toUpperCase() + s.slice(1) @@ -104,6 +107,8 @@ const getIcon = (t, tooltip) => { return case "Samples": return + case "Review": + return default: return
} @@ -138,6 +143,7 @@ const HeaderToolbar = ({ const c = useStyles() const { authProvider, isLoggedIn, logout } = useAuth() const { t } = useTranslation() + const isReviewMode = Boolean(Recoil.useRecoilValue(activeDatasetAtom)) return ( <> @@ -161,7 +167,6 @@ const HeaderToolbar = ({ {getIcon(name)} } - // label={isSmall ? "" : t} value={name.toLowerCase()} /> ))} @@ -169,24 +174,26 @@ const HeaderToolbar = ({ )}
- {fileOpen && } - - {!isDesktop && fileOpen && ( + {!isReviewMode && fileOpen && } + {!isReviewMode && ( + + )} + {!isReviewMode && !isDesktop && fileOpen && ( )} - {fileOpen && ( + {!isReviewMode && fileOpen && ( { const { addToast } = useToasts() useEffect(() => { + let pluginChangeWatcherInterval + const lastReloadTime = new Date().toGMTString() + const pluginUrlsToWatch = [] async function loadPlugins() { const pluginUrls = (fromConfig("pluginUrls") || "") .split("\n") @@ -31,21 +34,45 @@ export default () => { importPlugins, interfacePlugins, authenticationPlugins, + tabPlugins, + autoReload, } = (await import(/* webpackIgnore: true */ pluginUrl)).default() plugins.push( - ...transformPlugins.map((p) => ({ ...p, type: "transform" })) + ...transformPlugins.map((p) => ({ + ...p, + type: "transform", + pluginUrl, + })) + ) + plugins.push( + ...importPlugins.map((p) => ({ ...p, type: "import", pluginUrl })) ) - plugins.push(...importPlugins.map((p) => ({ ...p, type: "import" }))) plugins.push( - ...interfacePlugins.map((p) => ({ ...p, type: "interface" })) + ...interfacePlugins.map((p) => ({ + ...p, + type: "interface", + pluginUrl, + })) ) plugins.push( ...authenticationPlugins.map((p) => ({ ...p, type: "authentication", + pluginUrl, + })) + ) + plugins.push( + ...tabPlugins.map((p) => ({ + ...p, + type: "tab", + pluginUrl, })) ) + + if (autoReload) { + pluginUrlsToWatch.push(pluginUrl) + } } catch (e) { // TODO display broken plugin more nicely, using regex extraction of // package and version @@ -56,8 +83,19 @@ export default () => { } } setPlugins(plugins) + + if (pluginUrlsToWatch.length > 0) { + pluginChangeWatcherInterval = setInterval(() => { + // TODO check for 304s against the plugin and reload if necessary + }, 1000) + } } loadPlugins() + + return () => { + clearInterval(pluginChangeWatcherInterval) + } + // eslint-disable-next-line }, [fromConfig("pluginUrls")]) } diff --git a/src/components/PremiumStartingPage/index.js b/src/components/PremiumStartingPage/index.js new file mode 100644 index 00000000..722a6542 --- /dev/null +++ b/src/components/PremiumStartingPage/index.js @@ -0,0 +1,333 @@ +// @flow + +import React, { useState, useEffect } from "react" +import { makeStyles } from "@material-ui/core/styles" +import Grid from "@material-ui/core/Grid" +import { HeaderWithContainer } from "../Header" +import templates from "../StartingPage/templates" +import * as colors from "@material-ui/core/colors" +import { useDropzone } from "react-dropzone" +import { styled } from "@material-ui/core/styles" +import usePosthog from "../../hooks/use-posthog" +import packageInfo from "../../../package.json" +import useEventCallback from "use-event-callback" +import Box from "@material-ui/core/Box" +import Select from "react-select" +import { useTranslation } from "react-i18next" +import getEmbedYoutubeUrl from "../StartingPage/get-embed-youtube-url.js" +import packageJSON from "../../../package.json" +import Button from "@material-ui/core/Button" +import GetAppIcon from "@material-ui/icons/GetApp" +import useIsDesktop from "../../hooks/use-is-desktop" + +const useStyles = makeStyles((theme) => ({ + container: { + display: "flex", + flexDirection: "column", + backgroundColor: colors.grey[900], + height: "100vh", + }, + headerButton: { + fontSize: 12, + backgroundColor: "#fff", + }, + downloadIcon: { + marginTop: 2, + width: 18, + height: 18, + marginRight: 4, + marginLeft: -6, + color: colors.grey[700], + }, + languageSelectionWrapper: { + display: "flex", + flexDirection: "column", + textAlign: "center", + }, + languageSelectionBox: { + display: "flex", + paddingTop: 24, + [theme.breakpoints.up("sm")]: { + justifyContent: "flex-end", + }, + }, +})) + +const ContentContainer = styled("div")(({ theme }) => ({ + display: "flex", + justifyContent: "center", + flexGrow: 1, + color: "#fff", + overflowY: "scroll", + padding: 100, + [theme.breakpoints.down("sm")]: { + padding: 50, + }, +})) +const Content = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "calc(100% - 32px)", + marginLeft: 16, + maxWidth: 1000, +})) + +const Title = styled("div")({ + marginTop: 20, + fontSize: 36, + fontWeight: 600, + color: colors.grey[300], +}) + +const languageSelectionFormStyle = { + control: (base, state) => ({ + ...base, + border: "1px solid #9e9e9e", + background: "transparent", + color: "#e0e0e0", + }), + menuList: (base) => ({ + ...base, + padding: 0, + margin: 0, + color: "black", + }), + singleValue: (base) => ({ + ...base, + color: "white", + }), +} + +const Subtitle = styled("div")({ + fontSize: 18, + // fontWeight: "bold", + marginTop: 8, + color: colors.grey[500], +}) +const ActionList = styled("div")({ marginTop: 48 }) +const Action = styled("a")({ + display: "block", + color: colors.blue[500], + marginTop: 4, + cursor: "pointer", + textDecoration: "none", +}) +const ActionTitle = styled("div")({ + // fontWeight: "bold", + fontSize: 24, + marginBottom: 8, + color: colors.grey[500], +}) +const ActionText = styled("div")({ + color: colors.grey[300], + "& a": { + cursor: "pointer", + color: colors.blue[500], + textDecoration: "none", + }, +}) +const Actionless = styled("div")({ + color: colors.grey[600], + paddingTop: 16, +}) + +const BottomSpacer = styled("div")({ height: 100 }) + +const languageOptions = [ + { label: "English", value: "en" }, + { label: "French", value: "fr" }, + { label: "Chinese", value: "cn" }, + { label: "Portuguese", value: "pt" }, + { label: "Dutch", value: "nl" }, +] + +export default ({ + onFileDrop, + onOpenTemplate, + showDownloadLink = true, + recentItems = [], + onOpenRecentItem, + onClickOpenSession, +}) => { + const c = useStyles() + const posthog = usePosthog() + + // internalization hook + const { t, i18n } = useTranslation() + + const isDesktop = useIsDesktop() + // eslint-disable-next-line + const [newVersionAvailable, changeNewVersionAvailable] = useState(false) + useEffect(() => { + // if (!isDesktop) return + async function checkNewVersion() { + const newPackage = await fetch( + "https://raw.githubusercontent.com/UniversalDataTool/universal-data-tool/master/package.json" + ).then((r) => r.json()) + if (newPackage.version !== packageInfo.version) { + changeNewVersionAvailable(newPackage.version) + } + } + checkNewVersion() + }, []) + + const [latestCommunityUpdate, setLatestCommunityUpdate] = useState(null) + useEffect(() => { + async function getLatestREADME() { + const readme = await fetch( + "https://raw.githubusercontent.com/UniversalDataTool/universal-data-tool/master/README.md" + ).then((r) => r.text()) + const startCU = readme.search("COMMUNITY-UPDATE:START") + const endCU = readme.search("COMMUNITY-UPDATE:END") + const communityUpdates = readme + .slice(startCU, endCU) + .split("\n") + .slice(1, -1) + .filter((line) => line.trim() !== "") + const latestYtLink = communityUpdates[0].match(/\((.*)\)/)[1] + setLatestCommunityUpdate({ + name: communityUpdates[0].match(/\[(.*)\]/)[1], + ytLink: latestYtLink, + embedYTLink: getEmbedYoutubeUrl(latestYtLink), + }) + } + getLatestREADME() + }, []) + + const [ + createFromTemplateDialogOpen, + changeCreateFromTemplateDialogOpen, + ] = useState(false) + const [addAuthFromDialogOpen, changeAddAuthFromDialogOpen] = useState(false) + const onDrop = useEventCallback((acceptedFiles) => { + onFileDrop(acceptedFiles[0]) + }) + + const changeLanguage = (language) => { + i18n.changeLanguage(language) + } + + let { getRootProps, getInputProps } = useDropzone({ onDrop }) + + return ( +
+ + + + + + Universal Data Tool + {t("universaldatatool-description")} + v{packageJSON.version} + + + + + + {t("open-file")} + + {onClickOpenSession && ( + + {t("open-collaborative-session")} + + )} + changeAddAuthFromDialogOpen(true)}> + {t("add-authentication")} + + { + window.location.href = + "https://universaldatatool.com/courses" + }} + > + {t("create-training-course")} + + {/* Open Folder */} + + + {t("recent")} + {recentItems.length === 0 ? ( + {t("no-recent-files")} + ) : ( + recentItems.map((ri, i) => ( + onOpenRecentItem(ri)}> + {ri.fileName} + + )) + )} + + + {t("help")} + + {t("downloading-and-installing-udt")} + + + {t("labeling-images")} + + {/* Custom Data Entry */} + + {t("github-repository")} + + + {t("youtube-channel")} + + + + + {newVersionAvailable && isDesktop && ( + + )} + + + + + + + + +
+ ) +} diff --git a/src/components/PremiumStartingPage/index.story.js b/src/components/PremiumStartingPage/index.story.js new file mode 100644 index 00000000..860254a2 --- /dev/null +++ b/src/components/PremiumStartingPage/index.story.js @@ -0,0 +1,12 @@ +// @flow + +import React from "react" + +import { storiesOf } from "@storybook/react" +import { action } from "@storybook/addon-actions" + +import PremiumStartingPage from "./" + +storiesOf("PremiumStartingPage", module).add("Basic", () => ( + +)) diff --git a/src/components/PremiumWelcomeSidebarElement/index.js b/src/components/PremiumWelcomeSidebarElement/index.js new file mode 100644 index 00000000..cd6dc86e --- /dev/null +++ b/src/components/PremiumWelcomeSidebarElement/index.js @@ -0,0 +1,152 @@ +import React, { useEffect } from "react" +import { + styled, + colors, + TextField, + Button, + Box, + CircularProgress, +} from "@material-ui/core" +import { useRecoilState, useSetRecoilState, atom } from "recoil" +import useActiveDatasetManager from "../../hooks/use-active-dataset-manager" +import LocalStorageDatasetManager from "udt-dataset-managers/dist/LocalStorageDatasetManager" +import { useLogin, useDatasets, activeDatasetAtom } from "udt-review-hooks" +import moment from "moment" + +const Title = styled("div")({ + marginTop: 24, + color: colors.grey[500], + fontSize: 24, +}) + +const StyledTextField = styled(TextField)({ + "&.MuiTextField-root": { + marginTop: 12, + }, + "& .MuiInputLabel-formControl": { + color: colors.grey[500], + }, + "& .MuiInputBase-input": { + color: colors.grey[100], + }, +}) + +const StyledButton = styled(Button)({ + color: colors.grey[500], +}) + +const DatasetRow = styled("div")({ + display: "flex", + justifyContent: "space-between", + color: colors.grey[600], + "&:hover": { + backgroundColor: "rgba(255,255,255,0.1)", + color: colors.grey[400], + }, + cursor: "pointer", +}) +const DatasetCol = styled("div")({ + display: "flex", + padding: 8, + flexShrink: 0, + flexGrow: 1, + flexBasis: 1, + fontSize: 14, + textAlign: "left", + alignItems: "flex-end", + "&:last-child": { + color: colors.grey[700], + textAlign: "right", + fontSize: 12, + justifyContent: "flex-end", + }, +}) + +export const PremiumWelcomeSidebarElement = () => { + const [email, setEmail] = React.useState("") + const [password, setPassword] = React.useState("") + const { + login, + logout, + loginError, + isLoggedIn, + loading: loginLoading, + } = useLogin() + const { datasets } = useDatasets() + const setActiveDataset = useSetRecoilState(activeDatasetAtom) + const [, setActiveDatasetManager] = useActiveDatasetManager() + + if (loginLoading) + return ( + + + + ) + + if (!isLoggedIn) { + return ( + + {loginError && {loginError}} + setEmail(e.target.value)} + variant="filled" + label="Email" + /> + setPassword(e.target.value)} + variant="filled" + type="password" + label="Password" + /> + + login({ email, password })} + variant="filled" + > + Login + + + + ) + } + + return ( + + Datasets + + {(datasets || []).map((ds) => ( + { + setActiveDataset(ds) + const dm = new LocalStorageDatasetManager() + dm.setDataset({ + interface: {}, + samples: [], + dataset_id: ds.dataset_id, + }) + setActiveDatasetManager(dm) + }} + key={ds.dataset_id} + > + {ds.display_name} + {ds.number_of_samples} Samples + {moment(ds.last_activity).fromNow()} + + ))} + + + logout()} variant="filled"> + Sign Out + + + + ) +} + +export default PremiumWelcomeSidebarElement diff --git a/src/components/ReviewPluginContent/AddUserDialog.js b/src/components/ReviewPluginContent/AddUserDialog.js new file mode 100644 index 00000000..8d96eedf --- /dev/null +++ b/src/components/ReviewPluginContent/AddUserDialog.js @@ -0,0 +1,134 @@ +import React, { useState } from "react" +import { + Box, + CircularProgress, + Button, + Grid, + TextField, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + MenuItem, + colors, +} from "@material-ui/core" +import { useAddUser } from "udt-review-hooks" + +export const AddUserDialog = (props) => { + const addUser = useAddUser() + const [loading, setLoading] = useState(false) + const [error, setError] = useState() + const { onClose, open } = props + const [userData, setUserData] = useState({ + name: "", + email: "", + password: "", + role: "labeler", + }) + + const onSubmit = async () => { + setLoading(true) + setError(null) + try { + await addUser(userData) + onClose() + } catch (e) { + setError(e.toString()) + } + setLoading(false) + } + + const onInputChange = (event) => { + setUserData({ + ...userData, + [event.target.name]: event.target.value, + }) + } + + return ( + + Add new user + + {loading ? ( + + + + ) : ( + + {error && ( + + + {error} + + + )} + + + + + + + + + + + + {["admin", "reviewer", "labeler"].map((role) => ( + + {role} + + ))} + + + + )} + + + + + + + ) +} +export default AddUserDialog diff --git a/src/components/ReviewPluginContent/AdminSettings.js b/src/components/ReviewPluginContent/AdminSettings.js new file mode 100644 index 00000000..0324c4f5 --- /dev/null +++ b/src/components/ReviewPluginContent/AdminSettings.js @@ -0,0 +1,75 @@ +import React from "react" +import Recoil from "recoil" +import { Box, Button, CircularProgress } from "@material-ui/core" +import { activeDatasetAtom, useAddDataset } from "udt-review-hooks" +import useActiveDatasetManager from "../../hooks/use-active-dataset-manager" + +export const AdminSettings = () => { + const [activeDataset, setActiveDataset] = Recoil.useRecoilState( + activeDatasetAtom + ) + const [dm] = useActiveDatasetManager() + const [loading, setLoading] = React.useState(false) + const addDataset = useAddDataset() + + return ( + + {loading ? ( + + ) : activeDataset ? ( + + + Your dataset is loaded. + + + + + + + ) : ( + + + This dataset is being stored locally. + + + + + + )} + + ) +} + +export default AdminSettings diff --git a/src/components/ReviewPluginContent/Analytics.js b/src/components/ReviewPluginContent/Analytics.js new file mode 100644 index 00000000..710123f5 --- /dev/null +++ b/src/components/ReviewPluginContent/Analytics.js @@ -0,0 +1,28 @@ +import { Box } from "@material-ui/core" +import React from "react" +import TeamPerformanceTable from "./TeamPerformanceTitle" +import SimpleSidebar from "./SimpleSidebar" +import { colors } from "@material-ui/core" + +export const Analytics = () => { + return ( + + + This page isn't ready yet + + + + ) +} + +export default Analytics diff --git a/src/components/ReviewPluginContent/AuditTrail.js b/src/components/ReviewPluginContent/AuditTrail.js new file mode 100644 index 00000000..a62c85d5 --- /dev/null +++ b/src/components/ReviewPluginContent/AuditTrail.js @@ -0,0 +1,95 @@ +import React from "react" +import { CircularProgress, Box, styled, colors } from "@material-ui/core" +import EditIcon from "@material-ui/icons/Edit" +import ComputerIcon from "@material-ui/icons/Computer" +import SettingsApplicationsIcon from "@material-ui/icons/SettingsApplications" +import SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle" + +const ItemContainer = styled("div")({ + padding: 16, + margin: 8, + alignItems: "center", + fontSize: 12, + display: "flex", + border: "1px solid #ccc", + "& .icon": { + marginRight: 16, + width: 18, + height: 18, + }, + cursor: "pointer", + transition: "border-left 80ms ease,padding-right 80ms ease", + "&.selected": { + borderLeft: `4px solid ${colors.blue[500]}`, + paddingRight: 13, + }, + "&:hover": { + borderLeft: `4px solid ${colors.blue[500]}`, + }, +}) + +const ItemText = styled("div")({}) + +const getIcon = (type, item) => { + switch (type) { + case "label": + case "work": { + return + } + case "system": { + return ( + + ) + } + case "work_review": + case "review": { + return ( + + ) + } + default: { + } + } +} + +export const AuditTrail = ({ selectedItem, items, onSelectItem }) => { + if (!items) + return ( + + + + ) + return ( + + {items.map((item, i) => ( + onSelectItem(item)} + key={i} + className={selectedItem === item && "selected"} + > + {getIcon(item.type, item)} + {item.type === "work" && ( + {item.worker_name} submitted labels + )} + {item.type === "work_review" && ( + + {item.reviewer_name} {item.accept_work ? "accepted" : "rejected"}{" "} + labels from {item.worker_name} + + )} + {item.type === "system" && {item.message}} + + ))} + + ) +} + +export default AuditTrail diff --git a/src/components/ReviewPluginContent/Label.js b/src/components/ReviewPluginContent/Label.js new file mode 100644 index 00000000..f1d20aeb --- /dev/null +++ b/src/components/ReviewPluginContent/Label.js @@ -0,0 +1,31 @@ +import React from "react" +import Recoil from "recoil" +import { CircularProgress, Box, Button } from "@material-ui/core" +import UniversalSampleEditor from "../UniversalSampleEditor" +import { useAssignedSample } from "udt-review-hooks" + +export const Label = () => { + const { sample, submit, reassign, error, loadingSample } = useAssignedSample() + if (!sample) + return ( + + + + ) + return ( + <> + { + await submit(sample.annotation) + }} + onExit={() => null} + /> + + + ) +} + +export default Label diff --git a/src/components/ReviewPluginContent/QualityContent.js b/src/components/ReviewPluginContent/QualityContent.js new file mode 100644 index 00000000..d873ec98 --- /dev/null +++ b/src/components/ReviewPluginContent/QualityContent.js @@ -0,0 +1,78 @@ +import React from "react" +import { + Button, + Box, + TextField, + InputAdornment, + CircularProgress, +} from "@material-ui/core" +import { useDatasetSettings } from "udt-review-hooks" + +export const QualityContent = () => { + const { updateDatasetSettings, dataset, loading } = useDatasetSettings() + const [newSettings, setNewSetting] = React.useReducer( + (state, [key, value]) => + key === "erase" ? {} : { ...state, [key]: value }, + {} + ) + + return ( + + + Quality is controlled by assigning multiple labelers each dataset + sample. Each labeler's work is checked against other labelers who have + labeled the same sample. Based on the agreement or disagreement, the + incorrect labeler(s) will be notified and worker statistics relating to + accuracy will be updated. +
+
+ You can configure specific settings to ensure higher overall dataset + quality or increase the speed of the labeling operation. +
+ {loading ? ( + + + + ) : ( + <> + %, + }} + /> + { + setNewSetting(["votes_per_sample", parseInt(e.target.value)]) + }} + variant="outlined" + label="Votes" + helperText="Number of times each sample will be labeled" + /> + + )} + + + +
+ ) +} + +export default QualityContent diff --git a/src/components/ReviewPluginContent/Review.js b/src/components/ReviewPluginContent/Review.js new file mode 100644 index 00000000..7c6c4395 --- /dev/null +++ b/src/components/ReviewPluginContent/Review.js @@ -0,0 +1,82 @@ +import React, { useState } from "react" +import Title from "./Title" +import SimpleSidebar from "./SimpleSidebar" +import { + TextField, + InputAdornment, + colors, + Box, + Table, + TableHead, + TableRow, + TableBody, + TableCell, + Button, +} from "@material-ui/core" +import CheckIcon from "@material-ui/icons/Check" +import SearchIcon from "@material-ui/icons/Search" +import moment from "moment" +import ReviewSamplesTable from "./ReviewSamplesTable" +import ReviewSampleContent from "./ReviewSampleContent" + +const sidebarItems = [ + { + name: "All Samples", + }, + { + name: "Needs Review", + }, + { + name: "Reviewed", + }, +] + +export const Review = () => { + const [selectedItem, setSelectedItem] = useState("All Samples") + const [selectedSampleId, setSelectedSampleId] = useState(null) + const [sampleQueue, setSampleQueue] = useState(null) + return ( + setSelectedItem(selectedItem)} + selectedItem={selectedItem} + > + {!selectedItem.startsWith("Sample ") && ( + { + setSelectedItem(`Sample ${sample.sample_index}`) + setSelectedSampleId(sample.sample_id) + setSampleQueue(sampleQueue) + }} + /> + )} + {selectedItem.startsWith("Sample ") && ( + { + setSelectedItem("Needs Review") + }} + onNext={() => { + if (sampleQueue.length === 0) { + setSelectedItem("Needs Review") + return + } + const nextSample = sampleQueue[0] + setSelectedItem(`Sample ${nextSample.sample_index}`) + setSelectedSampleId(nextSample.sample_id) + setSampleQueue(sampleQueue.slice(1)) + }} + /> + )} + + ) +} + +export default Review diff --git a/src/components/ReviewPluginContent/ReviewSampleContent.js b/src/components/ReviewPluginContent/ReviewSampleContent.js new file mode 100644 index 00000000..cfd9668b --- /dev/null +++ b/src/components/ReviewPluginContent/ReviewSampleContent.js @@ -0,0 +1,144 @@ +import React from "react" +import { + CircularProgress, + Box, + styled, + Button, + colors, +} from "@material-ui/core" +import AuditTrail from "./AuditTrail.js" +import UniversalSampleEditor from "../UniversalSampleEditor" +import { useSample, useReviewWork } from "udt-review-hooks" + +export const ReviewSampleContent = ({ sampleId, onBack, onNext }) => { + const [mode, setMode] = React.useState("view-best") // correct, view-audit-item + const [selectedAuditItem, setSelectedAuditItem] = React.useState() + const { + sample, + auditTrail, + loading, + reloadSample, + submitAnnotation, + } = useSample({ + sampleId, + withAuditTrail: true, + }) + const work_id = + mode === "view-best" ? sample?.best_work_id : selectedAuditItem?.work_id + const { reviewWork } = useReviewWork(work_id) + + const sampleBeingViewed = React.useMemo( + () => ({ + ...sample?.sample_data, + annotation: + mode === "correct" + ? null + : mode === "view-best" + ? sample?.best_annotation + : mode === "view-audit-item" + ? selectedAuditItem?.annotation + : null, + }), + [sample, mode, selectedAuditItem] + ) + + return ( + + + + + + + + + + {loading || !sample ? ( + + + + ) : mode === "view-audit-item" && selectedAuditItem.type !== "work" ? ( +
{JSON.stringify(selectedAuditItem, null, "  ")}
+ ) : ( + { + await submitAnnotation(sample.annotation) + }} + /> + )} +
+ + + History + + { + setSelectedAuditItem(item) + setMode("view-audit-item") + }} + /> + + + + + +
+ ) +} + +export default ReviewSampleContent diff --git a/src/components/ReviewPluginContent/ReviewSamplesTable.js b/src/components/ReviewPluginContent/ReviewSamplesTable.js new file mode 100644 index 00000000..a758c3ea --- /dev/null +++ b/src/components/ReviewPluginContent/ReviewSamplesTable.js @@ -0,0 +1,115 @@ +import React from "react" +import { + TextField, + InputAdornment, + colors, + Box, + Table, + TableHead, + TableRow, + TableBody, + TableCell, + Button, + CircularProgress, +} from "@material-ui/core" +import Title from "./Title" +import CheckIcon from "@material-ui/icons/Check" +import SearchIcon from "@material-ui/icons/Search" +import moment from "moment" +import { useSampleSearch } from "udt-review-hooks" + +export const ReviewSamplesTable = ({ selectedItem, onClickSample }) => { + const searchOptions = React.useMemo( + () => ({ + limit: 20, + filter: + selectedItem === "Needs Review" + ? "needs-review" + : selectedItem === "Reviewed" + ? "reviewed" + : "", + }), + [selectedItem] + ) + const { samples } = useSampleSearch(searchOptions) + const [searchText, setSearchText] = React.useState("") + + return ( + <> + Samples + + setSearchText(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + + SI + Consensus + Confidence + Reviewed + Last Activity + + + + + {samples && + samples + .filter((s) => + selectedItem === "Needs Review" + ? !s.is_reviewed + : selectedItem === "Complete" + ? s.number_of_works >= s.number_of_times_to_be_labeled + : true + ) + .map((s, i) => ( + + {s.sample_index} + + {s.number_of_works} / {s.number_of_times_to_be_labeled} + + + {s.confidence.toFixed(1)}% + + + {s.is_reviewed ? : null} + + + {!s.last_activity ? "" : moment(s.last_activity).fromNow()} + + + + + + ))} + +
+ {!samples && ( + + + + )} + + ) +} + +export default ReviewSamplesTable diff --git a/src/components/ReviewPluginContent/Settings.js b/src/components/ReviewPluginContent/Settings.js new file mode 100644 index 00000000..d50b5ba1 --- /dev/null +++ b/src/components/ReviewPluginContent/Settings.js @@ -0,0 +1,45 @@ +import React, { useState, useMemo, styled } from "react" +import { useRecoilValue } from "recoil" +import { Box, colors } from "@material-ui/core" +import Title from "./Title" +import TeamTable from "./TeamTable" +import QualityContent from "./QualityContent" +import SimpleSidebar from "./SimpleSidebar" +import AdminSettings from "./AdminSettings" +import { userAtom } from "udt-review-hooks" + +export const Settings = () => { + const user = useRecoilValue(userAtom) + + const sidebarItems = useMemo( + () => + [ + user.role.toLowerCase() === "admin" && { + name: "Admin", + }, + { + name: "Team", + }, + { + name: "Quality", + }, + ].filter(Boolean), + [] + ) + const [selectedItem, setSelectedItem] = useState(sidebarItems[0].name) + + return ( + + {selectedItem} + {selectedItem === "Admin" && } + {selectedItem === "Team" && } + {selectedItem === "Quality" && } + + ) +} + +export default Settings diff --git a/src/components/ReviewPluginContent/SidebarItem.js b/src/components/ReviewPluginContent/SidebarItem.js new file mode 100644 index 00000000..9f97235f --- /dev/null +++ b/src/components/ReviewPluginContent/SidebarItem.js @@ -0,0 +1,15 @@ +import React from "react" +import { styled, colors } from "@material-ui/core" + +export const SidebarItem = styled("div")(({ selected }) => ({ + padding: 8, + fontWeight: 500, + color: selected ? colors.blue[600] : colors.grey[800], + opacity: !selected ? 0.75 : 1, + cursor: "pointer", + "&:hover": { + opacity: 1, + }, +})) + +export default SidebarItem diff --git a/src/components/ReviewPluginContent/SimpleSidebar.js b/src/components/ReviewPluginContent/SimpleSidebar.js new file mode 100644 index 00000000..ab85e228 --- /dev/null +++ b/src/components/ReviewPluginContent/SimpleSidebar.js @@ -0,0 +1,36 @@ +import React from "react" +import { Box, colors } from "@material-ui/core" +import SidebarItem from "./SidebarItem" + +export const SimpleSidebar = ({ + children, + sidebarItems, + selectedItem, + onSelectItem, +}) => { + return ( + + + {sidebarItems.map((item) => ( + onSelectItem(item.name)} + > + {item.name} + + ))} + + + {children} + + + ) +} + +export default SimpleSidebar diff --git a/src/components/ReviewPluginContent/TeamPerformanceTitle.js b/src/components/ReviewPluginContent/TeamPerformanceTitle.js new file mode 100644 index 00000000..478b6c74 --- /dev/null +++ b/src/components/ReviewPluginContent/TeamPerformanceTitle.js @@ -0,0 +1,65 @@ +import React from "react" +import { + colors, + Box, + Button, + Table, + TableHead, + TableRow, + TableBody, + TableCell, +} from "@material-ui/core" +import EditIcon from "@material-ui/icons/Edit" +import moment from "moment" + +const people = [ + { name: "Billy Acosta", email: "billyaacoasta@rhyta.com", role: "Admin" }, + { + name: "Michael Reynolds", + email: "MichaelCReynolds@rhyta.com", + samplesReviewed: 11, + role: "Reviewer", + }, + { name: "Mary Pack", email: "marypack@rhyta.com", role: "Labeler" }, + { name: "William Pierce", email: "willp@rhyta.com", role: "Labeler" }, + { name: "Micheal Myers", email: "mmyers@rhyta.com", role: "Labeler" }, + { name: "Micheal Lyons", email: "mikelyons@rhyta.com", role: "Labeler" }, +].map((a, i) => ({ + ...a, + samplesLabeled: 2 + ((i * 17) % 7) ** 2, + accuracy: 40 + (((2 + ((i * 17) % 7) ** 2) * 17.7) % 60), + timePerLabel: Math.floor(Math.random() * 600) / 10, +})) + +export const TeamPerformanceTable = () => { + return ( + <> + + + + Name + Email + Samples Labeled + Samples Reviewed + Accuracy + Avg Time/Label + + + + {people.map((p, i) => ( + + {p.name} + {p.email} + {p.samplesLabeled} + {p.samplesReviewed || 0} + {p.accuracy.toFixed(1)}% + {p.timePerLabel.toFixed(1)}s + + ))} + +
+ + ) +} + +export default TeamPerformanceTable diff --git a/src/components/ReviewPluginContent/TeamTable.js b/src/components/ReviewPluginContent/TeamTable.js new file mode 100644 index 00000000..220a66ed --- /dev/null +++ b/src/components/ReviewPluginContent/TeamTable.js @@ -0,0 +1,68 @@ +import React, { useState } from "react" +import { + colors, + Box, + Button, + Table, + TableHead, + TableRow, + TableBody, + TableCell, +} from "@material-ui/core" +import EditIcon from "@material-ui/icons/Edit" +import moment from "moment" +import AddUserDialog from "./AddUserDialog" +import { useTeam } from "udt-review-hooks" + +export const TeamTable = () => { + const [openAddUserDialog, setOpenAddUserDialog] = useState(false) + const { team, reloadTeam } = useTeam() + return ( + <> + + + + Name + Email + Role + Last Activity + + + + {team && + team.map((p, i) => ( + + {p.name} + {p.email} + + + {p.role} + + + + {moment(p.last_activity).fromNow()} + + ))} + +
+ + + + { + setOpenAddUserDialog(false) + reloadTeam() + }} + /> + + ) +} + +export default TeamTable diff --git a/src/components/ReviewPluginContent/Title.js b/src/components/ReviewPluginContent/Title.js new file mode 100644 index 00000000..b40059a7 --- /dev/null +++ b/src/components/ReviewPluginContent/Title.js @@ -0,0 +1,10 @@ +import React from "react" +import { styled, colors } from "@material-ui/core" + +export const Title = styled("div")({ + fontSize: 24, + color: colors.grey[600], + padding: 32, +}) + +export default Title diff --git a/src/components/ReviewPluginContent/index.js b/src/components/ReviewPluginContent/index.js new file mode 100644 index 00000000..d2c48a95 --- /dev/null +++ b/src/components/ReviewPluginContent/index.js @@ -0,0 +1,75 @@ +import React, { useState } from "react" +import Recoil, { useRecoilValue } from "recoil" +import useEventCallback from "use-event-callback" +import { Tabs, Tab, Box, colors, styled } from "@material-ui/core" +import SettingsIcon from "@material-ui/icons/Settings" +import RateReviewIcon from "@material-ui/icons/RateReview" +import CreateIcon from "@material-ui/icons/Create" +import PollIcon from "@material-ui/icons/Poll" +import Settings from "./Settings" +import Analytics from "./Analytics" +import Review from "./Review" +import Label from "./Label" +import AdminSettings from "./AdminSettings" +import { activeDatasetAtom, userAtom } from "udt-review-hooks" + +const tabs = [ + { name: "Settings", roles: ["admin"] }, + { name: "Review", roles: ["admin", "reviewer"] }, + { name: "Label", roles: ["admin", "reviewer", "labeler"] }, + { name: "Analytics", roles: ["admin", "reviewer", "labeler"] }, +] + +const getIcon = (s) => { + switch (s.toLowerCase()) { + case "settings": { + return + } + case "review": { + return + } + case "label": { + return + } + case "analytics": { + return + } + default: { + return null + } + } +} + +export const ReviewPluginContent = () => { + const user = useRecoilValue(userAtom) + const [tab, setTab] = useState("review") + const onChangeTab = useEventCallback((e, newTab) => { + setTab(newTab) + }) + const activeDataset = Recoil.useRecoilValue(activeDatasetAtom) + + if (!activeDataset) return + + return ( + + + {tabs + .filter((tab) => tab.roles.includes(user.role.toLowerCase())) + .map((tab) => ( + + ))} + + {tab === "settings" && } + {tab === "review" && } + {tab === "label" && + ) +} + +export default ReviewPluginContent diff --git a/src/components/ReviewPluginContent/index.story.js b/src/components/ReviewPluginContent/index.story.js new file mode 100644 index 00000000..3fe1942e --- /dev/null +++ b/src/components/ReviewPluginContent/index.story.js @@ -0,0 +1,11 @@ +// @flow + +import React from "react" + +import { storiesOf } from "@storybook/react" + +import ReviewPluginContent from "./" + +storiesOf("ReviewPluginContent", module).add("Basic", () => ( + +)) diff --git a/src/components/StartingPage/RightSideContent.js b/src/components/StartingPage/RightSideContent.js new file mode 100644 index 00000000..5b774cfc --- /dev/null +++ b/src/components/StartingPage/RightSideContent.js @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from "react" +import { Button, styled, Box, colors } from "@material-ui/core" +import packageInfo from "../../../package.json" +import useIsDesktop from "../../hooks/use-is-desktop" +import { + ContentContainer, + Content, + Title, + Subtitle, + ActionList, + Action, + ActionTitle, + ActionText, + Actionless, + BottomSpacer, + useStyles, +} from "./parts" +import { useTranslation } from "react-i18next" +import getEmbedYoutubeUrl from "./get-embed-youtube-url.js" +import GetAppIcon from "@material-ui/icons/GetApp" +import PremiumWelcomeSidebarElement from "../PremiumWelcomeSidebarElement" + +const Tab = styled("div")({ + fontWeight: 600, + color: colors.grey[500], + borderBottom: `1px solid ${colors.grey[700]}`, + marginLeft: 8, + fontSize: 14, + "&:first-child": { + marginLeft: 0, + }, + cursor: "pointer", + "&.active, &:hover": { + color: colors.blue[500], + borderBottom: `1px solid ${colors.blue[700]}`, + }, +}) + +export const RightSideContent = () => { + const { t } = useTranslation() + const c = useStyles() + const isDesktop = useIsDesktop() + const [tab, setTab] = useState("Premium") + const [newVersionAvailable, changeNewVersionAvailable] = useState(false) + + useEffect(() => { + // if (!isDesktop) return + async function checkNewVersion() { + const newPackage = await fetch( + "https://raw.githubusercontent.com/UniversalDataTool/universal-data-tool/master/package.json" + ).then((r) => r.json()) + if (newPackage.version !== packageInfo.version) { + changeNewVersionAvailable(newPackage.version) + } + } + checkNewVersion() + }, []) + + const [latestCommunityUpdate, setLatestCommunityUpdate] = useState(null) + useEffect(() => { + async function getLatestREADME() { + const readme = await fetch( + "https://raw.githubusercontent.com/UniversalDataTool/universal-data-tool/master/README.md" + ).then((r) => r.text()) + const startCU = readme.search("COMMUNITY-UPDATE:START") + const endCU = readme.search("COMMUNITY-UPDATE:END") + const communityUpdates = readme + .slice(startCU, endCU) + .split("\n") + .slice(1, -1) + .filter((line) => line.trim() !== "") + const latestYtLink = communityUpdates[0].match(/\((.*)\)/)[1] + setLatestCommunityUpdate({ + name: communityUpdates[0].match(/\[(.*)\]/)[1], + ytLink: latestYtLink, + embedYTLink: getEmbedYoutubeUrl(latestYtLink), + }) + } + getLatestREADME() + }, []) + + return ( +
+ + setTab("About")} + className={tab === "About" ? "active" : ""} + > + About + + setTab("Premium")} + className={tab === "Premium" ? "active" : ""} + > + Premium + + + {tab === "About" && ( + <> + {newVersionAvailable && isDesktop && ( + + )} + + {t("about")} + + {t("start-page-about-first-paragraph")} +
+
+ {t("start-page-about-second-paragraph")} +
+
+ {t("the-udt-uses-an")}{" "} + + open-source data format (.udt.json / .udt.csv) + {" "} + {t("start-page-about-third-paragraph")} +
+
+
+
+ + {latestCommunityUpdate && ( + <> + {latestCommunityUpdate.name} + + + )} + + + )} + {tab === "Premium" && } +
+ ) +} + +export default RightSideContent diff --git a/src/components/StartingPage/index.js b/src/components/StartingPage/index.js index 68e690ea..b2619145 100644 --- a/src/components/StartingPage/index.js +++ b/src/components/StartingPage/index.js @@ -16,70 +16,22 @@ import useEventCallback from "use-event-callback" import Box from "@material-ui/core/Box" import Select from "react-select" import { useTranslation } from "react-i18next" -import getEmbedYoutubeUrl from "./get-embed-youtube-url.js" import packageJSON from "../../../package.json" import Button from "@material-ui/core/Button" -import GetAppIcon from "@material-ui/icons/GetApp" -import useIsDesktop from "../../hooks/use-is-desktop" - -const useStyles = makeStyles((theme) => ({ - container: { - display: "flex", - flexDirection: "column", - backgroundColor: colors.grey[900], - height: "100vh", - }, - headerButton: { - fontSize: 12, - backgroundColor: "#fff", - }, - downloadIcon: { - marginTop: 2, - width: 18, - height: 18, - marginRight: 4, - marginLeft: -6, - color: colors.grey[700], - }, - languageSelectionWrapper: { - display: "flex", - flexDirection: "column", - textAlign: "center", - }, - languageSelectionBox: { - display: "flex", - paddingTop: 24, - [theme.breakpoints.up("sm")]: { - justifyContent: "flex-end", - }, - }, -})) - -const ContentContainer = styled("div")(({ theme }) => ({ - display: "flex", - justifyContent: "center", - flexGrow: 1, - color: "#fff", - overflowY: "scroll", - padding: 100, - [theme.breakpoints.down("sm")]: { - padding: 50, - }, -})) -const Content = styled("div")(({ theme }) => ({ - display: "flex", - flexDirection: "column", - width: "calc(100% - 32px)", - marginLeft: 16, - maxWidth: 1000, -})) - -const Title = styled("div")({ - marginTop: 20, - fontSize: 36, - fontWeight: 600, - color: colors.grey[300], -}) +import RightSideContent from "./RightSideContent" +import { + ContentContainer, + Content, + Title, + Subtitle, + ActionList, + Action, + ActionTitle, + ActionText, + Actionless, + BottomSpacer, + useStyles, +} from "./parts" const languageSelectionFormStyle = { control: (base, state) => ({ @@ -100,41 +52,6 @@ const languageSelectionFormStyle = { }), } -const Subtitle = styled("div")({ - fontSize: 18, - // fontWeight: "bold", - marginTop: 8, - color: colors.grey[500], -}) -const ActionList = styled("div")({ marginTop: 48 }) -const Action = styled("a")({ - display: "block", - color: colors.blue[500], - marginTop: 4, - cursor: "pointer", - textDecoration: "none", -}) -const ActionTitle = styled("div")({ - // fontWeight: "bold", - fontSize: 24, - marginBottom: 8, - color: colors.grey[500], -}) -const ActionText = styled("div")({ - color: colors.grey[300], - "& a": { - cursor: "pointer", - color: colors.blue[500], - textDecoration: "none", - }, -}) -const Actionless = styled("div")({ - color: colors.grey[600], - paddingTop: 16, -}) - -const BottomSpacer = styled("div")({ height: 100 }) - const languageOptions = [ { label: "English", value: "en" }, { label: "French", value: "fr" }, @@ -157,45 +74,6 @@ export default ({ // internalization hook const { t, i18n } = useTranslation() - const isDesktop = useIsDesktop() - // eslint-disable-next-line - const [newVersionAvailable, changeNewVersionAvailable] = useState(false) - useEffect(() => { - // if (!isDesktop) return - async function checkNewVersion() { - const newPackage = await fetch( - "https://raw.githubusercontent.com/UniversalDataTool/universal-data-tool/master/package.json" - ).then((r) => r.json()) - if (newPackage.version !== packageInfo.version) { - changeNewVersionAvailable(newPackage.version) - } - } - checkNewVersion() - }, []) - - const [latestCommunityUpdate, setLatestCommunityUpdate] = useState(null) - useEffect(() => { - async function getLatestREADME() { - const readme = await fetch( - "https://raw.githubusercontent.com/UniversalDataTool/universal-data-tool/master/README.md" - ).then((r) => r.text()) - const startCU = readme.search("COMMUNITY-UPDATE:START") - const endCU = readme.search("COMMUNITY-UPDATE:END") - const communityUpdates = readme - .slice(startCU, endCU) - .split("\n") - .slice(1, -1) - .filter((line) => line.trim() !== "") - const latestYtLink = communityUpdates[0].match(/\((.*)\)/)[1] - setLatestCommunityUpdate({ - name: communityUpdates[0].match(/\[(.*)\]/)[1], - ytLink: latestYtLink, - embedYTLink: getEmbedYoutubeUrl(latestYtLink), - }) - } - getLatestREADME() - }, []) - const [ createFromTemplateDialogOpen, changeCreateFromTemplateDialogOpen, @@ -315,61 +193,7 @@ export default ({ - {newVersionAvailable && isDesktop && ( - - )} - - {t("about")} - - {t("start-page-about-first-paragraph")} -
-
- {t("start-page-about-second-paragraph")} -
-
- {t("the-udt-uses-an")}{" "} - - open-source data format (.udt.json / .udt.csv) - {" "} - {t("start-page-about-third-paragraph")} -
-
-
-
- - {latestCommunityUpdate && ( - <> - {latestCommunityUpdate.name} - - - )} - {/* - changeCreateFromTemplateDialogOpen(true)} - > - {t("open-a-template")} - {" "} - {t("to-see-how-the-udt-could-work-for-your-data")} - */} - +
diff --git a/src/components/StartingPage/parts.js b/src/components/StartingPage/parts.js new file mode 100644 index 00000000..a7318e10 --- /dev/null +++ b/src/components/StartingPage/parts.js @@ -0,0 +1,83 @@ +import React from "react" +import { styled, colors, makeStyles } from "@material-ui/core" + +export const useStyles = makeStyles((theme) => ({ + container: { + display: "flex", + flexDirection: "column", + backgroundColor: colors.grey[900], + height: "100vh", + }, + languageSelectionWrapper: { + display: "flex", + flexDirection: "column", + textAlign: "center", + }, + languageSelectionBox: { + display: "flex", + paddingTop: 24, + [theme.breakpoints.up("sm")]: { + justifyContent: "flex-end", + }, + }, +})) + +export const ContentContainer = styled("div")(({ theme }) => ({ + display: "flex", + justifyContent: "center", + flexGrow: 1, + color: "#fff", + overflowY: "scroll", + padding: 100, + [theme.breakpoints.down("sm")]: { + padding: 50, + }, +})) +export const Content = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + width: "calc(100% - 32px)", + marginLeft: 16, + maxWidth: 1000, +})) + +export const Title = styled("div")({ + marginTop: 20, + fontSize: 36, + fontWeight: 600, + color: colors.grey[300], +}) + +export const Subtitle = styled("div")({ + fontSize: 18, + // fontWeight: "bold", + marginTop: 8, + color: colors.grey[500], +}) +export const ActionList = styled("div")({ marginTop: 48 }) +export const Action = styled("a")({ + display: "block", + color: colors.blue[500], + marginTop: 4, + cursor: "pointer", + textDecoration: "none", +}) +export const ActionTitle = styled("div")({ + // fontWeight: "bold", + fontSize: 24, + marginBottom: 8, + color: colors.grey[500], +}) +export const ActionText = styled("div")({ + color: colors.grey[300], + "& a": { + cursor: "pointer", + color: colors.blue[500], + textDecoration: "none", + }, +}) +export const Actionless = styled("div")({ + color: colors.grey[600], + paddingTop: 16, +}) +export const BottomSpacer = styled("div")({ height: 100 }) diff --git a/src/components/TransformPage/index.js b/src/components/TransformPage/index.js index 6e4f5ed6..d4a3aef0 100644 --- a/src/components/TransformPage/index.js +++ b/src/components/TransformPage/index.js @@ -99,16 +99,18 @@ export default () => { > Remove Invalid Samples - {plugins.map((plugin) => ( - - ))} + {plugins + .filter((plugin) => plugin.type === "transform") + .map((plugin) => ( + + ))} {openPlugin && (