Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AiLab] Model Manager Dialog and autogenerated design elements and code #38664

Merged
merged 43 commits into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5af4d1e
beginnings of a generic ml predict block
Erin007 Jan 6, 2021
4ed5c30
stop trying to use a dropdown for model names
Erin007 Jan 7, 2021
cd853b3
put prediction block behind applab-ml flag
Erin007 Jan 7, 2021
a018f1c
App Lab finds the user's most recently saved model and autogenerates …
Erin007 Jan 11, 2021
1a196bf
prepend design_ to ids, append onClick starter code
Erin007 Jan 12, 2021
fe30108
hack the dupe ids, layout design elements so they don't overlap
Erin007 Jan 13, 2021
74a8b96
merge in applab-model-load branch to access getPredict block
Erin007 Jan 13, 2021
7360835
get a prediction back from the trained model
Erin007 Jan 15, 2021
c2b38fc
tidy autogen code to prevent App Lab warnings
Erin007 Jan 15, 2021
1b931ab
put autogen function call behind experiment flag
Erin007 Jan 15, 2021
5387166
use localStorage to prevent re-gen of design elements and code on pag…
Erin007 Jan 15, 2021
8ee19f7
add manage models as an option in the cog menu
Erin007 Jan 19, 2021
a4ad3dc
ModelManagerDialog component open/close
Erin007 Jan 20, 2021
a693b39
Import button that autocreates starter code and design elements
Erin007 Jan 20, 2021
db7ce74
dropdown to select model to import
Erin007 Jan 20, 2021
14a6519
remove references to the allowAutoGenElements dead end boolean
Erin007 Jan 21, 2021
568be9e
Merge branch 'staging' into model-manager
Erin007 Feb 1, 2021
a92018d
hoist spacing pixel amount into variable
Erin007 Feb 1, 2021
26a9139
import jquery
Erin007 Feb 1, 2021
c561c6b
rely on model_id, don't grab the user's most recent model
Erin007 Feb 1, 2021
4425ebc
disable import button if the user doesn't have any trained models
Erin007 Feb 1, 2021
05f8cca
pass model id as a param to handle duplicate names and reduce ajax calls
Erin007 Feb 1, 2021
4354b6c
spinner and async for autogen code and elements
Erin007 Feb 2, 2021
f2f07e5
Merge branch 'staging' into model-manager
Erin007 Feb 2, 2021
6fd6500
lazy load the ModelManagerDialog
Erin007 Feb 3, 2021
72a2943
move autogen function into standalone file to keep applab.js tidier a…
Erin007 Feb 3, 2021
57878c8
fix SettingsCogTest by removing loabable to avoid the spinner icon
Erin007 Feb 4, 2021
2348784
stop using fat arrow to maybe? appease Uglify
Erin007 Feb 4, 2021
30d1396
pass autogenerateML down through the component tree
Erin007 Feb 4, 2021
7b91f3b
transpile all of the ml packages that have lib and lib-es6
Feb 9, 2021
52cf24b
don't require func
Erin007 Feb 9, 2021
15f5245
debugging thumbnail generation
Erin007 Feb 10, 2021
bf54c02
debugging thumbnails
Erin007 Feb 10, 2021
c313d29
Merge branch 'staging' into model-manager
breville Feb 11, 2021
90a6371
clean up logs, un-skip tests, only render modelmanager dialog if expe…
Erin007 Feb 11, 2021
5a422f1
allow non-owners to access models in shared projects, protect against…
Erin007 Feb 12, 2021
5144b3d
seperate UI generation from ajax call
Erin007 Feb 12, 2021
c2c4983
reduce # of times ml_model/names is called
Erin007 Feb 12, 2021
96f869c
surface failure in app lab
Erin007 Feb 13, 2021
d2e4a97
remove changes to sharedialog
Erin007 Feb 13, 2021
3827a44
remove console.log
Erin007 Feb 13, 2021
bea44e7
remove changes to sharealloweddialog
Erin007 Feb 13, 2021
66b4898
remove stray change
Erin007 Feb 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/src/MLTrainers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const KNN = require('ml-knn');
import KNN from 'ml-knn';

