From 17e46a291b88240070b65fbda67ca8b0f12f9907 Mon Sep 17 00:00:00 2001 From: Jesse Date: Thu, 1 Jun 2023 13:14:46 -0400 Subject: [PATCH] initial commit for connecting to the OpenAI API and sending requests, see #94 --- client/common/SceneryDisplay.js | 7 ++ .../{creator/view => common}/ViewConstants.js | 2 +- client/creator-ai/CreatorAIMain.css | 93 ++++++++++++++++++ client/creator-ai/CreatorAIMain.js | 97 +++++++++++++++++++ client/creator-ai/entry.js | 23 +++++ client/creator/CreatorMain.js | 7 -- client/creator/view/ComponentListItemNode.js | 2 +- client/creator/view/CreatorView.js | 2 +- client/creator/view/DeleteProgramAreaNode.js | 2 +- client/creator/view/ProgramNode.js | 2 +- client/utils.js | 5 + server/api.js | 91 +++++++++++++---- server/main.js | 13 ++- webpack.config.js | 3 +- www/creator-ai.html | 24 +++++ www/index.html | 3 + www/media/images/ai-icon.svg | 3 + www/media/images/person-icon.svg | 3 + 18 files changed, 347 insertions(+), 35 deletions(-) rename client/{creator/view => common}/ViewConstants.js (96%) create mode 100644 client/creator-ai/CreatorAIMain.css create mode 100644 client/creator-ai/CreatorAIMain.js create mode 100644 client/creator-ai/entry.js create mode 100644 www/creator-ai.html create mode 100644 www/media/images/ai-icon.svg create mode 100644 www/media/images/person-icon.svg diff --git a/client/common/SceneryDisplay.js b/client/common/SceneryDisplay.js index 8302a75f..0876b141 100644 --- a/client/common/SceneryDisplay.js +++ b/client/common/SceneryDisplay.js @@ -3,6 +3,7 @@ */ import React, { useEffect } from 'react'; +import ViewConstants from './ViewConstants.js'; const SceneryDisplay = props => { @@ -78,6 +79,12 @@ const SceneryDisplay = props => { phet.utteranceQueue.responseCollector.objectResponsesEnabledProperty.value = true; phet.utteranceQueue.responseCollector.contextResponsesEnabledProperty.value = true; phet.utteranceQueue.responseCollector.hintResponsesEnabledProperty.value = true; + + // custom focus highlight colors for this display + phet.scenery.HighlightOverlay.setInnerHighlightColor( ViewConstants.focusHighlightColor ); + phet.scenery.HighlightOverlay.setOuterHilightColor( ViewConstants.focusHighlightColor ); + phet.scenery.HighlightOverlay.setInnerGroupHighlightColor( ViewConstants.focusHighlightColor ); + phet.scenery.HighlightOverlay.setOuterGroupHighlightColor( ViewConstants.focusHighlightColor ); }, [] ); return
; diff --git a/client/creator/view/ViewConstants.js b/client/common/ViewConstants.js similarity index 96% rename from client/creator/view/ViewConstants.js rename to client/common/ViewConstants.js index 37214045..5870dedf 100644 --- a/client/creator/view/ViewConstants.js +++ b/client/common/ViewConstants.js @@ -1,4 +1,4 @@ -import CustomButtonAppearanceStrategy from './CustomButtonAppearanceStrategy.js'; +import CustomButtonAppearanceStrategy from '../creator/view/CustomButtonAppearanceStrategy.js'; const TEXT_FONT = new phet.scenery.Font( { size: 16 } ); const TEXT_FILL_COLOR = new phet.scenery.Color( 189, 203, 218 ); diff --git a/client/creator-ai/CreatorAIMain.css b/client/creator-ai/CreatorAIMain.css new file mode 100644 index 00000000..0e0d770a --- /dev/null +++ b/client/creator-ai/CreatorAIMain.css @@ -0,0 +1,93 @@ +@value baseButtonColor: rgb(89, 93, 94); + +@value pressedButtonColor: rgb(54,56,57); + +@value buttonBorderColor: transparent; + +@value focusHighlightColor: #6C8EAC; + +/*background color for the page*/ +@value backgroundColor: #2E4152; + +@value textColor: #bdcbda; + +@value padding: 10px; + +:local(.container ) { + display: flex; + flex-direction: column; + row-gap: 20px; + height: 100vh; +} + +:local(.row) { + display: flex; + flex-direction: column; + column-gap: 20px; + flex: 1; +} + +/*applied to many panels in the board page*/ +:local(.panelClass) { + border-color: #6C8EAC; + border-style: solid; + color: textColor; + padding: 10px; + height: 100vh; + overflow: auto; +} + +:local(.inputElement ) { + padding-top: padding; +} + +/* Container for chat components, aligning them in a column and taking up full space (regardless of contents) */ +:local(.chatForm) { + display: flex; + flex-direction: column; + height: 100%; /* Set the height of the parent container */ +} + +/* So that the chat input is at the bottom of the chat form */ +:local(.chatInput) { + margin-top: auto; +} + +/* So that the chat output is above with a scrollbar */ +:local(.chatOutput) { + height: 85%; + overflow: auto; +} + +/* Custom styling for list group items */ +:local(.listGroupItem ) { + background-color: backgroundColor; + color: textColor; +} + +:local(.meChatItem) { + background-color: backgroundColor; + color: textColor; +} + +:local(.botChatItem) { + background-color: #212f3a; + color: textColor; +} + +/* Apply colors to the bootstrap icons */ +:local(.chatIcon ) { + scale: 1.5; + padding: padding; +} + +:local(.chatMessage) { +} + +:local(.iconWithMessage ) { + display: flex; + justify-content: flex-start; + /*align-content: flex-end;*/ + gap: padding; +} + diff --git a/client/creator-ai/CreatorAIMain.js b/client/creator-ai/CreatorAIMain.js new file mode 100644 index 00000000..dc304b5a --- /dev/null +++ b/client/creator-ai/CreatorAIMain.js @@ -0,0 +1,97 @@ +/** + * Main react component for the CreatorAI page. + */ + +import React, { useState } from 'react'; +import Form from 'react-bootstrap/Form'; +import ListGroup from 'react-bootstrap/ListGroup'; +import { combineClasses } from '../utils.js'; +import styles from './CreatorAIMain.css'; + +export default function CreatorAIMain( props ) { + + // add react state for input an chatLog + const [ input, setInput ] = useState( '' ); + + // Items of the log, with values { user: 'me' | 'ai', message: String, type: 'chat' | 'error' } + const [ chatLog, setChatLog ] = useState( [] ); + + const handleSubmit = async event => { + + // no page refresh on submit + event.preventDefault(); + + const newChatLog = [ ...chatLog, { user: 'me', message: input, type: 'chat' } ]; + await setChatLog( newChatLog ); + + setInput( '' ); + + + // for chat, we feed all previous messages for a new response + const allMessages = newChatLog.map( message => message.message ).join( '\n' ); + + const response = await fetch( 'http://localhost:3000/openai', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { prompt: allMessages } ) + } ); + + const data = await response.json(); + + if ( data.error ) { + await setChatLog( [ ...newChatLog, { user: 'ai', message: data.error.message, type: 'error' } ] ); + } + else if ( data.choices.length > 0 ) { + await setChatLog( [ ...newChatLog, { user: 'ai', message: data.choices[ 0 ].text, type: 'chat' } ] ); + } + }; + + return ( +
+
+
+
+ + { + chatLog.map( ( logItem, index ) => ( + + ) ) + } + +
+
+
+ setInput( event.target.value )} + /> + +
+
+
+
Scenery
+
+ ); +} + +function ChatItem( props ) { + return ( + +
+
+ { + props.user === 'me' ? + : + + } +
+

{props.message}

+
+
+ ); +} \ No newline at end of file diff --git a/client/creator-ai/entry.js b/client/creator-ai/entry.js new file mode 100644 index 00000000..e97c4354 --- /dev/null +++ b/client/creator-ai/entry.js @@ -0,0 +1,23 @@ +/** + * Entry-point file for the Sim Design Board, which is a scene graph based on the PhET libraries that uses PhET + * components that can be manipulated using the paper programs. + * + * @author Jesse Greenberg (PhET Interactive Simulations) + */ + +import 'bootstrap/dist/css/bootstrap.min.css'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import CreatorAIMain from './CreatorAIMain.js'; + +// Create the root element for React. +const simDisplayDiv = document.getElementById( 'creator-root-element' ); +document.body.appendChild( simDisplayDiv ); + +// Create the root of the scene graph. +const scene = new phet.scenery.Node(); + +ReactDOM.render( + , + simDisplayDiv +); \ No newline at end of file diff --git a/client/creator/CreatorMain.js b/client/creator/CreatorMain.js index af224416..b42401b6 100644 --- a/client/creator/CreatorMain.js +++ b/client/creator/CreatorMain.js @@ -8,7 +8,6 @@ import styles from './CreatorMain.css'; import CreatorModel from './model/CreatorModel.js'; import CreatorControls from './react/CreatorControls.js'; import CreatorView from './view/CreatorView.js'; -import ViewConstants from './view/ViewConstants.js'; export default function CreatorMain( props ) { const scene = props.scene; @@ -87,12 +86,6 @@ export default function CreatorMain( props ) { }, true ); }; - // custom focus highlight colors for this display - phet.scenery.HighlightOverlay.setInnerHighlightColor( ViewConstants.focusHighlightColor ); - phet.scenery.HighlightOverlay.setOuterHilightColor( ViewConstants.focusHighlightColor ); - phet.scenery.HighlightOverlay.setInnerGroupHighlightColor( ViewConstants.focusHighlightColor ); - phet.scenery.HighlightOverlay.setOuterGroupHighlightColor( ViewConstants.focusHighlightColor ); - return (
diff --git a/client/creator/view/ComponentListItemNode.js b/client/creator/view/ComponentListItemNode.js index 5835dfc0..edb26909 100644 --- a/client/creator/view/ComponentListItemNode.js +++ b/client/creator/view/ComponentListItemNode.js @@ -1,5 +1,5 @@ import ImageLoader from './ImageLoader.js'; -import ViewConstants from './ViewConstants.js'; +import ViewConstants from '../../common/ViewConstants.js'; export default class ComponentListItemNode extends phet.scenery.Node { constructor( namedProperty, programWidth ) { diff --git a/client/creator/view/CreatorView.js b/client/creator/view/CreatorView.js index 8e32e248..e171afca 100644 --- a/client/creator/view/CreatorView.js +++ b/client/creator/view/CreatorView.js @@ -6,7 +6,7 @@ import DeleteProgramAreaNode from './DeleteProgramAreaNode.js'; import ProgramNode from './ProgramNode.js'; -import ViewConstants from './ViewConstants.js'; +import ViewConstants from '../../common/ViewConstants.js'; export default class CreatorView extends phet.scenery.Node { diff --git a/client/creator/view/DeleteProgramAreaNode.js b/client/creator/view/DeleteProgramAreaNode.js index c05c6bed..a8c1522b 100644 --- a/client/creator/view/DeleteProgramAreaNode.js +++ b/client/creator/view/DeleteProgramAreaNode.js @@ -1,5 +1,5 @@ import ImageLoader from './ImageLoader.js'; -import ViewConstants from './ViewConstants.js'; +import ViewConstants from '../../common/ViewConstants.js'; export default class DeleteProgramAreaNode extends phet.scenery.Node { constructor() { diff --git a/client/creator/view/ProgramNode.js b/client/creator/view/ProgramNode.js index 12ae151e..1abb2f3d 100644 --- a/client/creator/view/ProgramNode.js +++ b/client/creator/view/ProgramNode.js @@ -1,7 +1,7 @@ import ActiveEdit from '../model/ActiveEdit.js'; import EditType from '../model/EditType.js'; import ComponentListItemNode from './ComponentListItemNode.js'; -import ViewConstants from './ViewConstants.js'; +import ViewConstants from '../../common/ViewConstants.js'; // default dimensions of a paper, though it may change as components are added const WIDTH = 70; diff --git a/client/utils.js b/client/utils.js index e0186f2b..81079320 100644 --- a/client/utils.js +++ b/client/utils.js @@ -1,5 +1,10 @@ import Matrix from 'node-matrices'; +// A function that takes a list of class name strings and combines them into a single string. +export function combineClasses( ...classes ) { + return classes.join( ' ' ); +} + export function norm( vector ) { if ( vector.x !== undefined ) { return norm( [ vector.x, vector.y ] ); diff --git a/server/api.js b/server/api.js index ea7bd6f9..72a84db4 100644 --- a/server/api.js +++ b/server/api.js @@ -1,10 +1,11 @@ const express = require( 'express' ); const crypto = require( 'crypto' ); const restrictedSpacesList = require( './restrictedSpacesList.js' ); +const { Configuration, OpenAIApi } = require( 'openai' ); -const router = express.Router(); -router.use( express.json() ); -router.use( require( 'nocache' )() ); +const paperLandRouter = express.Router(); +paperLandRouter.use( express.json() ); +paperLandRouter.use( require( 'nocache' )() ); const knex = require( 'knex' )( require( '../knexfile' )[ process.env.NODE_ENV || 'development' ] ); @@ -17,7 +18,7 @@ const editorHandleDuration = 1500; /** * Get the current code for the specified space name and program number. */ -router.get( '/program.:spaceName.:number.js', ( req, res ) => { +paperLandRouter.get( '/program.:spaceName.:number.js', ( req, res ) => { const { spaceName, number } = req.params; knex .select( 'currentCode' ) @@ -49,7 +50,7 @@ router.get( '/program.:spaceName.:number.js', ( req, res ) => { * * @param spacesList - A comma separated list of the space names to query, or '*' for all spaces. */ -router.get( '/api/program-summary-list/:spacesList', ( req, res ) => { +paperLandRouter.get( '/api/program-summary-list/:spacesList', ( req, res ) => { const { spacesList } = req.params; let summaryQuery = knex.select( [ 'currentCode', 'number', 'spaceName' ] ).from( 'programs' ); @@ -71,7 +72,7 @@ router.get( '/api/program-summary-list/:spacesList', ( req, res ) => { } ); // Get a list of all the spaces available in the DB. -router.get( '/api/spaces-list', ( req, res ) => { +paperLandRouter.get( '/api/spaces-list', ( req, res ) => { knex .distinct() .from( 'programs' ) @@ -85,7 +86,7 @@ router.get( '/api/spaces-list', ( req, res ) => { } ); // Add a new space to the DB. -router.get( '/api/add-space/:newSpaceName', ( req, res ) => { +paperLandRouter.get( '/api/add-space/:newSpaceName', ( req, res ) => { console.log( `req.params.newSpaceName = ${req.params.newSpaceName}` ); res.json( req.params ); } ); @@ -122,7 +123,7 @@ function getSpaceData( req, callback ) { } ); } -router.get( '/api/spaces/:spaceName', ( req, res ) => { +paperLandRouter.get( '/api/spaces/:spaceName', ( req, res ) => { getSpaceData( req, spaceData => { res.json( spaceData ); } ); @@ -134,7 +135,7 @@ router.get( '/api/spaces/:spaceName', ( req, res ) => { * @param spaceName - The space to save the program to. */ const maxNumber = 8400 / 4; -router.post( '/api/spaces/:spaceName/programs', ( req, res ) => { +paperLandRouter.post( '/api/spaces/:spaceName/programs', ( req, res ) => { const { spaceName } = req.params; // extract code from the request @@ -174,7 +175,7 @@ router.post( '/api/spaces/:spaceName/programs', ( req, res ) => { // Create a new snippet const maxSnippets = 500; -router.post( '/api/snippets', ( req, res ) => { +paperLandRouter.post( '/api/snippets', ( req, res ) => { const { snippetCode } = req.body; if ( !snippetCode ) { res.status( 400 ).send( 'Missing "code"' ); @@ -198,7 +199,7 @@ router.post( '/api/snippets', ( req, res ) => { } ); // Save the program with the provided number to the provided space. -router.put( '/api/spaces/:spaceName/programs/:number', ( req, res ) => { +paperLandRouter.put( '/api/spaces/:spaceName/programs/:number', ( req, res ) => { const { spaceName, number } = req.params; const { code } = req.body; if ( !code ) { @@ -214,7 +215,7 @@ router.put( '/api/spaces/:spaceName/programs/:number', ( req, res ) => { } ); // Get all code snippets in the database -router.get( '/api/snippets', ( req, res ) => { +paperLandRouter.get( '/api/snippets', ( req, res ) => { knex .select( [ 'code', 'number' ] ) .from( 'snippets' ) @@ -224,7 +225,7 @@ router.get( '/api/snippets', ( req, res ) => { } ); // Save the snippet of the provided number -router.put( '/api/snippets/:number', ( req, res ) => { +paperLandRouter.put( '/api/snippets/:number', ( req, res ) => { const { number } = req.params; const { snippetCode } = req.body; @@ -240,7 +241,7 @@ router.put( '/api/snippets/:number', ( req, res ) => { } ); } ); -router.post( '/api/spaces/:spaceName/programs/:number/markPrinted', ( req, res ) => { +paperLandRouter.post( '/api/spaces/:spaceName/programs/:number/markPrinted', ( req, res ) => { const { spaceName, number } = req.params; const { printed } = req.body; if ( printed === undefined ) { @@ -260,7 +261,7 @@ router.post( '/api/spaces/:spaceName/programs/:number/markPrinted', ( req, res ) /** * Delete the specified program from the specified space. */ -router.get( '/api/spaces/:spaceName/delete/:programNumber', ( req, res ) => { +paperLandRouter.get( '/api/spaces/:spaceName/delete/:programNumber', ( req, res ) => { const { spaceName, programNumber } = req.params; knex( 'programs' ) .where( { spaceName, number: programNumber } ) @@ -270,7 +271,7 @@ router.get( '/api/spaces/:spaceName/delete/:programNumber', ( req, res ) => { } ); } ); -router.put( '/api/spaces/:spaceName/programs/:number/debugInfo', ( req, res ) => { +paperLandRouter.put( '/api/spaces/:spaceName/programs/:number/debugInfo', ( req, res ) => { const { spaceName, number } = req.params; knex( 'programs' ) @@ -281,7 +282,7 @@ router.put( '/api/spaces/:spaceName/programs/:number/debugInfo', ( req, res ) => } ); } ); -router.post( '/api/spaces/:spaceName/programs/:number/claim', ( req, res ) => { +paperLandRouter.post( '/api/spaces/:spaceName/programs/:number/claim', ( req, res ) => { const { spaceName, number } = req.params; knex @@ -315,4 +316,58 @@ router.post( '/api/spaces/:spaceName/programs/:number/claim', ( req, res ) => { } ); } ); -module.exports = router; \ No newline at end of file +//---------------------------------------------------------------------- +// Routes for the OpenAI requests +//---------------------------------------------------------------------- +const openAIRouter = express.Router(); +openAIRouter.use( express.json() ); +openAIRouter.use( require( 'nocache' )() ); + +// OpenAI configuration for requests +const configuration = new Configuration( { + organization: process.env.OPENAI_ORGANIZATION, + apiKey: process.env.OPENAI_API_KEY +} ); +const openai = new OpenAIApi( configuration ); + + +// Make a post to the openAI router. Recall that the path through this router +// is /openai +openAIRouter.post( '/', async ( req, res ) => { + + try { + + // To test responses + // res.json( { + // choices: [ { text: 'This is a response' } ] + // } ); + + const prompt = req.body.prompt; + console.log( prompt ); + + const response = await openai.createCompletion( { + model: 'text-davinci-003', + prompt: prompt, + // max_tokens: 7, + // temperature: 0 + } ); + + res.json( response.data ); + } + catch( error ) { + if ( error.response ) { + res.json( error.response.data ); + } + else { + res.json( { + data: { + error: { + message: error.message + } + } + } ); + } + } +} ); + +module.exports = { paperLandRouter, openAIRouter }; \ No newline at end of file diff --git a/server/main.js b/server/main.js index 530b5b65..708e3daf 100644 --- a/server/main.js +++ b/server/main.js @@ -1,6 +1,6 @@ -const api = require( './api' ); const express = require( 'express' ); const path = require( 'path' ); +const { paperLandRouter, openAIRouter } = require( './api.js' ); express.static.mime.types.wasm = 'application/wasm'; @@ -12,13 +12,18 @@ const app = express(); app.use( require( 'morgan' )( 'short' ) ); app.use( require( 'heroku-ssl-redirect' )( [ 'production' ] ) ); app.use( express.static( path.join( __dirname, '..', 'www' ) ) ); -app.use( api ); +app.use( '/', paperLandRouter ); +app.use( '/openai', openAIRouter ); if ( process.env.NODE_ENV !== 'production' ) { const compiler = require( 'webpack' )( require( '../webpack.config.js' ) ); app.use( require( 'webpack-dev-middleware' )( compiler ) ); } - + const port = process.env.PORT || 3000; -app.listen( port, () => console.log( `Listening on port ${port}!` ) ); \ No newline at end of file +app.listen( port, () => console.log( `Listening on port ${port}!` ) ); + +if ( process.env.OPENAI_API_KEY && process.env.OPENAI_ORGANIZATION ) { + console.log( 'Key detected-----------------------------' ); +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index fe233869..1cecb92b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,7 +9,8 @@ module.exports = { paper: [ './client/paper/entry.js' ], board: [ './client/board/entry.js' ], tests: [ './client/tests/entry.js' ], - creator: [ './client/creator/entry.js' ] + creator: [ './client/creator/entry.js' ], + creatorai: [ './client/creator-ai/entry.js' ] }, output: { path: path.join( __dirname, 'www' ), diff --git a/www/creator-ai.html b/www/creator-ai.html new file mode 100644 index 00000000..99e17c1f --- /dev/null +++ b/www/creator-ai.html @@ -0,0 +1,24 @@ + + + + + Creator AI + + + + + +
+ + + + + + + diff --git a/www/index.html b/www/index.html index 2a747347..e4f58134 100644 --- a/www/index.html +++ b/www/index.html @@ -33,6 +33,9 @@

To run the tool, open the three pages below:

  • Creator - A tool to help create programs and write code.
  • +
  • + CreatorAI - An AI powered tool to create paper programs. +
  • diff --git a/www/media/images/ai-icon.svg b/www/media/images/ai-icon.svg new file mode 100644 index 00000000..ef7ba96a --- /dev/null +++ b/www/media/images/ai-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/www/media/images/person-icon.svg b/www/media/images/person-icon.svg new file mode 100644 index 00000000..c3e125c7 --- /dev/null +++ b/www/media/images/person-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file