Skip to content

Commit

Permalink
Feature: Table Editing (#40)
Browse files Browse the repository at this point in the history
* Initial setup for CSV Task

* First pass at a CSV Preview component completed (for sample input)

* First pass at inputs for CSV finished

* Table editing first pass completed

* minor cleanup

* Code cleanup

* Fixing a bug with getting task names, particularly for UploadInputsTab

* Deleted unnecessary comments

* Minor bug fix for Video Classification Sample Input, other changes
  • Loading branch information
walkingtowork authored Aug 20, 2024
1 parent 0b6e56e commit d9e34b2
Show file tree
Hide file tree
Showing 35 changed files with 2,917 additions and 50 deletions.
50 changes: 36 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,54 @@ The project is structured as follows:
## Recommended Workflow for creating new Tasks (WIP)
- Add the task name to `TaskIDs.js`. Use camelcase for the variable and snakecase for the string
- Make a new directory in `Outputs` and create:
- `testData` subdirectory
- `testTaskNameOutput.js`
- `TaskNameOutput.js`
- `TaskName.stories.js`
- `TaskName.scss`
- Create a new `Task` in `Task.js`
- Search svgrepo.com for a suitable icon and add it to the `icons` folder (see below for more instructions)
- Add the new task to:
- `getStaticTask`
- `getStaticTasks`
- `getDefaultModel`
- Open `DefaultModels.js` and copy/paste one of the existing models (editing where appropriate)
- `getSampleOutput`
- Return the `Test[Task Name]Output` that you created in `testData` above
- Return the `Test[Task Name]Output` that you created in `testData` above
- `getStaticTasks`
- Return the `Test[Task Name]Output` that you created in `testData` (see next bullet point)
- Make a new directory in `Outputs` and create:
- `testData` subdirectory
- `testTaskNameOutput.js`
- `TaskNameOutput.js`
- `TaskName.stories.js`
- `TaskName.scss`
- Create a new story for the task in `QuickInput.stories.js`
- If the user is able to upload files for this task, add the task to `UppyFileTypeCheckerPlugin`
- Add the new task to `ModelDetailPage` in `getSampleInputs` and `getInputType`
- If the task requires new input types, you will need to add those
- If the task requires new input type(s), you will need to add those:
- Add the new input type to `TaskInputTypes` and `QuickInputType`
- Update `SampleInputsTab` as necessary
- Open `useQuickInputControl.js`
- Update `getInputTabType` and `getUploadTabType` with the new `QuickInputType`
- Create a new `[InputType]InputTab` file
- You can look at the closed/merged Pull Requests for tasks like Table Editing or Audio to Text for examples of new Input Tabs.
- You can temporarily switch the default state for `selectedTab` to the index of your new tab, so that you don't need to repeatedly switch tabs through page refreshes
- Update `SampleInputsTab`:
- Open `sampleImages.js` and add `Sample[TaskName]Inputs`. If your task is using `useMultiInput` then you will need to make a parent array, and then for each input type another array of sample input objects.
- Add the new input type to `makeSampleInput`
- Create a `makeSample[input type]Input` function
- Add the new input type with appropriate text to `makeTaskTitle`
- Update the `SampleInputsTab.scss` file with styling for what the selected/unselected states of the new input type should look like
- Update `UploadInputsTab` as necessary
- If the user is able to upload files for this task, add the task ID to `getAllowedFileTypes` in `UppyFileTypeCheckerPlugin` so that Uppy will only allow the correct file types to be selected/sent to the server
- Update `URLInputsTab` as necessary
- Be sure to read the `IMPORTANT` comment before editing the inputs
- There are multiple paths in the render function depending on if your input is a `useMultipleInput`, `multiple`, etc. - if you change one, you will likely need to update all of them.
...WIP?

- Add the new task to `ModelDetailPage` in `getSampleInputs` and `getInputType`

- If you haven't already done so, create the various `Output` files above, then:
- Update `QuickOutput` with your new Output
- In the `makeOutput` function, add a new case statement for your new Output
- Generally you will only need to pass through the `trialOutput` and `onBackClicked` but if you need to be able to re-run the trial on the same page (for example with Conversations) then you would also add a prop here for `runTrial`.
- ...to be continued
- Additional Notes:
- To test the upload dashboard in storybook, open `useUploadInputControl` and:
- Find the text `COMMENT THIS OUT BEFORE COMMITTING` and uncomment it.
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"jest-resolve": "^27.4.2",
"jest-watch-typeahead": "^2.2.2",
"mini-css-extract-plugin": "^2.4.5",
"papaparse": "^5.4.1",
"postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1",
Expand Down
14 changes: 13 additions & 1 deletion src/components/Experiment/QuickInput/QuickInput.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
audioClassification,
videoClassification,
maskGeneration,
tableEditing
} from "../../../helpers/TaskIDs";
import {
SampleImageClassificationInputs,
Expand All @@ -44,8 +45,9 @@ import {
SampleAudioClassificationInputs,
SampleVideoClassificationInputs,
SampleMaskGenerationInputs,
SampleTableEditingInputs
} from "../../../helpers/sampleImages";
import { QuickInputType } from "./quickInputType";

import { TaskInputTypes } from "../../../helpers/TaskInputTypes";

export default {
Expand Down Expand Up @@ -317,4 +319,14 @@ VideoClassification.args = {
type: videoClassification,
},
},
};

export const TableEditing = Template.bind({});
TableEditing.args = {
sampleInputs: SampleTableEditingInputs,
model: {
output: {
type: tableEditing,
},
},
};
120 changes: 120 additions & 0 deletions src/components/Experiment/QuickInput/Tabs/CsvInput/CsvInputsTab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';