const KNNTrainers = ['knnClassify', 'knnRegress'];

Expand Down Expand Up @@ -31,18 +31,20 @@ modelData = {
}

value in testData is the converted algorithm-ready number, not the string

TODO: convert string data in testValues using the featureNumberKey
*/
export function predict(modelData) {
// Determine which algorithm to use.
if (KNNTrainers.includes(modelData.selectedTrainer)) {
// Re-instantiate the trained model.
const model = KNN.load(modelData.trainedModel);
// Prepare test data.
const testValues = modelData.selectedFeatures.map(
feature => modelData.testData[feature]
const testValues = modelData.selectedFeatures.map(feature =>
parseInt(modelData.testData[feature])
);
// Make a prediction.
const rawPrediction = model.predict(testValues)[0];
const rawPrediction = model.predict(testValues);
// Convert prediction to human readable (if needed)
const prediction = Object.keys(modelData.featureNumberKey).includes(
modelData.labelColumn
Expand Down
2 changes: 2 additions & 0 deletions apps/src/applab/AppLabView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ExternalRedirectDialog from '@cdo/apps/applab/ExternalRedirectDialog';
class AppLabView extends React.Component {
static propTypes = {
handleVersionHistory: PropTypes.func.isRequired,
autogenerateML: PropTypes.func.isRequired,
hasDataMode: PropTypes.bool.isRequired,
hasDesignMode: PropTypes.bool.isRequired,
interfaceMode: PropTypes.oneOf([
Expand Down Expand Up @@ -57,6 +58,7 @@ class AppLabView extends React.Component {
<CodeWorkspace
withSettingsCog
style={{display: codeWorkspaceVisible ? 'block' : 'none'}}
autogenerateML={this.props.autogenerateML}
/>
{this.props.hasDesignMode && <ProtectedDesignWorkspace />}
{this.props.hasDataMode && (
Expand Down
70 changes: 70 additions & 0 deletions apps/src/applab/ai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import $ from 'jquery';
import designMode from './designMode';

export default function autogenerateML(modelId) {
return new Promise(function(resolve, reject) {
$.ajax({
url: `/api/v1/ml_models/${modelId}`,
method: 'GET'
})
.then(modelData => {
var x = 20;
Erin007 marked this conversation as resolved.
Show resolved Hide resolved
var y = 20;
var SPACE = 20;
designMode.onInsertEvent(`var testValues = {};`);
modelData.selectedFeatures.forEach(feature => {
y = y + SPACE;
var label = designMode.createElement('LABEL', x, y);
label.textContent = feature + ':';
label.id = 'design_' + feature + '_label';
label.style.width = '300px';
y = y + SPACE;
if (Object.keys(modelData.featureNumberKey).includes(feature)) {
var selectId = feature + '_dropdown';
var select = designMode.createElement('DROPDOWN', x, y);
select.id = 'design_' + selectId;
// App Lab automatically addss "option 1" and "option 2", remove them.
select.options.remove(0);
select.options.remove(0);
Object.keys(modelData.featureNumberKey[feature]).forEach(option => {
var optionElement = document.createElement('option');
optionElement.text = option;
select.options.add(optionElement);
});
} else {
var input = designMode.createElement('TEXT_INPUT');
input.id = 'design_' + feature + '_input';
}
var addFeature = `testValues.${feature} = getText("${selectId}");`;
designMode.onInsertEvent(addFeature);
});
y = y + 2 * SPACE;
var label = designMode.createElement('LABEL', x, y);
label.textContent = modelData.labelColumn;
// TODO: this could be problematic if the name isn't formatted appropriately
label.id = 'design_' + modelData.name + '_label';
label.style.width = '300px';
y = y + SPACE;
var predictionId = modelData.name + '_prediction';
var prediction = designMode.createElement('TEXT_INPUT', x, y);
prediction.id = 'design_' + predictionId;
y = y + 2 * SPACE;
var predictButton = designMode.createElement('BUTTON', x, y);
predictButton.textContent = 'Predict';
var predictButtonId = modelData.name + '_predict';
designMode.updateProperty(predictButton, 'id', predictButtonId);
var predictOnClick = `onEvent("${predictButtonId}", "click", function() {
getPrediction("${
modelData.name
}", "${modelId}", testValues, function(value) {
setText("${predictionId}", value);
});
});`;
Copy link
Member

@breville breville Feb 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to expose errors in prediction to the user writing an app? (Both so that they can debug, and also so that they can give information back to the user running their app.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen Shot 2021-02-12 at 4 55 53 PM

Screen Shot 2021-02-12 at 5 02 20 PM

I made it so that we're displaying the error if the prediction fails; do this seem reasonable? At least until we get more feedback?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a good approach for now.

designMode.onInsertEvent(predictOnClick);
return resolve();
})
.fail((jqXhr, status) => {
return alert({message: 'An error occurred'});
});
});
}
9 changes: 9 additions & 0 deletions apps/src/applab/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,12 @@ export function drawChartFromRecords(
callback: callback
});
}

export function getPrediction(modelName, modelId, testValues, callback) {
return Applab.executeCmd(null, 'getPrediction', {
modelName,
modelId,
testValues,
callback
});
}
4 changes: 3 additions & 1 deletion apps/src/applab/applab.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {setExportGeneratedProperties} from '../code-studio/components/exportDial
import {userAlreadyReportedAbuse} from '@cdo/apps/reportAbuse';
import {workspace_running_background, white} from '@cdo/apps/util/color';
import {MB_API} from '../lib/kits/maker/boards/microBit/MicroBitConstants';
import autogenerateML from '@cdo/apps/applab/ai';

/**
* Create a namespace for the application.
Expand Down Expand Up @@ -973,7 +974,8 @@ Applab.render = function() {
isEditingProject: project.isEditing(),
screenIds: designMode.getAllScreenIds(),
onScreenCreate: designMode.createScreen,
handleVersionHistory: Applab.handleVersionHistory
handleVersionHistory: Applab.handleVersionHistory,
autogenerateML: autogenerateML
});
ReactDOM.render(
<Provider store={getStore()}>
Expand Down
2 changes: 2 additions & 0 deletions apps/src/applab/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../lib/util/javascriptMode';
import {commands as audioCommands} from '@cdo/apps/lib/util/audioApi';
import {commands as timeoutCommands} from '@cdo/apps/lib/util/timeoutApi';
import {commands as mlCommands} from '@cdo/apps/lib/util/mlApi';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a lot of "ml" related naming, but it sounds like we are going to trend towards "ai" instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - it'd be good to get all on the same page about that and update the code to match. Ok to do as a rename/refactor after this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds fine.

import * as makerCommands from '@cdo/apps/lib/kits/maker/commands';
import {getAppOptions} from '@cdo/apps/code-studio/initApp/loadApp';
import {AllowedWebRequestHeaders} from '@cdo/apps/util/sharedConstants';
Expand Down Expand Up @@ -2291,4 +2292,5 @@ function stopLoadingSpinnerFor(elementId) {
// Include playSound, stopSound, etc.
Object.assign(applabCommands, audioCommands);
Object.assign(applabCommands, timeoutCommands);
Object.assign(applabCommands, mlCommands);
Object.assign(applabCommands, makerCommands);
11 changes: 11 additions & 0 deletions apps/src/applab/dropletConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './setPropertyDropdown';
import {getStore} from '../redux';
import * as applabConstants from './constants';
import experiments from '../util/experiments';

var DEFAULT_WIDTH = applabConstants.APP_WIDTH.toString();
var DEFAULT_HEIGHT = (
Expand Down Expand Up @@ -1131,6 +1132,16 @@ export var blocks = [
}
];

if (experiments.isEnabled(experiments.APPLAB_ML)) {
blocks.push({
func: 'getPrediction',
parent: api,
category: 'Data',
paletteParams: ['model_name', 'model_id', 'testValues', 'callback'],
params: ['"myModel"', '"modelId"', 'testValues', 'function (value) {\n \n}']
});
}

export const categories = {
'UI controls': {
id: 'uicontrols',
Expand Down
77 changes: 77 additions & 0 deletions apps/src/code-studio/components/ModelManagerDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import PropTypes from 'prop-types';
import React from 'react';
import $ from 'jquery';
import BaseDialog from '@cdo/apps/templates/BaseDialog';
import Button from '@cdo/apps/templates/Button';

export default class ModelManagerDialog extends React.Component {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this dialog will live next to and be most similar to these two components, so just linking those here in case it's helpful:
<LibraryManagerDialog/>
<AssetManager/>

The LibraryManagerDialog is newer and more consistent with our newer styles, so i'd go with that one if you're trying to decide how to style something (related to our convo here)

static propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
autogenerateML: PropTypes.func
};

state = {
models: [],
isImportPending: false
};

componentDidUpdate() {
this.getModelList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running a regular AppLab app, we keep on hitting /api/v1/ml_models/names. This component seems to be updating more than expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

componentDidMount() instead of componentDidUpdate() 🤦‍♀️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems to fire once, but due to the lifetime management of these dialogs (they live forever, just showing and hiding), it doesn't refresh the model list each time the dialog is opened. I have implemented what seems to be a solution in #39041.

}

closeModelManager = () => {
this.props.onClose();
};

getModelList = () => {
$.ajax({
url: '/api/v1/ml_models/names',
method: 'GET'
}).then(models => {
this.setState({models});
});
};

importMLModel = async () => {
this.setState({isImportPending: true});
const modelId = this.root.value;
await this.props.autogenerateML(modelId);
this.setState({isImportPending: false});
this.closeModelManager();
};

render() {
const {isOpen} = this.props;
const noModels = this.state.models.length === 0;

return (
<div>
<BaseDialog
isOpen={isOpen}
handleClose={this.closeModelManager}
useUpdatedStyles
>
<h2>Machine Learning Models</h2>
<select name="model" ref={element => (this.root = element)}>
{this.state.models.map(model => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</select>
{noModels && <div>You have not trained any AI models yet.</div>}
<Button
text={'Import'}
color={Button.ButtonColor.orange}
onClick={this.importMLModel}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function does most of its work asynchronously, should we show a spinner or something similar while it does its work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen.Recording.2021-02-01.at.9.29.13.PM.mov

disabled={noModels}
isPending={this.state.isImportPending}
pendingText={'Importing...'}
/>
<h3>Model card details will go here.</h3>
</BaseDialog>
</div>
);
}
}
1 change: 0 additions & 1 deletion apps/src/code-studio/components/ShareAllowedDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ class ShareAllowedDialog extends React.Component {
this.props.appType === 'applab' || this.props.appType === 'gamelab';
const artistTwitterHandle =
SongTitlesToArtistTwitterHandle[this.props.selectedSong];

const hasThumbnail = !!this.props.thumbnailUrl;
const thumbnailUrl = hasThumbnail
? this.props.thumbnailUrl
Expand Down
1 change: 1 addition & 0 deletions apps/src/code-studio/components/ShareDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ShareDialog extends Component {
allowSignedOutShare,
...otherProps
} = this.props;

// If we're on a project level (i.e. /projects/appname), always show signed
// in version of the dialog

Expand Down
28 changes: 26 additions & 2 deletions apps/src/lib/ui/SettingsCog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import PopUpMenu from './PopUpMenu';
import ConfirmEnableMakerDialog from './ConfirmEnableMakerDialog';
import LibraryManagerDialog from '@cdo/apps/code-studio/components/libraries/LibraryManagerDialog';
import {getStore} from '../../redux';
import experiments from '@cdo/apps/util/experiments';
import ModelManagerDialog from '@cdo/apps/code-studio/components/ModelManagerDialog';

const style = {
iconContainer: {
Expand Down Expand Up @@ -43,7 +45,8 @@ class SettingsCog extends Component {
static propTypes = {
isRunning: PropTypes.bool,
runModeIndicators: PropTypes.bool,
showMakerToggle: PropTypes.bool
showMakerToggle: PropTypes.bool,
autogenerateML: PropTypes.func
};

// This ugly two-flag state is a workaround for an event-handling bug in
Expand All @@ -55,7 +58,8 @@ class SettingsCog extends Component {
open: false,
canOpen: true,
confirmingEnableMaker: false,
managingLibraries: false
managingLibraries: false,
managingModels: false
};

open = () => this.setState({open: true, canOpen: false});
Expand All @@ -77,6 +81,11 @@ class SettingsCog extends Component {
this.setState({managingLibraries: true});
};

manageModels = () => {
this.close();
this.setState({managingModels: true});
};

toggleMakerToolkit = () => {
this.close();
if (!makerToolkitRedux.isEnabled(getStore().getState())) {
Expand All @@ -97,6 +106,7 @@ class SettingsCog extends Component {
showConfirmation = () => this.setState({confirmingEnableMaker: true});
hideConfirmation = () => this.setState({confirmingEnableMaker: false});
closeLibraryManager = () => this.setState({managingLibraries: false});
closeModelManager = () => this.setState({managingModels: false});

setTargetPoint(icon) {
if (!icon) {
Expand Down Expand Up @@ -145,10 +155,20 @@ class SettingsCog extends Component {
{this.areLibrariesEnabled() && (
<ManageLibraries onClick={this.manageLibraries} />
)}
{experiments.isEnabled(experiments.APPLAB_ML) && (
<ManageModels onClick={this.manageModels} />
)}
{this.props.showMakerToggle && (
<ToggleMaker onClick={this.toggleMakerToolkit} />
)}
</PopUpMenu>
{experiments.isEnabled(experiments.APPLAB_ML) && (
<ModelManagerDialog
isOpen={this.state.managingModels}
onClose={this.closeModelManager}
autogenerateML={this.props.autogenerateML}
/>
)}
<ConfirmEnableMakerDialog
isOpen={this.state.confirmingEnableMaker}
handleConfirm={this.confirmEnableMaker}
Expand All @@ -173,6 +193,10 @@ ManageAssets.propTypes = {
last: PropTypes.bool
};

export function ManageModels(props) {
return <PopUpMenu.Item {...props}>{'Manage Models'}</PopUpMenu.Item>;
}

export function ManageLibraries(props) {
return <PopUpMenu.Item {...props}>{msg.manageLibraries()}</PopUpMenu.Item>;
}
Expand Down
27 changes: 27 additions & 0 deletions apps/src/lib/util/mlApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* global Promise */

import $ from 'jquery';
import {predict} from '@cdo/apps/MLTrainers';

export const commands = {
async getPrediction(opts) {
return new Promise((resolve, reject) => {
$.ajax({
url: '/api/v1/ml_models/' + opts.modelId,
method: 'GET'
})
.then(modelData => {
const predictParams = {
...modelData,
testData: opts.testValues
};
const result = predict(predictParams);
opts.callback(result);
return resolve();
})
.fail((jqXhr, status) => {
return reject({message: 'An error occurred'});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it does seem we might want to expose this error to the user's program, so they can then report it back to their end-user.

});
});
}
};