diff --git a/examples/README.md b/examples/README.md index 0f48909d0..85e06883a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -145,15 +145,16 @@ This example builds further upon the Dynamic form example. Here we use presigned Putting it altogether, let's prepare and launch a task featuring a form containing one embedded file plus a few other fields. Here we'll assume working in directory `/mephisto/examples/form_composer_demo/data/dynamic_presigned_urls`. - Adjust `dynamic_presigned_urls_example_ec2_prolific.yaml` task config as needed -- Create `form_config.json` file to determine your form fields and layout +- Create `form_config.json` file to define your form fields and layout - it should contain a token named `file_location` - for more details see `mephisto/generators/form_composer/README.md` -- Create `separate_token_values_config.json` with desired token values (except embedded files) -- Remove content of folder `/tmp` (if you didn't shut the previous Task run correctly) +- Create `separate_token_values_config.json` with desired token values - Specify your AWS credentials - - Create file `docker/aws_credentials` and populate it with AWS keys info - - Populate your AWS credentials into `docker/envs/env.local` file -- Stand up docker containers: `docker-compose -f docker/docker-compose.local.vscode.yml up` + - Create file `docker/aws_credentials` and populate it with AWS keys info (for infrastructure and Mturk) + - Populate your AWS credentials into `docker/envs/env.local` file (for S3 access) + - Clone file `docker/docker-compose.dev.yml` as `docker/docker-compose.local.yml`, and point its `env_file` to `envs/env.local` +- Remove content of folder `/tmp` (if you didn't shut the previous Task run correctly) +- Launch docker containers: `docker-compose -f docker/docker-compose.local.yml up` - SSH into the running container: `docker exec -it mephisto_dc bash` - Generate your task data config with these commands: ```shell @@ -176,9 +177,9 @@ Putting it altogether, let's prepare and launch a task featuring a form containi ``` - Launch your task: ```shell - cd /mephisto/examples/form_composer_demo && python run_task_dynamic_ec2_prolific.py + cd /mephisto/examples/form_composer_demo && python run_task_dynamic_presigned_urls_ec2_prolific.py ``` -- After the Task is completed by all workers, launch task review app (for more details see `mephisto/review_app/README.md`): +- After the Task is completed by all workers, launch task review app and acces it at [http://localhost:8081](http://localhost:8081) (for more details see `mephisto/review_app/README.md`): ```shell mephisto review_app -h 0.0.0.0 -p 8000 -d True -f True ``` diff --git a/examples/form_composer_demo/run_task_dynamic_ec2_mturk_sandbox.py b/examples/form_composer_demo/run_task_dynamic_ec2_mturk_sandbox.py index dbd66cfd4..be90c5396 100644 --- a/examples/form_composer_demo/run_task_dynamic_ec2_mturk_sandbox.py +++ b/examples/form_composer_demo/run_task_dynamic_ec2_mturk_sandbox.py @@ -126,8 +126,8 @@ def generate_preview_html(): try: with open(data_config_path) as data_config_file: data_config_data = json.load(data_config_file) - except (JSONDecodeError, TypeError): - print(f"Could not read JSON from '{data_config_path}' file") + except (JSONDecodeError, TypeError) as e: + print(f"Could not read JSON from '{data_config_path}' file: {e}") raise first_form_version = data_config_data[0]["form"] diff --git a/examples/form_composer_demo/webapp/src/reviewapp.jsx b/examples/form_composer_demo/webapp/src/reviewapp.jsx index 6c810dfcc..3d01f2514 100644 --- a/examples/form_composer_demo/webapp/src/reviewapp.jsx +++ b/examples/form_composer_demo/webapp/src/reviewapp.jsx @@ -36,6 +36,10 @@ function ReviewApp() { } window.addEventListener("resize", updateSize); updateSize(); + // HACK: Catch-all resize, if normal resizes failed (e.g. acync long loading images) + setTimeout(() => { + updateSize(); + }, 3000); return () => window.removeEventListener("resize", updateSize); }, []); diff --git a/examples/remote_procedure/mnist/webapp/src/reviewapp.jsx b/examples/remote_procedure/mnist/webapp/src/reviewapp.jsx index 1bf4a0d6c..e5c498817 100644 --- a/examples/remote_procedure/mnist/webapp/src/reviewapp.jsx +++ b/examples/remote_procedure/mnist/webapp/src/reviewapp.jsx @@ -34,6 +34,10 @@ function ReviewApp() { } window.addEventListener("resize", updateSize); updateSize(); + // HACK: Catch-all resize, if normal resizes failed (e.g. acync long loading images) + setTimeout(() => { + updateSize(); + }, 3000); return () => window.removeEventListener("resize", updateSize); }, []); diff --git a/mephisto/abstractions/providers/prolific/api/messages.py b/mephisto/abstractions/providers/prolific/api/messages.py index cb4bf1454..902bbba39 100644 --- a/mephisto/abstractions/providers/prolific/api/messages.py +++ b/mephisto/abstractions/providers/prolific/api/messages.py @@ -60,5 +60,5 @@ def send(cls, study_id: str, recipient_id: str, text: str) -> Message: study_id=study_id, ) message.validate() - response_json = cls.post(cls.list_api_endpoint, params=message.to_dict()) - return Message(**response_json) + cls.post(cls.list_api_endpoint, params=message.to_dict()) + return message diff --git a/mephisto/abstractions/providers/prolific/prolific_worker.py b/mephisto/abstractions/providers/prolific/prolific_worker.py index 20c55b58f..440b2ba42 100644 --- a/mephisto/abstractions/providers/prolific/prolific_worker.py +++ b/mephisto/abstractions/providers/prolific/prolific_worker.py @@ -167,7 +167,7 @@ def unblock_worker(self, reason: str, requester: "Requester") -> Tuple[bool, str logger.debug(f"{self.log_prefix}Task Run: {task_run}") task_run_args = task_run.args - requester = cast("ProlificRequester", requester) + requester: "ProlificRequester" = cast("ProlificRequester", requester) client = self._get_client(requester.requester_name) prolific_utils.unblock_worker(client, task_run_args, self.worker_name, reason) self.datastore.set_worker_blocked(self.worker_name, is_blocked=False) @@ -182,7 +182,7 @@ def unblock_worker(self, reason: str, requester: "Requester") -> Tuple[bool, str def is_blocked(self, requester: "Requester") -> bool: """Determine if a worker is blocked""" task_run = self._get_first_task_run(requester) - requester = cast("ProlificRequester", requester) + requester: "ProlificRequester" = cast("ProlificRequester", requester) is_blocked = self.datastore.get_worker_blocked(self.get_prolific_participant_id()) logger.debug( @@ -256,7 +256,7 @@ def grant_crowd_qualification( """Grant qualification by the given name to this worker""" logger.debug(f"{self.log_prefix}Granting crowd qualification: {qualification_name}") - requester = cast( + requester: "ProlificRequester" = cast( "ProlificRequester", self.db.find_requesters(provider_type=self.provider_type)[-1], ) @@ -298,12 +298,11 @@ def revoke_crowd_qualification(self, qualification_name: str) -> None: def send_feedback_message(self, text: str, unit: "Unit") -> bool: """Send feedback message to a worker""" - requester = cast( + requester: "ProlificRequester" = cast( "ProlificRequester", self.db.find_requesters(provider_type=self.provider_type)[-1], ) - - assert isinstance(requester, ProlificRequester), "Must be an Prolific requester" + # assert isinstance(requester, ProlificRequester), "Must be an Prolific requester" client = self._get_client(requester.requester_name) datastore_unit = self.datastore.get_unit(unit.db_id) diff --git a/mephisto/generators/form_composer/config_validation/utils.py b/mephisto/generators/form_composer/config_validation/utils.py index edc16d090..a1865077e 100644 --- a/mephisto/generators/form_composer/config_validation/utils.py +++ b/mephisto/generators/form_composer/config_validation/utils.py @@ -50,8 +50,8 @@ def read_config_file( try: with open(config_path) as config_file: config_data = json.load(config_file) - except (JSONDecodeError, TypeError): - print(f"[red]Could not read JSON from file: '{config_path}'.[/red]") + except (JSONDecodeError, TypeError) as e: + print(f"[red]Could not read JSON from file: '{config_path}': {e}.[/red]") exit() return config_data diff --git a/mephisto/generators/form_composer/webapp/src/reviewapp.jsx b/mephisto/generators/form_composer/webapp/src/reviewapp.jsx index 8d4c30885..ad12a65d3 100644 --- a/mephisto/generators/form_composer/webapp/src/reviewapp.jsx +++ b/mephisto/generators/form_composer/webapp/src/reviewapp.jsx @@ -34,6 +34,10 @@ function ReviewApp() { } window.addEventListener("resize", updateSize); updateSize(); + // HACK: Catch-all resize, if normal resizes failed (e.g. acync long loading images) + setTimeout(() => { + updateSize(); + }, 3000); return () => window.removeEventListener("resize", updateSize); }, []); diff --git a/mephisto/review_app/README.md b/mephisto/review_app/README.md index 8cd312795..a95bc3e51 100644 --- a/mephisto/review_app/README.md +++ b/mephisto/review_app/README.md @@ -28,18 +28,22 @@ docker-compose -f docker/docker-compose.dev.yml run \ ``` where -- `--build` - build image before starting container -- `--publish 8081:8000` - docker port mapping, with `8000` being same port as in `-p` param -- `--rm` - automatically remove container when it already exits + +- `--build` - builds image before starting container +- `--publish 8081:8000` - maps docker ports, with `8000` being same port as in `-p` option +- `--rm` - automatically removes the previous container if it already exits - `mephisto_dc` - container name in `docker-compose.dev.yml` file -- `mephisto review_app -h 0.0.0.0 -p 8000 -d True` - launch Mephisto service inside container, where - - `-h/--host` - host where TaskReview app is going to be served - - `-p/--port` - port where TaskReview app is going to be served - - `-d/--debug` - debug mode - - `-f/--force-rebuild` - force rebuild React bundle (use if client code was updated between runs) - - `-s/--skip-build` - skip all installation and building steps for the UI, and directly launch the server - -Now open TaskReview app in your browser at [http://localhost:8081](http://localhost:8081). +- `mephisto review_app -h 0.0.0.0 -p 8000 -d True` - launches Mephisto's TaskReview app service inside the container + +Command `mephisto review_app` supports the following options: + +- `-h/--host` - host where TaskReview app will be served +- `-p/--port` - port where TaskReview app will be served +- `-d/--debug` - run in debug mode (with extra logging) +- `-f/--force-rebuild` - force rebuild React bundle (use if your Task client code has been updated) +- `-s/--skip-build` - skip all installation and building steps for the UI, and directly launch the server (use if no code has been changed) + +Now you can access TaskReview app in your browser at [http://localhost:8081](http://localhost:8081). --- diff --git a/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.css b/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.css index 4bcdd8728..af581e8d3 100644 --- a/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.css +++ b/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.css @@ -17,12 +17,16 @@ display: flex; align-items: center; min-height: 120px; - padding-bottom: 10px; + padding-left: 20px; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; } +.task-header .logo:hover { + opacity: 0.7; +} + .task-header .logo img { max-width: 100%; } @@ -32,7 +36,7 @@ } .task-header .table tr.total td { - color: #a6a6a6; + color: #aaaaaa; } .task-header .table tr th.center, @@ -46,6 +50,14 @@ line-height: 0.8; } +.task-header .table td .percentage { + font-size: 85%; +} + +.task-header .table tr td:nth-child(1) { + border-right: 1px solid grey; +} + .task-header .table .title b { display: inline-block; line-height: 2; diff --git a/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.tsx b/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.tsx index 5d84eb00d..85be2b85f 100644 --- a/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.tsx +++ b/mephisto/review_app/client/src/pages/TaskPage/TaskHeader/TaskHeader.tsx @@ -11,27 +11,60 @@ import logo from "static/images/logo.svg"; import urls from "urls"; import "./TaskHeader.css"; -interface PropsType { +interface TaskHeaderPropsType { + loading: boolean; taskStats?: TaskStatsType; workerId?: number; workerStats?: WorkerStatsType; } -function TaskHeader(props: PropsType) { - const wStats = props.workerStats; - const tStats = props.taskStats; +interface StatCountWithPercentagePropsType { + count: number; + totalCount: number; +} + +function StatCountWithPercentage(props: StatCountWithPercentagePropsType) { + const { totalCount, count } = props; - const toPercent = (total: number, value: number): number => { + function toPercent(total: number, value: number): number { return total !== 0 ? Math.round((value * 100) / total) : 0; - }; + } + + return ( + totalCount !== null ? ( + <> + {count} + {" "} + + ({toPercent(totalCount, count)}%) + + + ) : ( + <> + -- + + ) + ); +} + +function TaskHeader(props: TaskHeaderPropsType) { + const wStats = props.workerStats; + const tStats = props.taskStats; return ( - + {!props.loading ? ( + + logo + + ) : ( logo - + )} {wStats && tStats && ( @@ -70,44 +103,19 @@ function TaskHeader(props: PropsType) { )} - {wStats.total_count !== null ? ( - <> - {wStats.approved_count} ( - {toPercent(wStats.total_count, wStats.approved_count)}%) - - ) : ( - <> - -- - - )} + - {wStats.total_count !== null ? ( - <> - {wStats.soft_rejected_count} ( - {toPercent( - wStats.total_count, - wStats.soft_rejected_count - )} - %) - - ) : ( - <> - -- - - )} + - {wStats.total_count !== null ? ( - <> - {wStats.rejected_count} ( - {toPercent(wStats.total_count, wStats.rejected_count)}%) - - ) : ( - <> - -- - - )} + @@ -126,44 +134,19 @@ function TaskHeader(props: PropsType) { )} - {tStats.total_count !== null ? ( - <> - {tStats.approved_count} ( - {toPercent(tStats.total_count, tStats.approved_count)}%) - - ) : ( - <> - -- - - )} + - {tStats.total_count !== null ? ( - <> - {tStats.soft_rejected_count} ( - {toPercent( - tStats.total_count, - tStats.soft_rejected_count - )} - %) - - ) : ( - <> - -- - - )} + - {tStats.total_count !== null ? ( - <> - {tStats.rejected_count} ( - {toPercent(tStats.total_count, tStats.rejected_count)}%) - - ) : ( - <> - -- - - )} + diff --git a/mephisto/review_app/client/src/pages/TaskPage/TaskPage.css b/mephisto/review_app/client/src/pages/TaskPage/TaskPage.css index ea1b44afe..5b3b71c37 100644 --- a/mephisto/review_app/client/src/pages/TaskPage/TaskPage.css +++ b/mephisto/review_app/client/src/pages/TaskPage/TaskPage.css @@ -5,28 +5,33 @@ */ /* Buttons */ -.task .buttons { +.task .review-board { box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; - padding: 10px; + padding: 10px 20px; background-color: #ecdadf; display: flex; - gap: 10px; } -/* Content */ -.task .content { +.task .review-board .left-side { + flex: 0 50%; +} + +.task .review-board .left-side .review-controls { display: flex; - flex-direction: column; - padding: 20px; + flex-direction: row; + gap: 10px; } -.task .content pre { - white-space: normal; +.task .review-board .right-side { + flex: 0 50%; + display: flex; + align-items: center; + justify-content: flex-end; } -.task .content .info { +.task .review-board .right-side .info { display: flex; flex-direction: row; gap: 20px; @@ -34,12 +39,26 @@ color: #ccc; } -.task .content .info .info-title { - color: #ccc; +.task .review-board .right-side .info .grey { + color: #999; } -.task .content .question { - margin-top: 20px; +.task .review-board .right-side .info .black { + color: black; +} + +/* Content */ +.task .content { + display: flex; + flex-direction: column; + padding: 20px; +} + +.task .content pre { + white-space: normal; +} + +.task .content .unit-preview-container { position: relative; -webkit-user-select: none; /* Safari */ -ms-user-select: none; /* IE 10 and IE 11 */ @@ -47,29 +66,15 @@ cursor: default; } -.task .content .question .images { - position: absolute; - display: flex; - flex-direction: row; - bottom: 29px; - left: 0; - background: none; - z-index: 999; - height: 393px; -} - -.task .content .question .images img { - float: left; - padding: 6px; - width: 262px; - height: 262px; - cursor: default; -} - .task .content .results { - margin-top: 20px; + margin-bottom: 20px; background-color: #f5f5f5; padding: 10px 30px; + border-radius: 5px; +} + +.task .content .results:hover { + background-color: #eeeeee; } .task .content .results .results-header { @@ -80,8 +85,9 @@ display: inline-block; margin-left: 10px; font-style: normal; - font-size: 50px; - line-height: 1; + font-size: 40px; + line-height: 0.5; + } .task .content .results .results-closed{ @@ -113,12 +119,14 @@ justify-content: center; } -.task-iframe { +.unit-preview-iframe { width: 100%; } .json-pretty .__json-pretty__ { white-space: pre !important; + border-top: 1px solid #cccccc; + padding-top: 15px; } .json-pretty .__json-pretty__ .__json-key__ { diff --git a/mephisto/review_app/client/src/pages/TaskPage/TaskPage.tsx b/mephisto/review_app/client/src/pages/TaskPage/TaskPage.tsx index af0ef8be6..35e3e0af3 100644 --- a/mephisto/review_app/client/src/pages/TaskPage/TaskPage.tsx +++ b/mephisto/review_app/client/src/pages/TaskPage/TaskPage.tsx @@ -104,8 +104,9 @@ function TaskPage(props: PropsType) { const [unitInputsIsJSON, setUnitInputsIsJSON] = React.useState(false); const [unitResultsIsJSON, setUnitResultsIsJSON] = React.useState(false); - const [inputsVisibility, setInputsVisibility] = React.useState(false); - const [resultsVisibility, setResultsVisibility] = React.useState(false); + // Allow `null` state so that non-null values persist between task units + const [inputsVisibility, setInputsVisibility] = React.useState(null); + const [resultsVisibility, setResultsVisibility] = React.useState(null); window.onmessage = function (e) { if ( @@ -118,9 +119,7 @@ function TaskPage(props: PropsType) { } }; - const onGetTaskWorkerUnitsIdsSuccess = ( - workerUnitsIds: WorkerUnitIdType[] - ) => { + const onGetTaskWorkerUnitsIdsSuccess = (workerUnitsIds: WorkerUnitIdType[]) => { setWorkerUnits(() => { const workerUnitsMap = {}; @@ -355,22 +354,22 @@ function TaskPage(props: PropsType) { // [RECEIVING WIDGET DATA] // --- - const sendDataToTaskIframe = (data: object) => { + const sendDataToUnitIframe = (data: object) => { const reviewData = { REVIEW_DATA: { inputs: data["prepared_inputs"], outputs: data["outputs"], }, }; - const taskIframe = iframeRef.current; - taskIframe.contentWindow.postMessage(JSON.stringify(reviewData), "*"); + const unitIframe = iframeRef.current; + unitIframe.contentWindow.postMessage(JSON.stringify(reviewData), "*"); }; // --- // Effects useEffect(() => { // Set default title - setPageTitle("Mephisto - Task Review - Task"); + setPageTitle("Mephisto - Task Review - Current Task"); setFinishedTask(false); if (task === null) { @@ -466,7 +465,7 @@ function TaskPage(props: PropsType) { // --- useEffect(() => { if (iframeLoaded && currentUnitDetails?.has_task_source_review) { - sendDataToTaskIframe(currentUnitDetails); + sendDataToUnitIframe(currentUnitDetails); } }, [currentUnitDetails, iframeLoaded]); // --- @@ -483,6 +482,18 @@ function TaskPage(props: PropsType) { if (typeof unitOutputs === "object") { setUnitResultsIsJSON(true); } + + // If Task expressly does not provide a preview template, + // we just simply show JSON data for the Unit. + // Change values only one time on loading page to save user choice + if (currentUnitDetails.has_task_source_review === false) { + if (inputsVisibility === null) { + setInputsVisibility(false); + } + if (resultsVisibility === null) { + setResultsVisibility(true); + } + } } }, [currentUnitDetails]); @@ -496,26 +507,66 @@ function TaskPage(props: PropsType) { taskStats={taskStats} workerStats={workerStats} workerId={unitsOnReview ? currentWorkerOnReview : null} + loading={loading} /> -
- {!finishedTask ? ( - <> - - - - - ) : ( -
- No units left for this task. Redirecting to the list of tasks. -
- )} +
+
+ {!finishedTask ? ( +
+ + + +
+ ) : ( +
+ No unreviewed units left for this task.
+ Redirecting to the list of tasks. +
+ )} +
+ +
+ {/* Unit info */} + {unitDetails && currentUnitOnReview && ( +
+ {currentUnitDetails && ( + <> +
+ Task ID: {currentUnitDetails.task_id} +
+
+ Worker ID: {currentUnitDetails.worker_id} +
+
+ Unit ID: {currentUnitDetails.id} +
+ + )} +
+ )} +
@@ -528,29 +579,20 @@ function TaskPage(props: PropsType) {
)} - {/* Unit info */} - {unitDetails && currentUnitOnReview && ( -
- {currentUnitDetails && ( - <> -
Unit ID: {currentUnitDetails.id}
-
Task ID: {currentUnitDetails.task_id}
-
Worker ID: {currentUnitDetails.worker_id}
- - )} -
- )} - {currentUnitDetails?.inputs && ( <> - {/* Initial parameters */} + {/* Initial Unit parameters */}
-

setInputsVisibility(!inputsVisibility)}> - Initial Parameters +

setInputsVisibility(!inputsVisibility)} + title={"Toggle initial Unit parameters data"} + > + Initial Parameters {inputsVisibility ? <>▾ : <>▸} -

+
{unitInputsIsJSON ? ( @@ -573,12 +615,16 @@ function TaskPage(props: PropsType) { <> {/* Results */}
-

setResultsVisibility(!resultsVisibility)}> - Results +

setResultsVisibility(!resultsVisibility)} + title={"Toggle Unit results data"} + > + Results {resultsVisibility ? <>▾ : <>▸} -

+
{unitResultsIsJSON ? ( @@ -595,23 +641,18 @@ function TaskPage(props: PropsType) {
- {/* Task info */} -
e.preventDefault()}> - {currentUnitDetails.has_task_source_review ? ( + {/* Completed Unit preview */} +
e.preventDefault()}> + {currentUnitDetails.has_task_source_review && (