import useBEMNaming from "../../../../../common/useBEMNaming";
import Task from '../../../../../helpers/Task';
import CsvIcon from "../../../../../resources/icons/icon-csv-file.svg";

import "./CsvInputsTab.scss";

const csvHeaders = ["a", "b", "c", "d", "e", "f"];
const emptyCsv = [
["", "", "", "", "", ""],
["", "", "", "", "", ""],
["", "", "", "", "", ""],
["", "", "", "", "", ""],
["", "", "", "", "", ""],
["", "", "", "", "", ""],
];
const emptyCsvString = ',,,,,\n,,,,,\n,,,,,\n,,,,,\n,,,,,\n,,,,,';

export default function CsvInputsTab(props) {
const task = Task.getStaticTask(props.task);

const { getElement, getBlock } = useBEMNaming('csv-inputs');

const taskName = (task.useMultiInput ? (task.inputs[props.inputIndex]?.inputType) : task.inputType || '').toLowerCase();
// Note: Currently using both new and old way of handling inputs but should refactor in the future
const inputText = task.inputText || props.input.inputText;

const [csvData, setCsvData] = useState(emptyCsv);

useEffect(() => {
const csvString = stringifyCsvData();
if (csvString !== emptyCsvString) {
props.inputSelected(csvString, props.inputIndex);
}
}, [csvData])

const updateCell = (event, rowIndex, colIndex) => {
if (event.persist)
event.persist();

let csvCopy =[...csvData];
csvCopy[rowIndex][colIndex] = event.target.value;
setCsvData(csvCopy);
}

const stringifyCsvData = () => {
return csvData.map((row) => row.join(',')).join('\n');
}

const downloadCsv = () => {
const fileType = 'text/csv';
const fileName = 'text-input.csv';
const csvString = stringifyCsvData();
const blob = new Blob([csvString], { type: fileType });

let a = document.createElement('a');
a.download = fileName;
a.href = URL.createObjectURL(blob);
a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
a.style.display = "none";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(a.href); }, 1500);
}

return (
<div className={getBlock()}>
<div className={getElement('title')}>
<b>Manually enter {taskName} contents</b>
{" "}to {inputText.toLowerCase()}
</div>
<div className={getElement('container')}>
<div className={getElement('csv-header')}>
{ csvHeaders.map((header) => {
return (
<div className={getElement('col-header')} key={`row-header-${header}`}>
{header}
</div>
)
})}
</div>
<div className={getElement('table')}>
{ csvData.map((row, rowIndex) => {
return (
<div className={getElement('row')} key={`row-${rowIndex}`}>
<p className={getElement('row-label')}>
{rowIndex}
</p>

{ row.map((cell, colIndex) => {
return (
<div className={getElement('cell')} key={`cell-${rowIndex}-${colIndex}`}>
<input
className={getElement('text-input')}
type="text"
value={cell}
onChange={(event) => (updateCell(event, rowIndex, colIndex))}
/>
</div>
)
})}
</div>
)
})}
</div>
<div>
<button
disabled={csvData === emptyCsv}
className={getElement('download-csv-button')}
onClick={() => downloadCsv()}>
<img src={CsvIcon} alt="download-csv-icon" />
<p>Download</p>
</button>
</div>
</div>
</div>
)
}
106 changes: 106 additions & 0 deletions src/components/Experiment/QuickInput/Tabs/CsvInput/CsvInputsTab.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
@import "src/App";

$csv-label-size: 18px;

.csv-inputs {
display: flex;
flex-direction: column;
width: 100%;

&__title {
@include body;
}

&__container {
margin-top: 24px;
padding: 20px;
width: 100%;
display: flex;
flex-direction: column;

&::placeholder {
opacity: .9;
}
}

&__csv-header {
display: flex;
flex-direction: row;
padding-left: $csv-label-size // Indentation equal to width of row labels

}
&__col-header {
width: 151px;
padding-left: 5px;
border-top: 1px solid $charcoalLightest;
border-left: 1px solid $charcoalLightest;
background-color: $smokeDarkest;
font-weight: 800;
}
&__col-header:last-of-type {
width: 152px;
border-right: 1px solid $charcoalLightest;
}

&__table {
display: flex;
flex-direction: column;
font-size: 13px;
}

&__row {
display: flex;
flex-direction: row;
}
&__row:first-child {
p:first-of-type {
border-top: 1px solid $charcoalLightest;
}
}
&__row:first-of-type &__cell {
border-top: 1px solid $charcoalLightest;
}
&__row:last-of-type {
border-bottom: none;
}
&__row-label {
width: $csv-label-size;
background-color: $smokeDarkest;
font-weight: 800;
border-bottom: 1px solid $charcoalLightest;
border-left: 1px solid $charcoalLightest;

display: flex;
justify-content: center;
align-items: center;
}

&__cell {
border-right: 1px solid $charcoalLightest;
border-bottom: 1px solid $charcoalLightest;

input {
width: 150px;
padding-left: 5px;
}
}
&__cell:first-of-type {
border-left: 1px solid $charcoalLightest;
}

&__download-csv-button {
border: 1px solid $charcoalLightest;
border-radius: 5px;
margin-top: 20px;
padding: 5px;
font-weight: 600;
color: $charcoalLightest;
img {
height: 50px;
}
p {
margin-top: 5px;
font-size: 12px;
}
}
}
Loading

0 comments on commit d9e34b2

Please sign in to comment.