diff --git a/api_routes/gpt.js b/api_routes/gpt.js
new file mode 100644
index 00000000..c034d009
--- /dev/null
+++ b/api_routes/gpt.js
@@ -0,0 +1,23 @@
+const router = require('express').Router();
+
+router.route('/auth')
+ .post(async (req, res) => {
+ const { pw } = req.body;
+
+ if (pw !== process.env.GPT_PASSWORD) {
+ res.send({
+ status: 'error',
+ message: 'Incorrect password.',
+ });
+ return;
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ res.send({
+ status: 'success',
+ token: 'secretToken',
+ });
+ });
+
+module.exports = router;
diff --git a/api_routes/routes.js b/api_routes/routes.js
index 4e8f55c3..2350e338 100644
--- a/api_routes/routes.js
+++ b/api_routes/routes.js
@@ -8,6 +8,7 @@ const robokache = require('./robokache');
const { handleAxiosError } = require('./utils');
const services = require('./services');
const external_apis = require('./external');
+const gpt_auth = require('./gpt');
const samples = JSON.parse(fs.readFileSync(path.join(__dirname, './sample-query-cache.json')));
@@ -15,6 +16,8 @@ router.use('/', external_apis);
router.use('/robokache', robokache.router);
+router.use('/gpt', gpt_auth);
+
router.route('/quick_answer')
.post(async (req, res) => {
// if this is a sample query, load the response from the cache JSON:
diff --git a/package-lock.json b/package-lock.json
index afeea6f0..37d23c15 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"d3-force": "^2.1.1",
"dotenv": "^8.6.0",
"express": "^4.17.3",
+ "express-rate-limit": "^6.8.1",
"idb-keyval": "^5.0.6",
"js-yaml": "^3.14.0",
"lodash": "^4.17.21",
@@ -8538,6 +8539,16 @@
"node": ">=8"
}
},
+ "node_modules/boxen/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -12332,6 +12343,15 @@
"node": ">=8"
}
},
+ "node_modules/eslint/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/eslint/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -12735,6 +12755,17 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-rate-limit": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.1.tgz",
+ "integrity": "sha512-xJyudsE60CsDShK74Ni1MxsldYaIoivmG3ieK2tAckMsYCBewEuGalss6p/jHmFFnqM9xd5ojE0W2VlanxcOKg==",
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "peerDependencies": {
+ "express": "^4 || ^5"
+ }
+ },
"node_modules/express/node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -20727,19 +20758,6 @@
"node": ">=8"
}
},
- "node_modules/meow/node_modules/type-fest": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
- "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/meow/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
@@ -20999,6 +21017,7 @@
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==",
+ "peer": true,
"engines": {
"node": "*"
}
@@ -22602,6 +22621,16 @@
"node": ">=8"
}
},
+ "node_modules/np/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/np/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -23479,6 +23508,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/ow/node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/p-cancelable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
@@ -24753,6 +24792,12 @@
"react": "*"
}
},
+ "node_modules/react-graph-vis/node_modules/uuid": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
+ "integrity": "sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==",
+ "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details."
+ },
"node_modules/react-icons": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz",
@@ -27341,12 +27386,16 @@
}
},
"node_modules/type-fest": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
- "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
+ "optional": true,
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": {
@@ -27374,6 +27423,20 @@
"is-typedarray": "^1.0.0"
}
},
+ "node_modules/typescript": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/ua-parser-js": {
"version": "0.7.28",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz",
@@ -27843,10 +27906,13 @@
}
},
"node_modules/uuid": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
- "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
- "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details."
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "peer": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
},
"node_modules/v8-compile-cache": {
"version": "2.1.1",
@@ -27940,7 +28006,15 @@
"vis-util": "^1.1.6"
}
},
- "node_modules/vis-util": {
+ "node_modules/vis-network/node_modules/moment": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+ "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/vis-network/node_modules/vis-util": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-1.1.10.tgz",
"integrity": "sha512-8hGSxsFi2ogYYweClQyITzWnirWgQ8p0i9M4d3OXMuUO8vjXrf+2zHOYI9OZbtUduxAWuMEePnS9BXDtPJmJ7Q==",
@@ -27956,12 +28030,17 @@
"url": "https://opencollective.com/visjs"
}
},
- "node_modules/vis-util/node_modules/moment": {
- "version": "2.24.0",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
- "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
+ "node_modules/vis-util": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-4.3.4.tgz",
+ "integrity": "sha512-hJIZNrwf4ML7FYjs+m+zjJfaNvhjk3/1hbMdQZVnwwpOFJS/8dMG8rdbOHXcKoIEM6U5VOh3HNpaDXxGkOZGpw==",
+ "peer": true,
"engines": {
- "node": "*"
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/visjs"
}
},
"node_modules/vis-uuid": {
diff --git a/package.json b/package.json
index b09a292b..d39759b4 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"d3-force": "^2.1.1",
"dotenv": "^8.6.0",
"express": "^4.17.3",
+ "express-rate-limit": "^6.8.1",
"idb-keyval": "^5.0.6",
"js-yaml": "^3.14.0",
"lodash": "^4.17.21",
diff --git a/server.js b/server.js
index 48894eaf..a4589ce7 100644
--- a/server.js
+++ b/server.js
@@ -5,6 +5,7 @@ const bodyParser = require('body-parser');
// standard express logger
const morgan = require('morgan');
const path = require('path');
+const rateLimit = require('express-rate-limit');
// load env variables
dotenv.config();
@@ -19,8 +20,25 @@ const PORT = process.env.PORT || 7080;
axios.defaults.maxContentLength = Infinity;
axios.defaults.maxBodyLength = Infinity;
+// GPT auth: 300 reqs/hr
+const gptAuthLimiter = rateLimit({
+ windowMs: 1 * 60 * 60 * 1000, // 1 hour
+ max: 300, // 300 request per window
+ standardHeaders: true,
+ legacyHeaders: false,
+});
+// GPT: 60 req/min
+const gptLimiter = rateLimit({
+ windowMs: 1 * 60 * 1000, // 1 minute
+ max: 60, // 60 request per window
+ standardHeaders: true,
+ legacyHeaders: false,
+});
+
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json({ limit: '4000mb' }));
+app.use('/api/gpt', gptLimiter);
+app.use('/api/gpt/auth', gptAuthLimiter);
app.use(morgan('dev'));
// Add routes
@@ -36,5 +54,6 @@ app.get('*', (req, res) => {
});
app.listen(PORT, () => {
+ // eslint-disable-next-line no-console
console.log(`🌎 ==> qgraph running on port ${PORT}!`);
});
diff --git a/src/App.jsx b/src/App.jsx
index 72f2bda2..e8b98567 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -24,12 +24,15 @@ import API from '~/API';
import AlertContext from '~/context/alert';
import BiolinkContext from '~/context/biolink';
+import GPTContext from '~/context/gpt';
import useBiolinkModel from '~/stores/useBiolinkModel';
+import useGPT from '~/stores/useGPT';
export default function App() {
const [alert, setAlert] = useState({});
const biolink = useBiolinkModel();
+ const gpt = useGPT();
function simpleSetAlert(severity, msg) {
setAlert({ severity, msg });
@@ -60,44 +63,46 @@ export default function App() {
>
-
-
- simpleSetAlert(alert.severity, '')}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ simpleSetAlert(alert.severity, '')}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/context/gpt.js b/src/context/gpt.js
new file mode 100644
index 00000000..5d1e5486
--- /dev/null
+++ b/src/context/gpt.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+const GPTContext = React.createContext({});
+
+export default GPTContext;
diff --git a/src/pages/answer/GPTForm.jsx b/src/pages/answer/GPTForm.jsx
new file mode 100644
index 00000000..a0d8f186
--- /dev/null
+++ b/src/pages/answer/GPTForm.jsx
@@ -0,0 +1,159 @@
+import React, {
+ useContext, useRef, useState,
+} from 'react';
+
+import {
+ Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Link, TextField,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import GPTContext from '../../context/gpt';
+
+const EnableForm = ({ open, handleClose }) => {
+ const { setToken } = useContext(GPTContext);
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const controllerRef = useRef(new AbortController());
+
+ const handleCloseWithAbort = () => {
+ if (controllerRef.current) { controllerRef.current.abort(); }
+ handleClose();
+ };
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+
+ if (password === '') {
+ setError('Please enter your password.');
+ return;
+ }
+
+ setLoading(true);
+
+ let res;
+ try {
+ res = await fetch('/api/gpt/auth', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ pw: password }),
+ signal: controllerRef.current ? controllerRef.current.signal : undefined,
+ });
+ } catch (err) {
+ setError('An error has occurred, please try again later');
+ return;
+ }
+
+ if (res.status !== 200) {
+ setError(res.statusText);
+ setLoading(false);
+ return;
+ }
+
+ const apiResponse = await res.json();
+
+ if (apiResponse.status === 'error') {
+ setError(apiResponse.message);
+ } else if (apiResponse.status === 'success') {
+ setPassword('');
+ setLoading(false);
+ setToken(apiResponse.token);
+ handleCloseWithAbort();
+ }
+ };
+
+ return (
+
+ );
+};
+
+const DisableForm = ({ open, handleClose }) => {
+ const { setToken } = useContext(GPTContext);
+
+ const onSubmit = (e) => {
+ e.preventDefault();
+ setToken('');
+ handleClose();
+ };
+
+ return (
+
+ );
+};
+
+export default function GPTForm(props) {
+ const { enabled } = useContext(GPTContext);
+
+ return enabled ? : ;
+}
diff --git a/src/pages/answer/leftDrawer/LeftDrawer.jsx b/src/pages/answer/leftDrawer/LeftDrawer.jsx
index e9ee883a..8cd86901 100644
--- a/src/pages/answer/leftDrawer/LeftDrawer.jsx
+++ b/src/pages/answer/leftDrawer/LeftDrawer.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useContext, useState } from 'react';
import { useRouteMatch } from 'react-router-dom';
import Drawer from '@material-ui/core/Drawer';
import Toolbar from '@material-ui/core/Toolbar';
@@ -8,6 +8,8 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Checkbox from '@material-ui/core/Checkbox';
import IconButton from '@material-ui/core/IconButton';
+import { Assistant } from '@material-ui/icons';
+import { Badge, makeStyles } from '@material-ui/core';
import { useAuth0 } from '@auth0/auth0-react';
@@ -19,6 +21,17 @@ import HighlightOffIcon from '@material-ui/icons/HighlightOff';
import ConfirmDialog from '~/components/ConfirmDialog';
import './leftDrawer.css';
+import GPTContext from '../../../context/gpt';
+import GPTForm from '../GPTForm';
+
+const badgeStyles = makeStyles({
+ colorPrimary: {
+ backgroundColor: '#2dd04a',
+ },
+ colorError: {
+ backgroundColor: '#ff4a4a',
+ },
+});
/**
* Main Drawer component on answer page
@@ -34,9 +47,11 @@ export default function LeftDrawer({
onUpload, displayState, updateDisplayState, message,
saveAnswer, deleteAnswer, owned,
}) {
+ const { enabled } = useContext(GPTContext);
const { isAuthenticated } = useAuth0();
const urlHasAnswerId = useRouteMatch('/answer/:answer_id');
const [confirmOpen, setConfirmOpen] = useState(false);
+ const [isGPTFormOpen, setIsGPTFormOpen] = useState(false);
function toggleDisplay(component, show) {
updateDisplayState({ type: 'toggle', payload: { component, show } });
@@ -159,6 +174,25 @@ export default function LeftDrawer({
+ setIsGPTFormOpen(true)}
+ button
+ >
+
+
+
+
+
+
+
+
+
+ setIsGPTFormOpen(false)} />
);
}
diff --git a/src/stores/useGPT.js b/src/stores/useGPT.js
new file mode 100644
index 00000000..48f23e66
--- /dev/null
+++ b/src/stores/useGPT.js
@@ -0,0 +1,13 @@
+import { useState } from 'react';
+
+export default function useGPT() {
+ const [token, setToken] = useState('');
+
+ const enabled = token !== '';
+
+ return {
+ enabled,
+ token,
+ setToken,
+ };
+}