diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index b06efcf956..cbcd019abc 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -46,6 +46,7 @@ import {FileDecorator} from './app/plugins/file-decorator' import {CodeFormat} from './app/plugins/code-format' import {SolidityUmlGen} from './app/plugins/solidity-umlgen' import {ContractFlattener} from './app/plugins/contractFlattener' +import {OpenAIGpt} from './app/plugins/openaigpt' const isElectron = require('is-electron') @@ -180,6 +181,9 @@ class AppComponent { // ----------------- ContractFlattener ---------------------------- const contractFlattener = new ContractFlattener() + // ----------------- Open AI -------------------------------------- + const openaigpt = new OpenAIGpt() + // ----------------- import content service ------------------------ const contentImport = new CompilerImports() @@ -297,7 +301,8 @@ class AppComponent { search, solidityumlgen, contractFlattener, - solidityScript + solidityScript, + openaigpt ]) // LAYOUT & SYSTEM VIEWS @@ -410,7 +415,7 @@ class AppComponent { ]) await this.appManager.activatePlugin(['settings']) await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder']) - await this.appManager.activatePlugin(['solidity-script']) + await this.appManager.activatePlugin(['solidity-script', 'openaigpt']) this.appManager.on('filePanel', 'workspaceInitializationCompleted', async () => { // for e2e tests diff --git a/apps/remix-ide/src/app/panels/layout.ts b/apps/remix-ide/src/app/panels/layout.ts index 3a44d1f83b..a152efc72e 100644 --- a/apps/remix-ide/src/app/panels/layout.ts +++ b/apps/remix-ide/src/app/panels/layout.ts @@ -6,7 +6,7 @@ import { QueryParams } from '@remix-project/remix-lib' const profile: Profile = { name: 'layout', description: 'layout', - methods: ['minimize', 'maximiseSidePanel', 'resetSidePanel'] + methods: ['minimize', 'maximiseSidePanel', 'resetSidePanel', 'maximizeTerminal'] } interface panelState { @@ -109,6 +109,12 @@ export class Layout extends Plugin { this.maximised[current] = true } + async maximizeTerminal() { + this.panels.terminal.minimized = false + this.event.emit('change', this.panels) + this.emit('change', this.panels) + } + async resetSidePanel () { this.event.emit('resetsidepanel') const current = await this.call('sidePanel', 'currentFocus') diff --git a/apps/remix-ide/src/app/plugins/openaigpt.tsx b/apps/remix-ide/src/app/plugins/openaigpt.tsx new file mode 100644 index 0000000000..e95babe00a --- /dev/null +++ b/apps/remix-ide/src/app/plugins/openaigpt.tsx @@ -0,0 +1,47 @@ +import { Plugin } from '@remixproject/engine' +import { CreateChatCompletionResponse } from 'openai' + +const _paq = (window._paq = window._paq || []) + +const profile = { + name: 'openaigpt', + displayName: 'openaigpt', + description: 'openaigpt', + methods: ['message'], + events: [], + maintainedBy: 'Remix', +} + +export class OpenAIGpt extends Plugin { + constructor() { + super(profile) + } + + async message(prompt): Promise { + this.call('layout', 'maximizeTerminal') + this.call('terminal', 'log', 'Waiting for GPT answer...') + let result + try { + result = await ( + await fetch('https://openai-gpt.remixproject.org', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ prompt }), + }) + ).json() + } catch (e) { + this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` }) + return + } + + if (result && result.choices && result.choices.length) { + this.call('terminal', 'log', { type: 'typewritersuccess', value: result.choices[0].message.content }) + } else { + this.call('terminal', 'log', { type: 'typewritersuccess', value: 'No response...' }) + } + return result.data + } +} diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index 55d5c4025c..691c8955e9 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -1,30 +1,30 @@ -import React, {useState, useRef, useEffect, useReducer} from 'react' // eslint-disable-line -import {isArray} from 'lodash' -import Editor, {loader, Monaco} from '@monaco-editor/react' -import {AlertModal} from '@remix-ui/app' -import {reducerActions, reducerListener, initialState} from './actions/editor' -import {solidityTokensProvider, solidityLanguageConfig} from './syntaxes/solidity' -import {cairoTokensProvider, cairoLanguageConfig} from './syntaxes/cairo' -import {zokratesTokensProvider, zokratesLanguageConfig} from './syntaxes/zokrates' -import {moveTokenProvider, moveLanguageConfig} from './syntaxes/move' -import {monacoTypes} from '@remix-ui/editor' -import {loadTypes} from './web-types' -import {retrieveNodesAtPosition} from './helpers/retrieveNodesAtPosition' -import {RemixHoverProvider} from './providers/hoverProvider' -import {RemixReferenceProvider} from './providers/referenceProvider' -import {RemixCompletionProvider} from './providers/completionProvider' -import {RemixHighLightProvider} from './providers/highlightProvider' -import {RemixDefinitionProvider} from './providers/definitionProvider' -import {RemixCodeActionProvider} from './providers/codeActionProvider' +import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line +import { isArray } from 'lodash' +import Editor, { loader, Monaco } from '@monaco-editor/react' +import { AlertModal } from '@remix-ui/app' +import { reducerActions, reducerListener, initialState } from './actions/editor' +import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solidity' +import { cairoTokensProvider, cairoLanguageConfig } from './syntaxes/cairo' +import { zokratesTokensProvider, zokratesLanguageConfig } from './syntaxes/zokrates' +import { moveTokenProvider, moveLanguageConfig } from './syntaxes/move' +import { monacoTypes } from '@remix-ui/editor' +import { loadTypes } from './web-types' +import { retrieveNodesAtPosition } from './helpers/retrieveNodesAtPosition' +import { RemixHoverProvider } from './providers/hoverProvider' +import { RemixReferenceProvider } from './providers/referenceProvider' +import { RemixCompletionProvider } from './providers/completionProvider' +import { RemixHighLightProvider } from './providers/highlightProvider' +import { RemixDefinitionProvider } from './providers/definitionProvider' +import { RemixCodeActionProvider } from './providers/codeActionProvider' import './remix-ui-editor.css' -import {circomLanguageConfig, circomTokensProvider} from './syntaxes/circom' -import {IPosition} from 'monaco-editor' +import { circomLanguageConfig, circomTokensProvider } from './syntaxes/circom' +import { IPosition } from 'monaco-editor' enum MarkerSeverity { Hint = 1, Info = 2, Warning = 4, - Error = 8 + Error = 8, } type sourceAnnotation = { @@ -86,7 +86,7 @@ type errorMarker = { file: string } -loader.config({paths: {vs: 'assets/js/monaco-editor/min/vs'}}) +loader.config({ paths: { vs: 'assets/js/monaco-editor/min/vs' } }) export type DecorationsReturn = { currentDecorations: Array @@ -108,7 +108,8 @@ export type EditorAPIType = { clearDecorationsByPlugin: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn keepDecorationsFor: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn addErrorMarker: (errors: errorMarker[], from: string) => void - clearErrorMarkers: (sources: string[] | {[fileName: string]: any}, from: string) => void + clearErrorMarkers: (sources: string[] | { [fileName: string]: any }, from: string) => void + getPositionAt: (offset: number) => monacoTypes.IPosition } /* eslint-disable-next-line */ @@ -152,6 +153,7 @@ export const EditorUI = (props: EditorUIProps) => { const pasteCodeRef = useRef(false) const editorRef = useRef(null) const monacoRef = useRef(null) + const currentFunction = useRef('') const currentFileRef = useRef('') const currentUrlRef = useRef('') // const currentDecorations = useRef({ sourceAnnotationsPerFile: {}, markerPerFile: {} }) // decorations that are currently in use by the editor @@ -194,88 +196,88 @@ export const EditorUI = (props: EditorUIProps) => { base: themeType, inherit: true, // can also be false to completely replace the builtin rules rules: [ - {background: darkColor.replace('#', '')}, - {foreground: textColor.replace('#', '')}, + { background: darkColor.replace('#', '') }, + { foreground: textColor.replace('#', '') }, // global variables - {token: 'keyword.abi', foreground: blueColor}, - {token: 'keyword.block', foreground: blueColor}, - {token: 'keyword.bytes', foreground: blueColor}, - {token: 'keyword.msg', foreground: blueColor}, - {token: 'keyword.tx', foreground: blueColor}, + { token: 'keyword.abi', foreground: blueColor }, + { token: 'keyword.block', foreground: blueColor }, + { token: 'keyword.bytes', foreground: blueColor }, + { token: 'keyword.msg', foreground: blueColor }, + { token: 'keyword.tx', foreground: blueColor }, // global functions - {token: 'keyword.assert', foreground: blueColor}, - {token: 'keyword.require', foreground: blueColor}, - {token: 'keyword.revert', foreground: blueColor}, - {token: 'keyword.blockhash', foreground: blueColor}, - {token: 'keyword.keccak256', foreground: blueColor}, - {token: 'keyword.sha256', foreground: blueColor}, - {token: 'keyword.ripemd160', foreground: blueColor}, - {token: 'keyword.ecrecover', foreground: blueColor}, - {token: 'keyword.addmod', foreground: blueColor}, - {token: 'keyword.mulmod', foreground: blueColor}, - {token: 'keyword.selfdestruct', foreground: blueColor}, - {token: 'keyword.type ', foreground: blueColor}, - {token: 'keyword.gasleft', foreground: blueColor}, + { token: 'keyword.assert', foreground: blueColor }, + { token: 'keyword.require', foreground: blueColor }, + { token: 'keyword.revert', foreground: blueColor }, + { token: 'keyword.blockhash', foreground: blueColor }, + { token: 'keyword.keccak256', foreground: blueColor }, + { token: 'keyword.sha256', foreground: blueColor }, + { token: 'keyword.ripemd160', foreground: blueColor }, + { token: 'keyword.ecrecover', foreground: blueColor }, + { token: 'keyword.addmod', foreground: blueColor }, + { token: 'keyword.mulmod', foreground: blueColor }, + { token: 'keyword.selfdestruct', foreground: blueColor }, + { token: 'keyword.type ', foreground: blueColor }, + { token: 'keyword.gasleft', foreground: blueColor }, // specials - {token: 'keyword.super', foreground: infoColor}, - {token: 'keyword.this', foreground: infoColor}, - {token: 'keyword.virtual', foreground: infoColor}, + { token: 'keyword.super', foreground: infoColor }, + { token: 'keyword.this', foreground: infoColor }, + { token: 'keyword.virtual', foreground: infoColor }, // for state variables - {token: 'keyword.constants', foreground: grayColor}, - {token: 'keyword.override', foreground: grayColor}, - {token: 'keyword.immutable', foreground: grayColor}, + { token: 'keyword.constants', foreground: grayColor }, + { token: 'keyword.override', foreground: grayColor }, + { token: 'keyword.immutable', foreground: grayColor }, // data location - {token: 'keyword.memory', foreground: locationColor}, - {token: 'keyword.storage', foreground: locationColor}, - {token: 'keyword.calldata', foreground: locationColor}, + { token: 'keyword.memory', foreground: locationColor }, + { token: 'keyword.storage', foreground: locationColor }, + { token: 'keyword.calldata', foreground: locationColor }, // for Events - {token: 'keyword.indexed', foreground: yellowColor}, - {token: 'keyword.anonymous', foreground: yellowColor}, + { token: 'keyword.indexed', foreground: yellowColor }, + { token: 'keyword.anonymous', foreground: yellowColor }, // for functions - {token: 'keyword.external', foreground: successColor}, - {token: 'keyword.internal', foreground: successColor}, - {token: 'keyword.private', foreground: successColor}, - {token: 'keyword.public', foreground: successColor}, - {token: 'keyword.view', foreground: successColor}, - {token: 'keyword.pure', foreground: successColor}, - {token: 'keyword.payable', foreground: successColor}, - {token: 'keyword.nonpayable', foreground: successColor}, + { token: 'keyword.external', foreground: successColor }, + { token: 'keyword.internal', foreground: successColor }, + { token: 'keyword.private', foreground: successColor }, + { token: 'keyword.public', foreground: successColor }, + { token: 'keyword.view', foreground: successColor }, + { token: 'keyword.pure', foreground: successColor }, + { token: 'keyword.payable', foreground: successColor }, + { token: 'keyword.nonpayable', foreground: successColor }, // Errors - {token: 'keyword.Error', foreground: dangerColor}, - {token: 'keyword.Panic', foreground: dangerColor}, + { token: 'keyword.Error', foreground: dangerColor }, + { token: 'keyword.Panic', foreground: dangerColor }, // special functions - {token: 'keyword.fallback', foreground: pinkColor}, - {token: 'keyword.receive', foreground: pinkColor}, - {token: 'keyword.constructor', foreground: pinkColor}, + { token: 'keyword.fallback', foreground: pinkColor }, + { token: 'keyword.receive', foreground: pinkColor }, + { token: 'keyword.constructor', foreground: pinkColor }, // identifiers - {token: 'keyword.identifier', foreground: warningColor}, - {token: 'keyword.for', foreground: warningColor}, - {token: 'keyword.break', foreground: warningColor}, - {token: 'keyword.continue', foreground: warningColor}, - {token: 'keyword.while', foreground: warningColor}, - {token: 'keyword.do', foreground: warningColor}, - {token: 'keyword.delete', foreground: warningColor}, + { token: 'keyword.identifier', foreground: warningColor }, + { token: 'keyword.for', foreground: warningColor }, + { token: 'keyword.break', foreground: warningColor }, + { token: 'keyword.continue', foreground: warningColor }, + { token: 'keyword.while', foreground: warningColor }, + { token: 'keyword.do', foreground: warningColor }, + { token: 'keyword.delete', foreground: warningColor }, - {token: 'keyword.if', foreground: yellowColor}, - {token: 'keyword.else', foreground: yellowColor}, + { token: 'keyword.if', foreground: yellowColor }, + { token: 'keyword.else', foreground: yellowColor }, - {token: 'keyword.throw', foreground: orangeColor}, - {token: 'keyword.catch', foreground: orangeColor}, - {token: 'keyword.try', foreground: orangeColor}, + { token: 'keyword.throw', foreground: orangeColor }, + { token: 'keyword.catch', foreground: orangeColor }, + { token: 'keyword.try', foreground: orangeColor }, // returns - {token: 'keyword.returns', foreground: greenColor}, - {token: 'keyword.return', foreground: greenColor} + { token: 'keyword.returns', foreground: greenColor }, + { token: 'keyword.return', foreground: greenColor }, ], colors: { // see https://code.visualstudio.com/api/references/theme-color for more settings @@ -294,8 +296,8 @@ export const EditorUI = (props: EditorUIProps) => { 'menu.background': textbackground, 'menu.selectionBackground': secondaryColor, 'menu.selectionForeground': textColor, - 'menu.selectionBorder': secondaryColor - } + 'menu.selectionBorder': secondaryColor, + }, }) monacoRef.current.editor.setTheme(themeName) } @@ -313,7 +315,7 @@ export const EditorUI = (props: EditorUIProps) => { const file = editorModelsState[props.currentFile] editorRef.current.setModel(file.model) editorRef.current.updateOptions({ - readOnly: editorModelsState[props.currentFile].readOnly + readOnly: editorModelsState[props.currentFile].readOnly, }) if (file.language === 'sol') { monacoRef.current.editor.setModelLanguage(file.model, 'remix-solidity') @@ -337,10 +339,10 @@ export const EditorUI = (props: EditorUIProps) => { options: { isWholeLine: false, glyphMarginHoverMessage: { - value: (decoration.from ? `from ${decoration.from}:\n` : '') + decoration.text + value: (decoration.from ? `from ${decoration.from}:\n` : '') + decoration.text, }, - glyphMarginClassName: `fal fa-exclamation-square text-${decoration.type === 'error' ? 'danger' : decoration.type === 'warning' ? 'warning' : 'info'}` - } + glyphMarginClassName: `fal fa-exclamation-square text-${decoration.type === 'error' ? 'danger' : decoration.type === 'warning' ? 'warning' : 'info'}`, + }, } } if (typeOfDecoration === 'markerPerFile') { @@ -363,8 +365,8 @@ export const EditorUI = (props: EditorUIProps) => { ), options: { isWholeLine, - inlineClassName: `${isWholeLine ? 'alert-info' : 'inline-class'} border-0 highlightLine${decoration.position.start.line + 1}` - } + inlineClassName: `${isWholeLine ? 'alert-info' : 'inline-class'} border-0 highlightLine${decoration.position.start.line + 1}`, + }, } } if (typeOfDecoration === 'lineTextPerFile') { @@ -380,11 +382,11 @@ export const EditorUI = (props: EditorUIProps) => { options: { after: { content: ` ${lineTextDecoration.content}`, - inlineClassName: `${lineTextDecoration.className}` + inlineClassName: `${lineTextDecoration.className}`, }, afterContentClassName: `${lineTextDecoration.afterContentClassName}`, - hoverMessage: lineTextDecoration.hoverMessage - } + hoverMessage: lineTextDecoration.hoverMessage, + }, } } if (typeOfDecoration === 'lineTextPerFile') { @@ -400,11 +402,11 @@ export const EditorUI = (props: EditorUIProps) => { options: { after: { content: ` ${lineTextDecoration.content}`, - inlineClassName: `${lineTextDecoration.className}` + inlineClassName: `${lineTextDecoration.className}`, }, afterContentClassName: `${lineTextDecoration.afterContentClassName}`, - hoverMessage: lineTextDecoration.hoverMessage - } + hoverMessage: lineTextDecoration.hoverMessage, + }, } } } @@ -414,7 +416,7 @@ export const EditorUI = (props: EditorUIProps) => { if (!model) return { currentDecorations: [], - registeredDecorations: [] + registeredDecorations: [], } const decorations = [] const newRegisteredDecorations = [] @@ -428,7 +430,7 @@ export const EditorUI = (props: EditorUIProps) => { } return { currentDecorations: model.deltaDecorations(currentDecorations, decorations), - registeredDecorations: newRegisteredDecorations + registeredDecorations: newRegisteredDecorations, } } @@ -436,7 +438,7 @@ export const EditorUI = (props: EditorUIProps) => { const model = editorModelsState[filePath]?.model if (!model) return { - currentDecorations: [] + currentDecorations: [], } const decorations = [] if (registeredDecorations) { @@ -447,17 +449,17 @@ export const EditorUI = (props: EditorUIProps) => { } } return { - currentDecorations: model.deltaDecorations(currentDecorations, decorations) + currentDecorations: model.deltaDecorations(currentDecorations, decorations), } } const addDecoration = (decoration: sourceAnnotation | sourceMarker, filePath: string, typeOfDecoration: string) => { const model = editorModelsState[filePath]?.model - if (!model) return {currentDecorations: []} + if (!model) return { currentDecorations: [] } const monacoDecoration = convertToMonacoDecoration(decoration, typeOfDecoration) return { currentDecorations: model.deltaDecorations([], [monacoDecoration]), - registeredDecorations: [{value: decoration, type: typeOfDecoration}] + registeredDecorations: [{ value: decoration, type: typeOfDecoration }], } } @@ -478,7 +480,7 @@ export const EditorUI = (props: EditorUIProps) => { const errorServerityMap = { error: MarkerSeverity.Error, warning: MarkerSeverity.Warning, - info: MarkerSeverity.Info + info: MarkerSeverity.Info, } if (model) { const markerData: monacoTypes.editor.IMarkerData = { @@ -487,7 +489,7 @@ export const EditorUI = (props: EditorUIProps) => { startColumn: (error.position.start && error.position.start.column) || 0, endLineNumber: (error.position.end && error.position.end.line) || 0, endColumn: (error.position.end && error.position.end.column) || 0, - message: error.message + message: error.message, } if (!allMarkersPerfile[filePath]) { allMarkersPerfile[filePath] = [] @@ -503,7 +505,7 @@ export const EditorUI = (props: EditorUIProps) => { } } - props.editorAPI.clearErrorMarkers = async (sources: string[] | {[fileName: string]: any}, from: string) => { + props.editorAPI.clearErrorMarkers = async (sources: string[] | { [fileName: string]: any }, from: string) => { if (sources) { for (const source of Array.isArray(sources) ? sources : Object.keys(sources)) { const filePath = source @@ -572,9 +574,9 @@ export const EditorUI = (props: EditorUIProps) => { range: new monacoRef.current.Range(position.lineNumber, 1, position.lineNumber, 1), options: { isWholeLine: false, - glyphMarginClassName: 'fas fa-circle text-info' - } - } + glyphMarginClassName: 'fas fa-circle text-info', + }, + }, ] ) prevState[currentFile][position.lineNumber] = decorationIds[0] @@ -626,7 +628,7 @@ export const EditorUI = (props: EditorUIProps) => { - ) + ), } props.plugin.call('notification', 'alert', modalContent) pasteCodeRef.current = true @@ -649,7 +651,7 @@ export const EditorUI = (props: EditorUIProps) => { contextMenuGroupId: 'zooming', // create a new grouping keybindings: [ // eslint-disable-next-line no-bitwise - monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.Equal + monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.Equal, ], run: () => { editor.updateOptions({fontSize: editor.getOption(51) + 1}) @@ -662,7 +664,7 @@ export const EditorUI = (props: EditorUIProps) => { contextMenuGroupId: 'zooming', // create a new grouping keybindings: [ // eslint-disable-next-line no-bitwise - monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.Minus + monacoRef.current.KeyMod.CtrlCmd | monacoRef.current.KeyCode.Minus, ], run: () => { editor.updateOptions({fontSize: editor.getOption(51) - 1}) @@ -675,12 +677,48 @@ export const EditorUI = (props: EditorUIProps) => { contextMenuGroupId: 'formatting', // create a new grouping keybindings: [ // eslint-disable-next-line no-bitwise - monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyF + monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyF, ], run: async () => { const file = await props.plugin.call('fileManager', 'getCurrentFile') await props.plugin.call('codeFormatter', 'format', file) - } + }, + } + + let gptGenerateDocumentationAction + const executeGptGenerateDocumentationAction = { + id: 'generateDocumentation', + label: 'Generate documentation for this function', + contextMenuOrder: 0, // choose the order + contextMenuGroupId: 'gtp', // create a new grouping + keybindings: [], + run: async () => { + const file = await props.plugin.call('fileManager', 'getCurrentFile') + const content = await props.plugin.call('fileManager', 'readFile', file) + const message = ` + solidity code: ${content} + Generate the documentation for the function ${currentFunction.current} using the Doxygen style syntax + ` + await props.plugin.call('openaigpt', 'message', message) + }, + } + + let gptExplainFunctionAction + const executegptExplainFunctionAction = { + id: 'explainFunction', + label: 'Explain this function', + contextMenuOrder: 1, // choose the order + contextMenuGroupId: 'gtp', // create a new grouping + keybindings: [], + run: async () => { + const file = await props.plugin.call('fileManager', 'getCurrentFile') + const content = await props.plugin.call('fileManager', 'readFile', file) + const message = ` + solidity code: ${content} + Explain the function ${currentFunction.current} + ` + await props.plugin.call('openaigpt', 'message', message) + }, } const freeFunctionCondition = editor.createContextKey('freeFunctionCondition', false) @@ -693,10 +731,10 @@ export const EditorUI = (props: EditorUIProps) => { precondition: 'freeFunctionCondition', keybindings: [ // eslint-disable-next-line no-bitwise - monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyR + monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyR, ], run: async () => { - const {nodesAtPosition} = await retrieveNodesAtPosition(props.editorAPI, props.plugin) + const { nodesAtPosition } = await retrieveNodesAtPosition(props.editorAPI, props.plugin) // find the contract and get the nodes of the contract and the base contracts and imports if (nodesAtPosition && isArray(nodesAtPosition) && nodesAtPosition.length) { const freeFunctionNode = nodesAtPosition.find((node) => node.kind === 'freeFunction') @@ -709,12 +747,14 @@ export const EditorUI = (props: EditorUIProps) => { } else { props.plugin.call('notification', 'toast', 'Please go to Remix settings and activate the code editor features or wait that the current editor context is loaded.') } - } + }, } editor.addAction(formatAction) editor.addAction(zoomOutAction) editor.addAction(zoominAction) freeFunctionAction = editor.addAction(executeFreeFunctionAction) + gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction) + gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction) // we have to add the command because the menu action isn't always available (see onContextMenuHandlerForFreeFunction) editor.addCommand(monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyR, () => executeFreeFunctionAction.run()) @@ -726,17 +766,34 @@ export const EditorUI = (props: EditorUIProps) => { freeFunctionAction.dispose() freeFunctionAction = null } + if (gptGenerateDocumentationAction) { + gptGenerateDocumentationAction.dispose() + gptGenerateDocumentationAction = null + } + if (gptExplainFunctionAction) { + gptExplainFunctionAction.dispose() + gptExplainFunctionAction = null + } + const file = await props.plugin.call('fileManager', 'getCurrentFile') if (!file.endsWith('.sol')) { freeFunctionCondition.set(false) return } - const {nodesAtPosition} = await retrieveNodesAtPosition(props.editorAPI, props.plugin) + const { nodesAtPosition } = await retrieveNodesAtPosition(props.editorAPI, props.plugin) const freeFunctionNode = nodesAtPosition.find((node) => node.kind === 'freeFunction') if (freeFunctionNode) { executeFreeFunctionAction.label = `Run the free function "${freeFunctionNode.name}" in the Remix VM` freeFunctionAction = editor.addAction(executeFreeFunctionAction) } + const functionImpl = nodesAtPosition.find((node) => node.kind === 'function') + if (functionImpl) { + currentFunction.current = functionImpl.name + executeGptGenerateDocumentationAction.label = `Generate documentation for the function "${functionImpl.name}"` + gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction) + executegptExplainFunctionAction.label = `Explain the function "${functionImpl.name}"` + gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction) + } freeFunctionCondition.set(!!freeFunctionNode) } contextmenu._onContextMenu = (...args) => { @@ -757,7 +814,7 @@ export const EditorUI = (props: EditorUIProps) => { editor.revealRange(input.options.selection) editor.setPosition({ column: input.options.selection.startColumn, - lineNumber: input.options.selection.startLineNumber + lineNumber: input.options.selection.startLineNumber, }) } } catch (e) { @@ -775,11 +832,11 @@ export const EditorUI = (props: EditorUIProps) => { function handleEditorWillMount(monaco) { monacoRef.current = monaco // Register a new language - monacoRef.current.languages.register({id: 'remix-solidity'}) - monacoRef.current.languages.register({id: 'remix-cairo'}) - monacoRef.current.languages.register({id: 'remix-zokrates'}) - monacoRef.current.languages.register({id: 'remix-move'}) - monacoRef.current.languages.register({id: 'remix-circom'}) + monacoRef.current.languages.register({ id: 'remix-solidity' }) + monacoRef.current.languages.register({ id: 'remix-cairo' }) + monacoRef.current.languages.register({ id: 'remix-zokrates' }) + monacoRef.current.languages.register({ id: 'remix-move' }) + monacoRef.current.languages.register({ id: 'remix-circom' }) // Register a tokens provider for the language monacoRef.current.languages.setMonarchTokensProvider('remix-solidity', solidityTokensProvider as any) @@ -817,7 +874,7 @@ export const EditorUI = (props: EditorUIProps) => { beforeMount={handleEditorWillMount} options={{ glyphMargin: true, - readOnly: (!editorRef.current || !props.currentFile) && editorModelsState[props.currentFile]?.readOnly + readOnly: (!editorRef.current || !props.currentFile) && editorModelsState[props.currentFile]?.readOnly, }} defaultValue={defaultEditorValue} /> diff --git a/libs/remix-ui/renderer/src/lib/renderer.tsx b/libs/remix-ui/renderer/src/lib/renderer.tsx index 41d860bc18..ac491b28d8 100644 --- a/libs/remix-ui/renderer/src/lib/renderer.tsx +++ b/libs/remix-ui/renderer/src/lib/renderer.tsx @@ -67,6 +67,21 @@ export const Renderer = ({message, opt = {}, plugin}: RendererProps) => { } } + const askGtp = async () => { + try { + const content = await plugin.call('fileManager', 'readFile', editorOptions.errFile) + const message = ` + solidity code: ${content} + error message: ${messageText} + explain why the error occurred and how to fix it. + ` + await plugin.call('openaigpt', 'message', message) + } catch (err) { + console.error('unable to askGtp') + console.error(err) + } + } + return ( <> {messageText && !close && ( @@ -82,6 +97,7 @@ export const Renderer = ({message, opt = {}, plugin}: RendererProps) => { + { askGtp() }}>ASK GPT )} diff --git a/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts b/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts index 61ba9d85fc..c451abfbb8 100644 --- a/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts +++ b/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts @@ -1,4 +1,4 @@ -import { CLEAR_CONSOLE, CMD_HISTORY, EMPTY_BLOCK, ERROR, HTML, INFO, KNOWN_TRANSACTION, LISTEN_ON_NETWORK, LOG, NEW_TRANSACTION, SCRIPT, UNKNOWN_TRANSACTION, WARN } from '../types/terminalTypes' +import { CLEAR_CONSOLE, CMD_HISTORY, EMPTY_BLOCK, ERROR, HTML, INFO, KNOWN_TRANSACTION, LISTEN_ON_NETWORK, LOG, TYPEWRITERLOG, TYPEWRITERWARNING, TYPEWRITERSUCCESS, NEW_TRANSACTION, SCRIPT, UNKNOWN_TRANSACTION, WARN } from '../types/terminalTypes' export const initialState = { journalBlocks: [ @@ -151,6 +151,21 @@ export const registerScriptRunnerReducer = (state, action) => { ...state, journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, style: 'text-log', provider: action.payload.provider }) } + case TYPEWRITERLOG: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, typewriter: true, style: 'text-log', provider: action.payload.provider }) + } + case TYPEWRITERWARNING: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, typewriter: true, style: 'text-warning', provider: action.payload.provider }) + } + case TYPEWRITERSUCCESS: + return { + ...state, + journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, typewriter: true, style: 'text-success', provider: action.payload.provider }) + } case INFO: return { ...state, diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx index db3c012714..67a439fd29 100644 --- a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -84,6 +84,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { // terminal inputRef const inputEl = useRef(null) const messagesEndRef = useRef(null) + const typeWriterIndexes = useRef([]) // terminal dragable const panelRef = useRef(null) @@ -389,6 +390,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { const handleClearConsole = () => { setClearConsole(true) + typeWriterIndexes.current = [] dispatch({ type: 'clearconsole', payload: [] }) inputEl.current.focus() } @@ -723,21 +725,36 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { ) } else { - return ( -
- {msg ? msg.toString() : null} -
- ) + // typeWriterIndexes: we don't want to rerender using typewriter when the react component updates + if (x.typewriter && !typeWriterIndexes.current.includes(index)) { + typeWriterIndexes.current.push(index) + return ( +
{ + typewrite(element, msg ? msg.toString() : null) + }} className={x.style}>
+ ) + } else { + return ( +
{msg ? msg.toString() : null}
+ ) + } } }) } else { - if (typeof x.message !== 'function') { + // typeWriterIndexes: we don't want to rerender using typewriter when the react component updates + if (x.typewriter && !typeWriterIndexes.current.includes(index)) { + typeWriterIndexes.current.push(index) return ( -
- {' '} - {x.message} -
+
{ + typewrite(element, x.message) + }} className={x.style}>
) + } else { + if (typeof x.message !== 'function') { + return ( +
{x.message}
+ ) + } } } })} @@ -778,7 +795,18 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { ) } -function isHtml(value) { +const typewrite = (elementsRef, message) => { + (() => { + let count = 0 + const id = setInterval(() => { + count++ + elementsRef.innerText = message.substr(0, count) + if (message === count) clearInterval(id) + }, 5) + })() +} + +function isHtml (value) { if (!value.indexOf) return false return value.indexOf('