add dockerfile
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM node:latest
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY . /work
|
||||
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["npm", "run"]
|
||||
CMD ["start-local"]
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Jonathan Shobrook
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
49
README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Adrenaline
|
||||
|
||||
Adrenaline is a debugger powered by the OpenAI Codex. It not only fixes your code, but teaches you along the way.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Adrenaline can be used [here.](https://useadrenaline.com/playground) Simply plug in your broken code and an error message (e.g. a stack trace or natural language description of the error) and click "Debug."
|
||||
|
||||
> Note that you will have to supply your own OpenAI API key. This is to prevent API misuse.
|
||||
|
||||
### Running Locally
|
||||
|
||||
To run locally, clone the repository and run the following:
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
$ npm run start-local
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Debugging
|
||||
|
||||
Adrenaline sends your code and error message to the OpenAI Edit & Insert API (`code-davinci-edit-001`), which returns code changes that might fix your error (or at least give you a starting point). The proposed fixes are displayed in-line like a diff, with the option to accept, reject, or modify each code change.
|
||||
|
||||
### Error Explanation
|
||||
|
||||
Not only does Adrenaline propose fixes for your errors, but it also explains errors in plain English using GPT-3 (`text-davinci-003`).
|
||||
|
||||
### Linting
|
||||
|
||||
If your code isn't throwing an exception, it may still contain bugs. Adrenaline can scan your code for potential issues and propose fixes for them, if any exist.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Right now, Adrenaline is just a simple wrapper around GPT-3, meant to demonstrate what's possible with AI-driven debugging. There's many ways it can be improved:
|
||||
|
||||
1. Client-side intelligence (e.g. static code analysis) could be used to build a better prompt for GPT-3.
|
||||
2. Instead of simply _explaining_ your error, Adrenaline should provide chain-of-thought reasoning for how it fixed the error.
|
||||
3. In addition to chain-of-thought reasoning, Adrenaline could provide a ChatGPT-style assistant to answer questions about your error. I can even see Adrenaline being repurposed as a "coding tutor" for beginners.
|
||||
4. Creating a VSCode extension that does this would eliminate the friction of copy-pasting your code and error message into the site.
|
||||
|
||||
Ultimately, while the OpenAI Codex is surprisingly good at debugging code, I believe [a more specialized model](https://ai.stanford.edu/blog/DrRepair/) trained on all publicly available code could yield better results. There are interesting research questions here, such as how to generate synthetic training data (i.e. how can you systematically break code in a random but non-trivial way?).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Malik Drabla for helping build the initial PoC during [AI Hack Week](https://www.aihackweek.com/). Ramsey Lehman for design feedback. Paul Bogdan, Michael Usachenko, and Samarth Makhija for various other feedback.
|
||||
14
build/asset-manifest.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.b7f9d58b.css",
|
||||
"main.js": "/static/js/main.1a8f135d.js",
|
||||
"static/media/Gilroy-ExtraBold.otf": "/static/media/Gilroy-ExtraBold.0094146a0505298ed06a.otf",
|
||||
"index.html": "/index.html",
|
||||
"main.b7f9d58b.css.map": "/static/css/main.b7f9d58b.css.map",
|
||||
"main.1a8f135d.js.map": "/static/js/main.1a8f135d.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.b7f9d58b.css",
|
||||
"static/js/main.1a8f135d.js"
|
||||
]
|
||||
}
|
||||
BIN
build/demo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
build/favicon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
build/github.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
build/index.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><script async src="https://www.googletagmanager.com/gtag/js?id=G-C9ZFX7H10Q"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-C9ZFX7H10Q")</script><meta charset="utf-8"/><link rel="icon" href="/favicon.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A debugger powered by the OpenAI Codex."/><link rel="manifest" href="/manifest.json"/><title>Adrenaline</title><script defer="defer" src="/static/js/main.1a8f135d.js"></script><link href="/static/css/main.b7f9d58b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>gtag("event","screen_view")</script></body></html>
|
||||
BIN
build/key.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
build/logo.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
15
build/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Adrenaline",
|
||||
"name": "A debugger powered by GPT-3",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
build/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
2
build/static/css/main.b7f9d58b.css
Normal file
1
build/static/css/main.b7f9d58b.css.map
Normal file
3
build/static/js/main.1a8f135d.js
Normal file
85
build/static/js/main.1a8f135d.js.LICENSE.txt
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.2.1
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.6.1
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.6.1
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license React v0.19.1
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.14.0
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.14.0
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.14.0
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
1
build/static/js/main.1a8f135d.js.map
Normal file
BIN
build/static/media/Gilroy-ExtraBold.0094146a0505298ed06a.otf
Normal file
30668
package-lock.json
generated
Normal file
53
package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "adrenaline",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^12.1.5",
|
||||
"codemirror": "^5.65.10",
|
||||
"create-react-app": "^5.0.1",
|
||||
"diff": "^5.1.0",
|
||||
"openai": "^3.1.0",
|
||||
"react": "^16.13.0",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-router-dom": "^6.6.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-select": "^5.7.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start-local": "react-scripts start",
|
||||
"start": "node server.js",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"gh-pages": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "8.6.0",
|
||||
"node": "18.0.0"
|
||||
}
|
||||
}
|
||||
BIN
public/demo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/github.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
57
public/index.html
Normal file
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C9ZFX7H10Q"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-C9ZFX7H10Q');
|
||||
</script>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A debugger powered by the OpenAI Codex."
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
|
||||
<title>Adrenaline</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
|
||||
<script>
|
||||
gtag('event', 'screen_view');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/key.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
15
public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Adrenaline",
|
||||
"name": "A debugger powered by GPT-3",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
14
server.js
Normal file
@ -0,0 +1,14 @@
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
|
||||
const app = express();
|
||||
const publicPath = path.join(__dirname, 'build');
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.static(publicPath));
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(publicPath, 'index.html'));
|
||||
});
|
||||
app.listen(port, () => {
|
||||
console.log('Server is up!');
|
||||
});
|
||||
37
src/components/Button.css
Normal file
@ -0,0 +1,37 @@
|
||||
.primaryButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #D93CFF;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
/* height: 40px; */
|
||||
height: 35px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.primaryButton:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.secondaryButton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
border: solid 2px rgba(255, 255, 255, 0.25);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
height: 35px;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.secondaryButton:hover {
|
||||
border: solid 2px rgba(255, 255, 255, 1.0);
|
||||
}
|
||||
20
src/components/Button.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
import "./Button.css";
|
||||
|
||||
export default class Button extends Component {
|
||||
render() {
|
||||
const { className, onClick, isPrimary, children, isLoading } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${className} ${isPrimary ? "primaryButton" : "secondaryButton"}`}
|
||||
onClick={() => isLoading ? null : onClick()}
|
||||
>
|
||||
{isLoading ? (<Spinner />) : children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
8
src/components/Link.css
Normal file
@ -0,0 +1,8 @@
|
||||
.link {
|
||||
padding-right: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
18
src/components/Link.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import "./Link.css";
|
||||
|
||||
export default class Link extends Component {
|
||||
render() {
|
||||
const { className, onClick, children } = this.props;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={`link ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
111
src/components/Popup.css
Normal file
@ -0,0 +1,111 @@
|
||||
@media (max-width: 1130px) {
|
||||
.popup {
|
||||
width: 75% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 50%;
|
||||
max-width: 600px;
|
||||
height: fit-content;
|
||||
background-color: #373751;
|
||||
box-shadow: 0px 2px 8px 2px rgba(32, 32, 48, 0.5);
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.popupInner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 85%;
|
||||
height: 72%;
|
||||
justify-content: space-between;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.popupHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popupHeading {
|
||||
font-size: 26px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1.25px;
|
||||
}
|
||||
|
||||
.popupSubheading {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: rgba(255,255,255,0.65);
|
||||
width: 75%;
|
||||
letter-spacing: 0.5px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.popupForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 45px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputText {
|
||||
border: 2px solid rgba(255, 255, 255, 0.65);
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
resize: none;
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
padding-top: 9px;
|
||||
font-size: 18px;
|
||||
font-family: "Helvetica Neue";
|
||||
padding-left: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.inputText:placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.popupSubmit {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
padding-top: 10px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.instructions a {
|
||||
color: #279AF1;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.instructions a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.lastStep {
|
||||
padding-top: 3px;
|
||||
}
|
||||
59
src/components/Popup.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Button from "./Button";
|
||||
|
||||
import "./Popup.css";
|
||||
|
||||
export default class Popup extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { value: "" };
|
||||
|
||||
const apiKey = localStorage.getItem("openAiApiKey");
|
||||
if (apiKey) {
|
||||
this.state.value = JSON.parse(apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
onChange = event => this.setState({ value: event.target.value });
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
const { onSubmit, setRef } = this.props;
|
||||
|
||||
return (
|
||||
<div className="popup" ref={ref => setRef(ref)}>
|
||||
<div className="popupInner">
|
||||
<div className="popupHeader">
|
||||
<span className="popupHeading">Adrenaline is powered by OpenAI</span>
|
||||
<span className="popupSubheading">Please provide an OpenAI API key. This key will only be stored locally in your browser cache.</span>
|
||||
</div>
|
||||
<div className="popupForm">
|
||||
<div className="inputField">
|
||||
<textarea
|
||||
className="inputText"
|
||||
ref={ref => this.input = ref}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
placeholder="Enter your OpenAI API key"
|
||||
/>
|
||||
<Button className="popupSubmit" isPrimary onClick={() => {
|
||||
window.gtag("event", "submit_api_key");
|
||||
|
||||
localStorage.setItem("openAiApiKey", JSON.stringify(value));
|
||||
onSubmit(value);
|
||||
}}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
<div className="instructions">
|
||||
<span>1. Navigate to <a href="https://beta.openai.com/account/api-keys" target="_blank">https://beta.openai.com/account/api-keys</a></span>
|
||||
<span className="lastStep">2. Log in and click <b>+ Create a new secret key</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
48
src/components/Spinner.css
Normal file
@ -0,0 +1,48 @@
|
||||
.spinner {
|
||||
margin: 100px auto;
|
||||
height: 15px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
/* padding-right: 5px; */
|
||||
}
|
||||
|
||||
.spinner > div {
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
display: inline-block;
|
||||
|
||||
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.spinner .bounce1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.spinner .bounce2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.spinner .bounce3 {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bouncedelay {
|
||||
0%, 80%, 100% { -webkit-transform: scale(0) }
|
||||
40% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-bouncedelay {
|
||||
0%, 80%, 100% {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
} 40% {
|
||||
-webkit-transform: scale(1.0);
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
13
src/components/Spinner.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import "./Spinner.css";
|
||||
|
||||
export default function Spinner() {
|
||||
return (
|
||||
<div className="spinner">
|
||||
<div className="bounce1" />
|
||||
<div className="bounce2" />
|
||||
<div className="bounce3" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
174
src/containers/CodeEditor.css
Normal file
@ -0,0 +1,174 @@
|
||||
.codeEditorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 66%;
|
||||
overflow: auto;
|
||||
padding-left: 11px;
|
||||
padding-right: 24px;
|
||||
font-family: "Monaco" !important;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid #54546A; /* #4E4881 */
|
||||
}
|
||||
|
||||
.codeEditorHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 18px;
|
||||
padding-top: 20px;
|
||||
justify-content: space-between;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.languageDropdown__control {
|
||||
background-color: transparent !important;
|
||||
border-color: #9B9BA8 !important;
|
||||
border-radius: 5px !important;
|
||||
border-width: 2px !important;
|
||||
min-height: 35px !important;
|
||||
height: 35px !important;
|
||||
}
|
||||
|
||||
.languageDropdown__single-value {
|
||||
color: white !important;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-top: -5px !important;
|
||||
}
|
||||
|
||||
.languageDropdown__indicators {
|
||||
margin-top: -2px !important;
|
||||
}
|
||||
|
||||
.languageDropdown__indicator-separator {
|
||||
background-color: #9B9BA8 !important;
|
||||
width: 2px !important;
|
||||
margin-top: 11px !important;
|
||||
margin-bottom: 11px !important;
|
||||
}
|
||||
|
||||
.languageDropdown__dropdown-indicator {
|
||||
color: #9B9BA8 !important;
|
||||
}
|
||||
|
||||
/* .languageDropdown {
|
||||
font-weight: 500 !important;
|
||||
font-family: "Helvetica Neue" !important;
|
||||
font-size: 16px !important;
|
||||
color: white !important;
|
||||
background: none !important;
|
||||
} */
|
||||
|
||||
.lintButton {
|
||||
font-weight: 500;
|
||||
font-family: "Helvetica Neue" !important;
|
||||
font-size: 16px;
|
||||
background-color: #279AF1;
|
||||
width: 45px !important;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.codeEditor {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
padding-left: 5px !important;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.CodeMirror-code > div {
|
||||
padding-bottom: 2px; /* Line spacing */
|
||||
}
|
||||
|
||||
.oldLine {
|
||||
background-color: rgba(39, 154, 241, 0.25);
|
||||
}
|
||||
|
||||
.last {
|
||||
border-radius: 0px 0px 7px 7px;
|
||||
}
|
||||
|
||||
.newLine {
|
||||
background-color: rgb(217, 60, 255, 0.25);
|
||||
}
|
||||
|
||||
.first {
|
||||
border-radius: 7px 7px 0px 0px;
|
||||
}
|
||||
|
||||
.both {
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.mergeLine {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.useMeHeader {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding: 0px 5px 0px 5px;
|
||||
align-items: center;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
/* letter-spacing: 0.75px; */
|
||||
}
|
||||
|
||||
.oldCodeHeader {
|
||||
/* background-color: rgba(39, 154, 241, 0.33) !important; */
|
||||
background-color: #2F68A1 !important;
|
||||
border-radius: 7px 7px 0px 0px !important;
|
||||
}
|
||||
|
||||
.newCodeHeader {
|
||||
/* background-color: rgb(217, 60, 255, 0.325); */
|
||||
background-color: #8839A8 !important;
|
||||
border-radius: 0px 0px 7px 7px;
|
||||
position: relative;
|
||||
top: 2px; /* Equal to line spacing */
|
||||
}
|
||||
|
||||
.useMeButton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
width: 65px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.useMeButton:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.newCodeButton {
|
||||
background-color: rgb(217, 60, 255, 1.0);
|
||||
}
|
||||
|
||||
.oldCodeButton {
|
||||
background-color: #279AF1;
|
||||
}
|
||||
|
||||
.useMeLabel {
|
||||
font-weight: 500 !important;
|
||||
padding-right: 5px;
|
||||
margin-left: auto;
|
||||
}
|
||||
255
src/containers/CodeEditor.js
Normal file
@ -0,0 +1,255 @@
|
||||
import React, { Component } from "react";
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import Select from 'react-select';
|
||||
|
||||
import Button from "../components/Button";
|
||||
|
||||
import "./CodeEditor.css";
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import "./theme.css"; // TODO: Move to CodeEditor.css
|
||||
|
||||
require('codemirror/mode/python/python');
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/clike/clike');
|
||||
|
||||
const LANGUAGES = [
|
||||
{label: "Python", value: "python"},
|
||||
{label: "JavaScript", value: "javascript"},
|
||||
{label: "Java", value: "clike"},
|
||||
{label: "Ruby", value: "ruby"},
|
||||
{label: "PHP", value: "php"},
|
||||
{label: "C++", value: "clike"},
|
||||
{label: "C", value: "clike"},
|
||||
{label: "Shell", value: "shell"},
|
||||
{label: "C#", value: "clike"},
|
||||
{label: "Objective-C", value: "clike"},
|
||||
{label: "R", value: "r"},
|
||||
{label: "Go", value: "go"},
|
||||
{label: "Perl", value: "perl"},
|
||||
{label: "CoffeeScript", value: "coffeescript"},
|
||||
{label: "Scala", value: "clike"},
|
||||
{label: "Haskell", value: "haskell"},
|
||||
{label: "HTML", value: "htmlmixed"},
|
||||
{label: "CSS", value: "css"},
|
||||
{label: "Kotlin", value: "clike"},
|
||||
{label: "Rust", value: "rust"},
|
||||
{label: "SQL", value: "sql"},
|
||||
{label: "Swift", value: "swift"}
|
||||
];
|
||||
|
||||
export default class CodeEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.createWidget = this.createWidget.bind(this);
|
||||
this.addDiffWidgets = this.addDiffWidgets.bind(this);
|
||||
this.deleteDiffWidgets = this.deleteDiffWidgets.bind(this);
|
||||
this.addLineHighlights = this.addLineHighlights.bind(this);
|
||||
this.deleteLineHighlights = this.deleteLineHighlights.bind(this);
|
||||
this.addDiffsToEditor = this.addDiffsToEditor.bind(this);
|
||||
this.deleteDiffsFromEditor = this.deleteDiffsFromEditor.bind(this);
|
||||
}
|
||||
|
||||
/* Diff Utilities */
|
||||
|
||||
createWidget(insertLineNum, isAboveLine, onClickUseMe) {
|
||||
let widget = document.createElement("div");
|
||||
widget.className = isAboveLine ? "useMeHeader oldCodeHeader" : "useMeHeader newCodeHeader";
|
||||
|
||||
let useMeButton = document.createElement("div");
|
||||
useMeButton.className = isAboveLine ? "useMeButton oldCodeButton" : "useMeButton newCodeButton";
|
||||
useMeButton.innerHTML = "Use me";
|
||||
useMeButton.onclick = onClickUseMe;
|
||||
|
||||
let label = document.createElement("span");
|
||||
label.className = "useMeLabel";
|
||||
label.innerHTML = isAboveLine ? "your code" : "fixed code";
|
||||
|
||||
widget.appendChild(useMeButton);
|
||||
widget.appendChild(label);
|
||||
|
||||
return this.codeMirrorRef.addLineWidget(insertLineNum, widget, { above: isAboveLine });
|
||||
}
|
||||
|
||||
addDiffWidgets(diff, onResolveDiff) {
|
||||
const { oldLines, newLines, mergeLine } = diff;
|
||||
|
||||
const oldCodeStart = oldLines.at(0);
|
||||
const newCodeEnd = newLines.at(-1);
|
||||
|
||||
const onUseOldCode = () => {
|
||||
let linesToDelete = newLines;
|
||||
linesToDelete.push(mergeLine);
|
||||
|
||||
onResolveDiff(diff, linesToDelete, oldCodeStart);
|
||||
};
|
||||
const onUseNewCode = () => {
|
||||
let linesToDelete = oldLines;
|
||||
linesToDelete.push(mergeLine);
|
||||
|
||||
onResolveDiff(diff, linesToDelete, newCodeEnd);
|
||||
}
|
||||
|
||||
const oldCodeWidgetInsertLine = oldLines.length === 0 ? mergeLine : oldCodeStart;
|
||||
const newCodeWidgetInsertLine = newLines.length === 0 ? mergeLine : newCodeEnd;
|
||||
|
||||
diff.oldCodeWidget = this.createWidget(oldCodeWidgetInsertLine, true, onUseOldCode);
|
||||
diff.newCodeWidget = this.createWidget(newCodeWidgetInsertLine, false, onUseNewCode);
|
||||
}
|
||||
|
||||
deleteDiffWidgets(diff) {
|
||||
diff.oldCodeWidget?.clear();
|
||||
diff.newCodeWidget?.clear();
|
||||
}
|
||||
|
||||
addLineHighlights(diff) {
|
||||
const { oldLines, newLines, mergeLine } = diff;
|
||||
|
||||
oldLines.forEach((lineNum, index) => {
|
||||
let className = index === 0 ? "oldLine first" : "oldLine";
|
||||
this.codeMirrorRef.addLineClass(lineNum, "wrap", className);
|
||||
});
|
||||
newLines.forEach((lineNum, index) => {
|
||||
let className = index === newLines.length - 1 ? "newLine last" : "newLine";
|
||||
this.codeMirrorRef.addLineClass(lineNum, "wrap", className);
|
||||
});
|
||||
|
||||
if (mergeLine !== -1) {
|
||||
let className = "mergeLine";
|
||||
if (oldLines.length === 0 && newLines.length === 0) {
|
||||
className += " both";
|
||||
} else if (oldLines.length === 0) {
|
||||
className += " first";
|
||||
} else if (newLines.length === 0) {
|
||||
className += " last";
|
||||
}
|
||||
|
||||
this.codeMirrorRef.addLineClass(mergeLine, "wrap", className);
|
||||
}
|
||||
}
|
||||
|
||||
deleteLineHighlights(diff) {
|
||||
const { oldLines, newLines, mergeLine } = diff;
|
||||
|
||||
oldLines.forEach((oldLine, index) => {
|
||||
let className = "oldLine";
|
||||
className += index === 0 ? " first" : "";
|
||||
|
||||
this.codeMirrorRef.removeLineClass(index, "wrap", className);
|
||||
});
|
||||
newLines.forEach((newLine, index) => {
|
||||
let className = "newLine";
|
||||
className += index === newLines.length - 1 ? " last" : "";
|
||||
|
||||
this.codeMirrorRef.removeLineClass(index, "wrap", className);
|
||||
});
|
||||
|
||||
if (mergeLine !== -1) {
|
||||
let className = "mergeLine";
|
||||
if (oldLines.length === 0 && newLines.length === 0) {
|
||||
className += " both";
|
||||
} else if (oldLines.length === 0) {
|
||||
className += " first";
|
||||
} else if (newLines.length === 0) {
|
||||
className += " last";
|
||||
}
|
||||
|
||||
this.codeMirrorRef.removeLineClass(mergeLine, "wrap", className);
|
||||
}
|
||||
}
|
||||
|
||||
addDiffsToEditor(diffs, onResolveDiff) {
|
||||
diffs.forEach((diff, index) => {
|
||||
this.addLineHighlights(diff);
|
||||
this.addDiffWidgets(diff, onResolveDiff);
|
||||
});
|
||||
}
|
||||
|
||||
deleteDiffsFromEditor(diffs) {
|
||||
diffs.forEach((diff, index) => {
|
||||
this.deleteLineHighlights(diff);
|
||||
this.deleteDiffWidgets(diff);
|
||||
});
|
||||
}
|
||||
|
||||
/* Lifecycle Methods */
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { diffs: prevDiffs } = prevProps;
|
||||
const { diffs, onResolveDiff } = this.props;
|
||||
|
||||
this.deleteDiffsFromEditor(prevDiffs);
|
||||
this.addDiffsToEditor(diffs, onResolveDiff);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { diffs, onResolveDiff } = this.props;
|
||||
|
||||
this.deleteDiffsFromEditor(diffs);
|
||||
this.addDiffsToEditor(diffs, onResolveDiff);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
language,
|
||||
code,
|
||||
onChange,
|
||||
onSelectLanguage,
|
||||
isLoading,
|
||||
onLint
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="codeEditorContainer">
|
||||
<div className="codeEditorHeader">
|
||||
<Select
|
||||
classNamePrefix="languageDropdown"
|
||||
isClearable={false}
|
||||
options={LANGUAGES}
|
||||
onChange={onSelectLanguage}
|
||||
defaultValue={language}
|
||||
styles={{
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
boxShadow: "none",
|
||||
cursor: "pointer"
|
||||
}),
|
||||
menu: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: "#202030"
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
fontFamily: "Helvetica Neue",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
backgroundColor: state.isFocused ? "#279AF1" : "transparent",
|
||||
cursor: "pointer"
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="lintButton"
|
||||
onClick={onLint}
|
||||
isLoading={isLoading}
|
||||
isPrimary
|
||||
>
|
||||
Lint
|
||||
</Button>
|
||||
</div>
|
||||
<CodeMirror
|
||||
className="codeEditor"
|
||||
value={code.join("\n")}
|
||||
options={{
|
||||
mode: language.value,
|
||||
theme: "dracula",
|
||||
lineNumbers: true
|
||||
}}
|
||||
onBeforeChange={onChange}
|
||||
onChange={onChange}
|
||||
editorDidMount={editor => this.codeMirrorRef = editor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
22
src/containers/ErrorExplanation.css
Normal file
@ -0,0 +1,22 @@
|
||||
.errorExplanation {
|
||||
border-left: 2px solid #54546A; /* #4E4881 */
|
||||
padding-top: 20px;
|
||||
padding-left: 30px;
|
||||
font-weight: 500;
|
||||
width: 33%;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.errorExplanationHeader {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.errorExplanationHeader p {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-weight: 400;
|
||||
font-family: Monaco;
|
||||
font-size: 14px;
|
||||
overflow: auto;
|
||||
line-height: 22px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
66
src/containers/ErrorExplanation.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { Component, useState, useEffect } from 'react';
|
||||
|
||||
import "./ErrorExplanation.css";
|
||||
|
||||
export default class ErrorExplanation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { currentWordIndex: 0 };
|
||||
}
|
||||
|
||||
isErrorExplanationEmpty = () => {
|
||||
const { errorExplanation } = this.props;
|
||||
return errorExplanation === "";
|
||||
}
|
||||
|
||||
isErrorExplanationFullyRendered = () => {
|
||||
const { errorExplanation } = this.props;
|
||||
const { currentWordIndex } = this.state;
|
||||
|
||||
return currentWordIndex >= errorExplanation.split(" ").length;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const delay = 75;
|
||||
this.interval = setInterval(() => {
|
||||
if (this.isErrorExplanationEmpty()){
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(prevState => ({ currentWordIndex: prevState.currentWordIndex + 1 }));
|
||||
}, delay);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { errorExplanation } = this.props;
|
||||
const { currentWordIndex } = this.state;
|
||||
|
||||
if (!this.isErrorExplanationEmpty() && this.isErrorExplanationFullyRendered()) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorExplanation } = this.props;
|
||||
const { currentWordIndex } = this.state;
|
||||
|
||||
const words = errorExplanation.split(' ');
|
||||
const currentText = words.slice(0, currentWordIndex).join(' ');
|
||||
|
||||
return (
|
||||
<div className="errorExplanation">
|
||||
<div className="errorExplanationHeader">
|
||||
<span>Error Explanation</span>
|
||||
{errorExplanation === "" ? (
|
||||
<p>Provide an error message and click <b>Debug</b> to explain and fix your error.</p>
|
||||
) : (<p>{currentText}</p>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
53
src/containers/ErrorMessage.css
Normal file
@ -0,0 +1,53 @@
|
||||
@media (max-width: 700px) {
|
||||
.errorMessage {
|
||||
height: 50% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
height: 33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.errorMessageHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-top: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.debugButton {
|
||||
background-color: #D93CFF;
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.errorMessageInput {
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
color: white;
|
||||
background-color: #54546A; /* #373171 */
|
||||
padding-left: 20px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
padding-right: 15px;
|
||||
width: calc(100% - 60px);
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
resize: none;
|
||||
margin-left: 30px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 15px;
|
||||
font-family: Monaco;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.errorMessageInput:placeholder {
|
||||
color: #D4D4D9;
|
||||
}
|
||||
43
src/containers/ErrorMessage.js
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import Button from "../components/Button";
|
||||
|
||||
import "./ErrorMessage.css";
|
||||
|
||||
export default class ErrorMessage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { value: "" };
|
||||
}
|
||||
|
||||
onChange = event => this.setState({ value: event.target.value });
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
const { onDebug, isLoading } = this.props;
|
||||
|
||||
return (
|
||||
<div className="errorMessage">
|
||||
<div className="errorMessageHeader">
|
||||
<span>Error Message</span>
|
||||
<Button
|
||||
className="debugButton"
|
||||
onClick={() => onDebug(value)}
|
||||
isPrimary
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Debug
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
className="errorMessageInput"
|
||||
ref={ref => this.input = ref}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
placeholder="Describe your error in simple terms. Or paste an error message / stack trace."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
92
src/containers/Header.css
Normal file
@ -0,0 +1,92 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 55px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid #54546A; /* #4E4881 */
|
||||
background-color: #202030;
|
||||
border-top: 4px solid #D93CFF;
|
||||
padding-top: 2px;
|
||||
min-height: 55px;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header .compactButtons {
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.header .compactButtons {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.githubIcon img {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.githubIcon:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.keyIcon {
|
||||
margin-left: 15px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.keyIcon:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.header .buttons {
|
||||
display: none !important;
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header .buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header .buttons > a {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
padding-right: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header .buttons .headerGithubButton a {
|
||||
padding-right: 0px !important;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.apiKeyButton {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.header .buttons > a:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 20px;
|
||||
}
|
||||
39
src/containers/Header.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
// import Link from "../components/Link";
|
||||
import Button from "../components/Button";
|
||||
|
||||
import "./Header.css";
|
||||
|
||||
export default class Header extends Component {
|
||||
render() {
|
||||
const { isPlaygroundActive, onClick } = this.props;
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="logo">
|
||||
<Link to="/">
|
||||
<img src="./logo.png" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="buttons">
|
||||
<Button
|
||||
className="headerGithubButton"
|
||||
isPrimary={false}
|
||||
onClick={() => window.gtag("event", "click_view_on_github")}
|
||||
>
|
||||
<a href="https://github.com/shobrook/adrenaline/" target="_blank">View on GitHub</a>
|
||||
</Button>
|
||||
<Button className="apiKeyButton" isPrimary onClick={onClick}>Set API key</Button>
|
||||
</div>
|
||||
<div className="compactButtons">
|
||||
<a className="githubIcon" href="https://github.com/shobrook/adrenaline/" target="_blank">
|
||||
<img src="./github.png" />
|
||||
</a>
|
||||
<img className="keyIcon" src="./key.png" onClick={onClick} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
30
src/containers/theme.css
Normal file
@ -0,0 +1,30 @@
|
||||
.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {
|
||||
background-color: #373751 !important; /* #140C58 !important; */
|
||||
color: #E5FCFF !important;
|
||||
border: none;
|
||||
}
|
||||
.cm-s-dracula .CodeMirror-gutters { color: #140C58; }
|
||||
.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #E5FCFF; }
|
||||
.cm-s-dracula .CodeMirror-linenumber { color: #8985AB; }
|
||||
.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-dracula span.cm-comment { color: #6272a4; } /* OLD */
|
||||
.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }
|
||||
.cm-s-dracula span.cm-number { color: #bd93f9; }
|
||||
.cm-s-dracula span.cm-variable { color: #FFF; }
|
||||
.cm-s-dracula span.cm-variable-2 { color: #FFF; }
|
||||
.cm-s-dracula span.cm-def { color: #279AF1; }
|
||||
.cm-s-dracula span.cm-operator { color: #D93CFF; }
|
||||
.cm-s-dracula span.cm-keyword { color: #D93CFF; }
|
||||
.cm-s-dracula span.cm-atom { color: #bd93f9; }
|
||||
.cm-s-dracula span.cm-meta { color: #f8f8f2; }
|
||||
.cm-s-dracula span.cm-tag { color: #D93CFF; }
|
||||
.cm-s-dracula span.cm-attribute { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-qualifier { color: #50fa7b; }
|
||||
.cm-s-dracula span.cm-property { color: #66d9ef; }
|
||||
.cm-s-dracula span.cm-builtin { color: #45F0DF; }
|
||||
.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #FFF; }
|
||||
|
||||
.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }
|
||||
.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
|
||||
13
src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
27
src/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { HashRouter as Router, Route } from 'react-router-dom';
|
||||
|
||||
import './index.css';
|
||||
|
||||
import Landing from "./routes/Landing";
|
||||
import App from "./routes/App";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Landing />
|
||||
},
|
||||
{
|
||||
path: "/playground",
|
||||
element: <App />
|
||||
}
|
||||
]);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
66
src/routes/App.css
Normal file
@ -0,0 +1,66 @@
|
||||
.popupLayer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: rgba(32, 32, 48, 0.8);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #373751; /* #140C58 */
|
||||
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.app .body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100% - 55px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app .body .lhs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 66%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.app .body {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app .body .lhs {
|
||||
width: 100%;
|
||||
height: 66%;
|
||||
}
|
||||
|
||||
.errorExplanation {
|
||||
border-top: 2px solid #54546A;
|
||||
border-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
height: 34% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.errorExplanationHeader {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.errorExplanationHeader p {
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
428
src/routes/App.js
Normal file
@ -0,0 +1,428 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Configuration, OpenAIApi } from "openai";
|
||||
|
||||
import {
|
||||
OLD_CODE_LABEL,
|
||||
FIXED_CODE_LABEL,
|
||||
range,
|
||||
diffGPTOutput,
|
||||
withRouter
|
||||
} from "../utilities";
|
||||
|
||||
import Popup from "../components/Popup";
|
||||
import Header from "../containers/Header";
|
||||
import CodeEditor from "../containers/CodeEditor";
|
||||
import ErrorMessage from "../containers/ErrorMessage";
|
||||
import ErrorExplanation from "../containers/ErrorExplanation";
|
||||
|
||||
import './App.css';
|
||||
|
||||
const FIXED_CODE = [
|
||||
"import numpy as np",
|
||||
"import pandas as pd",
|
||||
"",
|
||||
"from statsmodels.tsa.stattools import grangercausalitytests",
|
||||
"",
|
||||
"n = 1000",
|
||||
"ls = np.linspace(0, 2*np.pi, n)",
|
||||
"",
|
||||
"df1 = pd.DataFrame(np.sin(ls))",
|
||||
"df2 = pd.DataFrame(2*np.sin(1+ls))",
|
||||
"noise = 0.0001 * np.random.rand(n, 2)",
|
||||
"df2 += noise",
|
||||
"df = pd.concat([df1, df2], axis=1)",
|
||||
"",
|
||||
"df.plot()",
|
||||
"",
|
||||
"grangercausalitytests(df, maxlag=20)"
|
||||
]
|
||||
const DEFAULT_CODE = [
|
||||
"#################",
|
||||
"### DEMO CODE ###",
|
||||
"#################",
|
||||
"",
|
||||
"# Replace this with your own code to get started.",
|
||||
"",
|
||||
"def apply_func_to_input(func, input):",
|
||||
"\tfunc(input)",
|
||||
"",
|
||||
"def main():",
|
||||
"\tmy_data = []",
|
||||
"\tfor i in range(10):",
|
||||
"\t\tapply_func_to_input(my_data.add, i)",
|
||||
"",
|
||||
"\tprint(my_data)",
|
||||
"",
|
||||
"main()"
|
||||
];
|
||||
const EDIT_PROMPT_PARAMS = {
|
||||
model: "code-davinci-edit-001"
|
||||
};
|
||||
const COMPLETION_PROMPT_PARAMS = {
|
||||
model: "text-davinci-003",
|
||||
max_tokens: 500,
|
||||
temperature: 0.2,
|
||||
top_p: 1,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
best_of: 1,
|
||||
n: 1,
|
||||
stream: false
|
||||
};
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onCodeChange = this.onCodeChange.bind(this);
|
||||
this.onResolveDiff = this.onResolveDiff.bind(this);
|
||||
this.onDebug = this.onDebug.bind(this);
|
||||
this.onLint = this.onLint.bind(this);
|
||||
this.onSelectLanguage = this.onSelectLanguage.bind(this);
|
||||
this.onOpenPopup = this.onOpenPopup.bind(this);
|
||||
this.onSetAPIKey = this.onSetAPIKey.bind(this);
|
||||
this.onClosePopup = this.onClosePopup.bind(this);
|
||||
this.onSetPopupRef = this.onSetPopupRef.bind(this);
|
||||
|
||||
this.state = {
|
||||
language: {label: "Python", value: "python"},
|
||||
code: DEFAULT_CODE,
|
||||
errorMessage: "",
|
||||
diffs: [],
|
||||
errorExplanation: "",
|
||||
apiKey: "",
|
||||
waitingForCodeFix: false,
|
||||
waitingForCodeLint: false,
|
||||
askForAPIKey: false
|
||||
};
|
||||
|
||||
const apiKey = localStorage.getItem("openAiApiKey");
|
||||
if (apiKey) {
|
||||
this.state.apiKey = JSON.parse(apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
/* Event Handlers */
|
||||
|
||||
onCodeChange(editor, data, newCode) {
|
||||
const { code, diffs } = this.state;
|
||||
newCode = newCode.split("\n")
|
||||
|
||||
// Lines were either inserted or deleted, requiring an update in diff indexing
|
||||
if (code.length !== newCode.length) {
|
||||
const { from, text, to } = data;
|
||||
|
||||
if (from.line === to.line) { // Insertion
|
||||
let insertLine = from.line;
|
||||
let numLinesAdded = text.length - 1;
|
||||
|
||||
diffs.forEach(diff => {
|
||||
const { oldLines, mergeLine, newLines } = diff;
|
||||
const lastLineInDiff = newLines.at(-1);
|
||||
|
||||
// Insertions don't affect diffs *before* the insertLine
|
||||
if (lastLineInDiff < insertLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldLines.includes(insertLine)) { // Change occurred in old code
|
||||
let lastOldLine = oldLines.at(-1)
|
||||
diff.oldLines.push(...range(numLinesAdded, lastOldLine + 1));
|
||||
|
||||
diff.mergeLine += numLinesAdded;
|
||||
diff.newLines = newLines.map(line => line + numLinesAdded);
|
||||
} else if (mergeLine === insertLine || newLines.includes(insertLine)) { // Change occurred in new code
|
||||
let lastNewLine = newLines.at(-1);
|
||||
diff.newLines.push(...range(numLinesAdded, lastNewLine + 1))
|
||||
} else { // Change occurred outside of diff
|
||||
diff.oldLines = oldLines.map(line => line + numLinesAdded);
|
||||
diff.mergeLine += numLinesAdded;
|
||||
diff.newLines = newLines.map(line => line + numLinesAdded);
|
||||
}
|
||||
});
|
||||
} else if (from.line < to.line) { // Deletion
|
||||
let deleteLine = to.line;
|
||||
let numLinesDeleted = to.line - from.line;
|
||||
|
||||
diffs.forEach(diff => {
|
||||
const { oldLines, mergeLine, newLines } = diff;
|
||||
const lastLineInDiff = newLines.at(-1);
|
||||
|
||||
// Deletions don't affect diffs *before* the deleteLine
|
||||
if (lastLineInDiff < deleteLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldLines.includes(deleteLine)) { // Change occurred in old code
|
||||
let deleteStartIndex = oldLines.indexOf(from.line);
|
||||
let deleteEndIndex = oldLines.indexOf(to.line);
|
||||
|
||||
diff.oldLines = oldLines.map((line, index) => {
|
||||
if (index > deleteEndIndex) {
|
||||
return line - numLinesDeleted;
|
||||
}
|
||||
|
||||
return line;
|
||||
});
|
||||
|
||||
if (deleteStartIndex === -1) {
|
||||
diff.oldLines.splice(0, deleteEndIndex + 1);
|
||||
} else {
|
||||
diff.oldLines.splice(deleteStartIndex + 1, deleteEndIndex - deleteStartIndex);
|
||||
}
|
||||
|
||||
diff.mergeLine -= numLinesDeleted;
|
||||
diff.newLines = newLines.map(line => line - numLinesDeleted);
|
||||
} else if (mergeLine === deleteLine) {
|
||||
// TODO: Delete entire diff if merge line is deleted
|
||||
return;
|
||||
} else if (newLines.includes(deleteLine)) { // Change occurred in new code
|
||||
let deleteStartIndex = newLines.indexOf(from.line);
|
||||
let deleteEndIndex = newLines.indexOf(to.line);
|
||||
|
||||
diff.newLines = newLines.map((line, index) => {
|
||||
if (index > deleteEndIndex) {
|
||||
return line - numLinesDeleted;
|
||||
}
|
||||
|
||||
return line;
|
||||
});
|
||||
|
||||
if (deleteStartIndex === -1) { // Deletion extends beyond merge line
|
||||
diff.newLines.splice(0, deleteEndIndex + 1);
|
||||
// TODO: Delete entire diff if merge line is deleted
|
||||
} else {
|
||||
diff.newLines.splice(deleteStartIndex + 1, deleteEndIndex - deleteStartIndex);
|
||||
}
|
||||
} else { // Change occurred outside of diff
|
||||
diff.oldLines = oldLines.map(line => line - numLinesDeleted);
|
||||
diff.mergeLine -= numLinesDeleted;
|
||||
diff.newLines = newLines.map(line => line - numLinesDeleted);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ code: newCode, diffs });
|
||||
}
|
||||
|
||||
onResolveDiff(diff, linesToDelete, indicatorLineNum) {
|
||||
window.gtag("event", "click_use_me");
|
||||
|
||||
const { code, diffs } = this.state;
|
||||
const { id: diffId, oldCodeWidget, newCodeWidget } = diff;
|
||||
|
||||
if (indicatorLineNum !== undefined) {
|
||||
let line = code[indicatorLineNum];
|
||||
|
||||
if (line === OLD_CODE_LABEL || line === FIXED_CODE_LABEL) {
|
||||
linesToDelete.push(indicatorLineNum);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete widgets from editor
|
||||
oldCodeWidget.clear();
|
||||
newCodeWidget.clear();
|
||||
|
||||
let numLinesDeleted = linesToDelete.length;
|
||||
let updatedDiffs = diffs.map((otherDiff, index) => {
|
||||
const {
|
||||
id: otherDiffId,
|
||||
oldLines,
|
||||
newLines,
|
||||
mergeLine
|
||||
} = otherDiff;
|
||||
|
||||
// If diff comes before one that was resolved, no line update needed
|
||||
if (otherDiffId <= diffId) {
|
||||
return otherDiff;
|
||||
}
|
||||
|
||||
// Updates line numbers in codeChange objects after lines are deleted
|
||||
otherDiff.oldLines = oldLines.map(line => line - numLinesDeleted);
|
||||
otherDiff.newLines = newLines.map(line => line - numLinesDeleted);
|
||||
otherDiff.mergeLine = mergeLine - numLinesDeleted;
|
||||
|
||||
return otherDiff;
|
||||
}).filter(otherDiff => otherDiff.id != diffId);
|
||||
let updatedCode = code.filter((_, index) => !linesToDelete.includes(index));
|
||||
|
||||
this.setState({ code: updatedCode, diffs: updatedDiffs });
|
||||
}
|
||||
|
||||
onDebug(errorMessage) {
|
||||
window.gtag("event", "click_debug");
|
||||
|
||||
const { code, language, apiKey } = this.state;
|
||||
|
||||
if (apiKey === "") {
|
||||
this.setState({ askForAPIKey: true });
|
||||
return;
|
||||
} else if (errorMessage === "") {
|
||||
return;
|
||||
} else {
|
||||
this.setState({ waitingForCodeFix: true });
|
||||
}
|
||||
|
||||
// function sleep(ms) {
|
||||
// return new Promise(resolve => setTimeout(resolve, ms));
|
||||
// }
|
||||
//
|
||||
// let inputCode = code.join("\n").trim().split("\n");
|
||||
// let gptCode = FIXED_CODE;
|
||||
// let { mergedCode, diffs } = diffGPTOutput(inputCode, gptCode);
|
||||
//
|
||||
// sleep(3000).then(() => this.setState({
|
||||
// waitingForCodeFix: false,
|
||||
// code: mergedCode,
|
||||
// diffs,
|
||||
// errorMessage,
|
||||
// errorExplanation: "This error message means that the Granger causality test statistic cannot be computed because the VAR (Vector Autoregression) model has a perfect fit of the data. This means that the data is too predictable and the VAR model is not able to find any meaningful relationships between the variables. To fix this, you can try using a different model or adjusting the parameters of the VAR model."
|
||||
// }));
|
||||
|
||||
const apiConfig = new Configuration({ apiKey });
|
||||
const api = new OpenAIApi(apiConfig);
|
||||
|
||||
let instruction = `Fix this error: ${errorMessage}`;
|
||||
api
|
||||
.createEdit({
|
||||
...EDIT_PROMPT_PARAMS, input: code.join("\n"), instruction
|
||||
})
|
||||
.then(data => {
|
||||
let inputCode = code.join("\n").trim().split("\n");
|
||||
let gptCode = data.data.choices[0].text.trim().replace(" ", "\t").split("\n");
|
||||
|
||||
let { mergedCode, diffs } = diffGPTOutput(inputCode, gptCode);
|
||||
|
||||
// let prompt = `Explain the following error message:\n\`\`\`\n${errorMessage}\n\`\`\``;
|
||||
let prompt = `Explain what this error message means and how to fix it:\n\`\`\`\n${errorMessage}\n\`\`\``;
|
||||
api
|
||||
.createCompletion({ ...COMPLETION_PROMPT_PARAMS, prompt })
|
||||
.then(data => {
|
||||
let errorExplanation = data.data.choices[0].text;
|
||||
this.setState({
|
||||
waitingForCodeFix: false,
|
||||
code: mergedCode,
|
||||
diffs,
|
||||
errorMessage,
|
||||
errorExplanation
|
||||
});
|
||||
}).
|
||||
catch(error => console.log(error.response));
|
||||
})
|
||||
.catch(error => console.log(error.response));
|
||||
};
|
||||
|
||||
onLint() {
|
||||
window.gtag("event", "click_lint");
|
||||
|
||||
const { code, language, apiKey } = this.state;
|
||||
|
||||
if (apiKey === "") {
|
||||
this.setState({ askForAPIKey: true });
|
||||
return;
|
||||
} else {
|
||||
this.setState({ waitingForCodeLint: true });
|
||||
}
|
||||
|
||||
const apiConfig = new Configuration({ apiKey });
|
||||
const api = new OpenAIApi(apiConfig);
|
||||
|
||||
let instruction = `Identify and fix all bugs in this ${language.label} code.`;
|
||||
|
||||
api
|
||||
.createEdit({
|
||||
...EDIT_PROMPT_PARAMS, input: code.join("\n"), instruction
|
||||
})
|
||||
.then(data => {
|
||||
let inputCode = code.join("\n").trim().split("\n");
|
||||
let gptCode = data.data.choices[0].text.trim().replace(" ", "\t").split("\n");
|
||||
let { mergedCode, diffs } = diffGPTOutput(inputCode, gptCode);
|
||||
|
||||
this.setState({
|
||||
waitingForCodeLint: false,
|
||||
code: mergedCode,
|
||||
diffs
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({ waitingForCodeLint: false });
|
||||
});
|
||||
}
|
||||
|
||||
onSelectLanguage(language) {
|
||||
window.gtag("event", "select_language", { language });
|
||||
|
||||
this.setState({ language });
|
||||
}
|
||||
|
||||
onOpenPopup() {
|
||||
window.gtag("event", "click_set_api_key");
|
||||
|
||||
this.setState({ askForAPIKey: true });
|
||||
}
|
||||
|
||||
onSetAPIKey(apiKey) { this.setState({ apiKey, askForAPIKey: false }); }
|
||||
|
||||
onClosePopup(event) {
|
||||
if (this.popupRef && this.popupRef.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ askForAPIKey: false });
|
||||
}
|
||||
|
||||
onSetPopupRef(ref) { this.popupRef = ref; }
|
||||
|
||||
render() {
|
||||
const { location } = this.props.router;
|
||||
const {
|
||||
language,
|
||||
code,
|
||||
diffs,
|
||||
errorExplanation,
|
||||
waitingForCodeFix,
|
||||
waitingForCodeLint,
|
||||
askForAPIKey,
|
||||
apiKey
|
||||
} = this.state;
|
||||
|
||||
window.gtag("event", "page_view", {
|
||||
page_path: location.pathname + location.search,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{askForAPIKey ? (
|
||||
<div className="popupLayer" onClick={this.onClosePopup}>
|
||||
<Popup
|
||||
onSubmit={this.onSetAPIKey}
|
||||
setRef={this.onSetPopupRef}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="app">
|
||||
<Header onClick={this.onOpenPopup} isPlaygroundActive={true} />
|
||||
<div className="body">
|
||||
<div className="lhs">
|
||||
<CodeEditor
|
||||
code={code}
|
||||
diffs={diffs}
|
||||
onResolveDiff={this.onResolveDiff}
|
||||
onChange={this.onCodeChange}
|
||||
language={language}
|
||||
onSelectLanguage={this.onSelectLanguage}
|
||||
isLoading={waitingForCodeLint}
|
||||
onLint={this.onLint}
|
||||
/>
|
||||
<ErrorMessage onDebug={this.onDebug} isLoading={waitingForCodeFix} />
|
||||
</div>
|
||||
<ErrorExplanation errorExplanation={errorExplanation} />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(App);
|
||||
150
src/routes/Landing.css
Normal file
@ -0,0 +1,150 @@
|
||||
@font-face {
|
||||
font-family: "Gilroy";
|
||||
src: url("../styles/fonts/Gilroy-ExtraBold.otf") format("opentype");
|
||||
}
|
||||
|
||||
.landing {
|
||||
position: absolute;
|
||||
background-color: #373751;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.landingLHS {
|
||||
/* padding-left: 60px; */
|
||||
padding-left: 5%;
|
||||
margin-top: -80px;
|
||||
}
|
||||
|
||||
.landingBody {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 125px;
|
||||
}
|
||||
|
||||
@media (max-width: 1130px) {
|
||||
.landingLHS {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 65%;
|
||||
padding: 0 !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.landingBody {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.landingHeading {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.landingTitle {
|
||||
font-size: 42px !important;
|
||||
}
|
||||
|
||||
.landingSubtitle {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.demoImage {
|
||||
padding-top: 60px !important;
|
||||
width: 50% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.landingLHS {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.landingHeading {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.landingTitle {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
|
||||
.landingSubtitle {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.ctaButtons {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.githubButton {
|
||||
margin-left: 0px !important;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.demoImage {
|
||||
padding-top: 40px !important;
|
||||
width: 85% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.landingHeading {
|
||||
/* width: 650px; */
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.landingTitle {
|
||||
color: white;
|
||||
font-size: 3.25vw;
|
||||
font-weight: 900;
|
||||
font-family: "Gilroy";
|
||||
}
|
||||
|
||||
.landingSubtitle {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-family: "Helvetica Neue";
|
||||
/* font-size: 22px; */
|
||||
font-size: 1.375vw;
|
||||
font-weight: 400;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.ctaButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: fit-content;
|
||||
padding-top: 15px;
|
||||
font-weight: 600;
|
||||
/* font-size: 1.125vw !important; */
|
||||
}
|
||||
|
||||
.ctaButtons a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.getStartedButton {
|
||||
color: white;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.githubButton {
|
||||
height: 45px !important;
|
||||
/* background-color: white; */
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.githubButton a {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demoImage {
|
||||
/* width: 600px; */
|
||||
width: 62.5%;
|
||||
/* padding-right: 60px; */
|
||||
padding-right: 5%;
|
||||
max-width: 600px;
|
||||
}
|
||||
95
src/routes/Landing.js
Normal file
@ -0,0 +1,95 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Popup from "../components/Popup";
|
||||
import Button from "../components/Button";
|
||||
import Header from "../containers/Header";
|
||||
|
||||
import { withRouter } from "../utilities";
|
||||
|
||||
import "./Landing.css";
|
||||
|
||||
class Landing extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onOpenPopup = this.onOpenPopup.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClosePopup = this.onClosePopup.bind(this);
|
||||
this.onSetPopupRef = this.onSetPopupRef.bind(this);
|
||||
|
||||
this.state = { askForAPIKey: false };
|
||||
}
|
||||
|
||||
onOpenPopup() {
|
||||
window.gtag("event", "click_set_api_key");
|
||||
|
||||
this.setState({ askForAPIKey: true });
|
||||
}
|
||||
|
||||
onSubmit() { this.setState({ askForAPIKey: false }); }
|
||||
|
||||
onClosePopup(event) {
|
||||
if (this.popupRef && this.popupRef.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ askForAPIKey: false });
|
||||
}
|
||||
|
||||
onSetPopupRef(ref) { this.popupRef = ref; }
|
||||
|
||||
render() {
|
||||
const { location } = this.props.router;
|
||||
const { askForAPIKey } = this.state;
|
||||
|
||||
window.gtag("event", "page_view", {
|
||||
page_path: location.pathname + location.search,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{askForAPIKey ? (
|
||||
<div className="popupLayer" onClick={this.onClosePopup}>
|
||||
<Popup
|
||||
onSubmit={this.onSubmit}
|
||||
setRef={this.onSetPopupRef}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="landing">
|
||||
<Header onClick={this.onOpenPopup} isPlaygroundActive={false} />
|
||||
<div className="landingBody">
|
||||
<div className="landingLHS">
|
||||
<div className="landingHeading">
|
||||
<span className="landingTitle">Stop plugging your errors into StackOverflow</span>
|
||||
<p className="landingSubtitle">Adrenaline is a debugging assistant powered by the OpenAI Codex. It can fix and explain your broken code in seconds.</p>
|
||||
</div>
|
||||
<div className="ctaButtons">
|
||||
<Link to="/playground">
|
||||
<Button
|
||||
className="getStartedButton"
|
||||
isPrimary
|
||||
onClick={() => window.gtag("event", "click_get_started")}
|
||||
>
|
||||
Fix your code
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
className="githubButton"
|
||||
isPrimary={false}
|
||||
onClick={() => window.gtag("event", "click_view_on_github")}
|
||||
>
|
||||
<a href="https://github.com/shobrook/adrenaline/" target="_blank">View on Github</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<img className="demoImage" src="demo.png" />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Landing);
|
||||
BIN
src/styles/fonts/Gilroy-ExtraBold.otf
Normal file
BIN
src/styles/fonts/Gilroy-Light.otf
Normal file
92
src/utilities.js
Normal file
@ -0,0 +1,92 @@
|
||||
import {
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams,
|
||||
} from "react-router-dom";
|
||||
import * as Diff from 'diff';
|
||||
|
||||
export const OLD_CODE_LABEL = ">>>>>>> OLD CODE";
|
||||
export const CODE_SEPARATOR = "=======";
|
||||
export const FIXED_CODE_LABEL = ">>>>>>> FIXED CODE"
|
||||
|
||||
export const range = (size, startAt = 0) => [...Array(size).keys()].map(i => i + startAt);
|
||||
|
||||
export const diffGPTOutput = (inputCode, gptCode) => {
|
||||
const diffResults = Diff.diffArrays(inputCode, gptCode);
|
||||
|
||||
let mergedCode = []; let diffs = [];
|
||||
let i = 0; let j = -1;
|
||||
let currDiffId = 0;
|
||||
while (i < diffResults.length) {
|
||||
let diffResult = diffResults[i];
|
||||
let numLinesChanged = diffResult.value.length;
|
||||
let diff = { id: currDiffId, oldLines: [], mergeLine: -1, newLines: [] }
|
||||
|
||||
// Assumes deletions always come before insertions
|
||||
if (diffResult.removed) {
|
||||
mergedCode.push(OLD_CODE_LABEL); j += 1;
|
||||
diff.oldLines.push(j);
|
||||
|
||||
diff.oldLines.push(...range(numLinesChanged, j + 1));
|
||||
mergedCode.push(...diffResult.value); j += numLinesChanged;
|
||||
|
||||
mergedCode.push(CODE_SEPARATOR); j += 1;
|
||||
diff.mergeLine = j;
|
||||
|
||||
if (i < diffResults.length - 1 && diffResults[i + 1].added) { // Deletion with an insertion
|
||||
diff.newLines.push(...range(diffResults[i + 1].value.length, j + 1));
|
||||
mergedCode.push(...diffResults[i + 1].value); j += diffResults[i + 1].value.length;
|
||||
|
||||
i += 2;
|
||||
} else { // Deletion with no insertion
|
||||
i += 1;
|
||||
}
|
||||
|
||||
mergedCode.push(FIXED_CODE_LABEL); j += 1;
|
||||
diff.newLines.push(j);
|
||||
|
||||
diffs.push(diff);
|
||||
currDiffId++;
|
||||
|
||||
continue;
|
||||
} else if (diffResult.added) { // Insertion with no deletion
|
||||
mergedCode.push(OLD_CODE_LABEL); j += 1;
|
||||
diff.oldLines.push(j);
|
||||
|
||||
mergedCode.push(CODE_SEPARATOR); j += 1;
|
||||
diff.mergeLine = j;
|
||||
|
||||
diff.newLines.push(...range(numLinesChanged + 1, j + 1));
|
||||
mergedCode.push(...diffResult.value); j += numLinesChanged;
|
||||
mergedCode.push(FIXED_CODE_LABEL); j += 1;
|
||||
|
||||
diffs.push(diff);
|
||||
currDiffId++;
|
||||
} else { // No deletion or insertion
|
||||
mergedCode.push(...diffResult.value); j += numLinesChanged;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
};
|
||||
|
||||
return {
|
||||
diffs,
|
||||
mergedCode,
|
||||
};
|
||||
}
|
||||
|
||||
export function withRouter(Component) {
|
||||
function ComponentWithRouterProp(props) {
|
||||
let location = useLocation();
|
||||
let navigate = useNavigate();
|
||||
let params = useParams();
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
router={{ location, navigate, params }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return ComponentWithRouterProp;
|
||||
}
|
||||