Merge pull request #4554 from ethereum/solcoder/explain_contract

Solcoder/explain contract
4686-explain-this-function-or-generate-docs-appears-when-the-function-is-not-highlighted
yann300 7 months ago committed by GitHub
commit c9513986f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  2. 8
      apps/remix-ide/src/app.js
  3. 97
      apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts
  4. 100
      apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts
  5. 90
      apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js
  6. 8
      apps/remix-ide/src/app/plugins/openaigpt.tsx
  7. 168
      apps/remix-ide/src/app/plugins/solcoderAI.tsx
  8. 2
      apps/remix-ide/src/app/tabs/locales/en/editor.json
  9. 7
      apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json
  10. 16
      apps/remix-ide/src/app/tabs/settings-tab.tsx
  11. 4
      apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css
  12. 4
      apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css
  13. 6
      apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css
  14. 4
      apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css
  15. 4
      apps/remix-ide/src/assets/css/themes/remix-black_undtds.css
  16. 5
      apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css
  17. 4
      apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css
  18. 4
      apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css
  19. 5
      apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css
  20. 5
      apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css
  21. 5
      apps/remix-ide/src/assets/css/themes/remix-unicorn.css
  22. 5
      apps/remix-ide/src/assets/css/themes/remix-violet.css
  23. 2
      apps/remix-ide/src/remixAppManager.js
  24. 1
      apps/remix-ide/src/remixEngine.js
  25. 6
      apps/remixdesktop/src/menus/view.ts
  26. 31
      libs/remix-ui/editor/src/lib/providers/completionTimer.ts
  27. 93
      libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts
  28. 30
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  29. 14
      libs/remix-ui/renderer/src/lib/renderer.tsx
  30. 74
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  31. 23
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.css
  32. 172
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  33. 7
      libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts
  34. 6
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  35. 3
      libs/remix-ui/terminal/src/lib/terminalWelcome.tsx
  36. 1
      libs/remix-ui/terminal/src/lib/types/terminalTypes.ts
  37. 2
      libs/remix-ui/terminal/src/lib/utils/wrapScript.ts
  38. 1
      package.json
  39. 26
      yarn.lock

@ -244,13 +244,14 @@ module.exports = {
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000)
.waitForElementContainsText('#solidityUnittestsOutput', 'tests/hhLogs_test.sol', 60000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/hhLogs_test.sol', 60000)
.assert.containsText('#journal > div:nth-child(3) > span', 'Before all:') .pause(2000)
.assert.containsText('#journal > div:nth-child(3) > span', 'Inside beforeAll') .assert.containsText('#journal > div:nth-child(4) > span', 'Before all:')
.assert.containsText('#journal > div:nth-child(4) > span', 'Check sender:') .assert.containsText('#journal > div:nth-child(4) > span', 'Inside beforeAll')
.assert.containsText('#journal > div:nth-child(4) > span', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') .assert.containsText('#journal > div:nth-child(5) > span', 'Check sender:')
.assert.containsText('#journal > div:nth-child(5) > span', 'Check int logs:') .assert.containsText('#journal > div:nth-child(5) > span', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4')
.assert.containsText('#journal > div:nth-child(5) > span', '10 20') .assert.containsText('#journal > div:nth-child(6) > span', 'Check int logs:')
.assert.containsText('#journal > div:nth-child(5) > span', 'Number is 25') .assert.containsText('#journal > div:nth-child(6) > span', '10 20')
.assert.containsText('#journal > div:nth-child(6) > span', 'Number is 25')
.openFile('tests/hhLogs_test.sol') .openFile('tests/hhLogs_test.sol')
.removeFile('tests/hhLogs_test.sol', 'workspace_new') .removeFile('tests/hhLogs_test.sol', 'workspace_new')
}, },

@ -59,6 +59,7 @@ import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin'
import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin' import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin'
import {OpenAIGpt} from './app/plugins/openaigpt' import {OpenAIGpt} from './app/plugins/openaigpt'
import {SolCoder} from './app/plugins/solcoderAI'
const isElectron = require('is-electron') const isElectron = require('is-electron')
@ -67,7 +68,6 @@ const remixLib = require('@remix-project/remix-lib')
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
import { SearchPlugin } from './app/tabs/search' import { SearchPlugin } from './app/tabs/search'
import { ElectronProvider } from './app/files/electronProvider' import { ElectronProvider } from './app/files/electronProvider'
import { CopilotSuggestion } from './app/plugins/copilot/suggestion-service/copilot-suggestion'
const Storage = remixLib.Storage const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider') const RemixDProvider = require('./app/files/remixDProvider')
@ -233,7 +233,7 @@ class AppComponent {
// ----------------- AI -------------------------------------- // ----------------- AI --------------------------------------
const openaigpt = new OpenAIGpt() const openaigpt = new OpenAIGpt()
const copilotSuggestion = new CopilotSuggestion() const solcoder = new SolCoder()
// ----------------- import content service ------------------------ // ----------------- import content service ------------------------
const contentImport = new CompilerImports() const contentImport = new CompilerImports()
@ -362,7 +362,7 @@ class AppComponent {
solidityScript, solidityScript,
templates, templates,
openaigpt, openaigpt,
copilotSuggestion solcoder,
]) ])
//---- fs plugin //---- fs plugin
@ -517,6 +517,8 @@ class AppComponent {
} }
) )
await this.appManager.activatePlugin(['solidity-script', 'openaigpt']) await this.appManager.activatePlugin(['solidity-script', 'openaigpt'])
await this.appManager.activatePlugin(['solcoder'])
await this.appManager.activatePlugin(['filePanel']) await this.appManager.activatePlugin(['filePanel'])

@ -1,97 +0,0 @@
import {Plugin} from '@remixproject/engine'
import {SuggestionService, SuggestOptions} from './suggestion-service'
import axios, {AxiosResponse} from 'axios'
//@ts-ignore
const _paq = (window._paq = window._paq || []) //eslint-disable-line
const profile = {
name: 'copilot-suggestion',
displayName: 'copilot-suggestion',
description: 'Get Solidity suggestions in editor',
methods: ['suggest', 'init', 'uninstall', 'status', 'isActivate', 'useRemoteService', 'discardRemoteService'],
version: '0.1.0-alpha',
maintainedBy: "Remix"
}
export class CopilotSuggestion extends Plugin {
service: SuggestionService
remoteService: string
context: string
ready: boolean
constructor() {
super(profile)
this.service = new SuggestionService()
this.context = ''
this.service.events.on('progress', (data) => {
this.emit('loading', data)
})
this.service.events.on('done', (data) => {
})
this.service.events.on('ready', (data) => {
this.ready = true
})
}
useRemoteService(service: string) {
this.remoteService = service
}
discardRemoteService() {
this.remoteService = null
}
status () {
return this.ready
}
async isActivate () {
try {
return await this.call('settings', 'get', 'settings/copilot/suggest/activate')
} catch (e) {
console.error(e)
return false
}
}
async suggest(content: string) {
if (!await this.call('settings', 'get', 'settings/copilot/suggest/activate')) return { output: [{ generated_text: ''}]}
const max_new_tokens = await this.call('settings', 'get', 'settings/copilot/suggest/max_new_tokens')
const temperature = await this.call('settings', 'get', 'settings/copilot/suggest/temperature')
const options: SuggestOptions = {
do_sample: false,
top_k: 0,
temperature: temperature || 0,
max_new_tokens: max_new_tokens || 0
}
if (this.remoteService) {
const {data} = await axios.post(this.remoteService, {context: content, max_new_words: options.max_new_tokens, temperature: options.temperature})
const parsedData = JSON.parse(data).trimStart()
return {output: [{generated_text: parsedData}]}
} else {
return this.service.suggest(this.context ? this.context + '\n\n' + content : content, options)
}
}
async loadModeContent() {
let importsContent = ''
const imports = await this.call('codeParser', 'getImports')
for (const imp of imports.modules) {
try {
importsContent += '\n\n' + (await this.call('contentImport', 'resolve', imp)).content
} catch (e) {
console.log(e)
}
}
return importsContent
}
async init() {
return this.service.init()
}
async uninstall() {
this.service.terminate()
}
}

@ -1,100 +0,0 @@
import EventEmitter from 'events'
export type SuggestOptions = { max_new_tokens: number, temperature: number, top_k: number, do_sample: boolean }
export class SuggestionService {
worker: Worker
// eslint-disable-next-line @typescript-eslint/ban-types
responses: { [key: number]: Function }
events: EventEmitter
current: number
constructor() {
this.worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
this.events = new EventEmitter()
this.responses = {}
this.current
}
terminate(): void {
this.worker.terminate()
this.worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
}
async init() {
const onMessageReceived = (e) => {
switch (e.data.status) {
case 'initiate':
this.events.emit(e.data.status, e.data)
// Model file start load: add a new progress item to the list.
break;
case 'progress':
this.events.emit(e.data.status, e.data)
// Model file progress: update one of the progress items.
break;
case 'done':
this.events.emit(e.data.status, e.data)
// Model file loaded: remove the progress item from the list.
break;
case 'ready':
this.events.emit(e.data.status, e.data)
// Pipeline ready: the worker is ready to accept messages.
break;
case 'update':
this.events.emit(e.data.status, e.data)
// Generation update: update the output text.
break;
case 'complete':
if (this.responses[e.data.id]) {
if (this.current === e.data.id) {
this.responses[e.data.id](null, e.data)
} else {
this.responses[e.data.id]('aborted')
}
delete this.responses[e.data.id]
this.current = null
}
// Generation complete: re-enable the "Generate" button
break;
}
};
// Attach the callback function as an event listener.
this.worker.addEventListener('message', onMessageReceived)
this.worker.postMessage({
cmd: 'init',
model: 'Pipper/finetuned_sol'
})
}
suggest (content: string, options: SuggestOptions) {
return new Promise((resolve, reject) => {
if (this.current) return reject(new Error('already running'))
const timespan = Date.now()
this.current = timespan
this.worker.postMessage({
id: timespan,
cmd: 'suggest',
text: content,
max_new_tokens: options.max_new_tokens,
temperature: options.temperature,
top_k: options.top_k,
do_sample: options.do_sample
})
this.responses[timespan] = (error, result) => {
if (error) return reject(error)
resolve(result)
}
})
}
}

@ -1,90 +0,0 @@
import { pipeline, env } from '@xenova/transformers';
env.allowLocalModels = true;
const instance = null
/**
* This class uses the Singleton pattern to ensure that only one instance of the pipeline is loaded.
*/
class CodeCompletionPipeline {
static task = 'text-generation';
static model = null
static instance = null;
static async getInstance(progress_callback = null) {
if (this.instance === null) {
this.instance = pipeline(this.task, this.model, { progress_callback });
}
return this.instance;
}
}
// Listen for messages from the main thread
self.addEventListener('message', async (event) => {
const {
id, model, text, max_new_tokens, cmd,
// Generation parameters
temperature,
top_k,
do_sample,
} = event.data;
if (cmd === 'init') {
// Retrieve the code-completion pipeline. When called for the first time,
// this will load the pipeline and save it for future use.
CodeCompletionPipeline.model = model
await CodeCompletionPipeline.getInstance(x => {
// We also add a progress callback to the pipeline so that we can
// track model loading.
self.postMessage(x);
});
return
}
if (!CodeCompletionPipeline.instance) {
// Send the output back to the main thread
self.postMessage({
id,
status: 'error',
message: 'model not yet loaded'
});
}
if (cmd === 'suggest') {
// Retrieve the code-completion pipeline. When called for the first time,
// this will load the pipeline and save it for future use.
let generator = await CodeCompletionPipeline.getInstance(x => {
// We also add a progress callback to the pipeline so that we can
// track model loading.
self.postMessage(x);
});
// Actually perform the code-completion
let output = await generator(text, {
max_new_tokens,
temperature,
top_k,
do_sample,
// Allows for partial output
callback_function: x => {
/*self.postMessage({
id,
status: 'update',
output: generator.tokenizer.decode(x[0].output_token_ids, { skip_special_tokens: true })
});
*/
}
});
// Send the output back to the main thread
self.postMessage({
id,
status: 'complete',
output: output,
});
}
});

@ -19,7 +19,7 @@ export class OpenAIGpt extends Plugin {
async message(prompt): Promise<CreateChatCompletionResponse> { async message(prompt): Promise<CreateChatCompletionResponse> {
this.call('layout', 'maximizeTerminal') this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', 'Waiting for GPT answer...') this.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'Waiting for GPT answer...'})
let result let result
try { try {
result = await ( result = await (
@ -38,11 +38,11 @@ export class OpenAIGpt extends Plugin {
} }
if (result && result.choices && result.choices.length) { if (result && result.choices && result.choices.length) {
this.call('terminal', 'log', { type: 'typewriterwarning', value: result.choices[0].message.content }) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.choices[0].message.content })
} else if (result.error) { } else if (result.error) {
this.call('terminal', 'log', { type: 'typewriterwarning', value: result.error }) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.error })
} else { } else {
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'No response...' }) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'No response...' })
} }
return result.data return result.data
} }

@ -0,0 +1,168 @@
import { Plugin } from '@remixproject/engine'
export type SuggestOptions = {
max_new_tokens: number,
temperature: number,
do_sample:boolean
top_k: number,
top_p:number,
stream_result:boolean
}
const _paq = (window._paq = window._paq || [])
const profile = {
name: 'solcoder',
displayName: 'solcoder',
description: 'solcoder',
methods: ['code_generation', 'code_completion', "solidity_answer", "code_explaining"],
events: [],
maintainedBy: 'Remix',
}
export class SolCoder extends Plugin {
api_url: string
completion_url: string
constructor() {
super(profile)
this.api_url = "https://solcoder.remixproject.org"
this.completion_url = "https://completion.remixproject.org"
}
async code_generation(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'Code Generation: Waiting for Solcoder answer...'})
let result
try {
result = await(
await fetch(this.api_url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({"data":[prompt, "code_completion", "", false,1000,0.9,0.92,50]}),
})
).json()
console.log(result)
if ("error" in result){
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.error })
return result
}
return result.data
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
return
}finally {
this.emit("aiInferingDone")
}
}
async solidity_answer(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'Waiting for Solcoder answer...'})
let result
try {
result = await(
await fetch(this.api_url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({"data":[prompt, "solidity_answer", false,1000,0.9,0.8,50]}),
})
).json()
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
return
}finally {
this.emit("aiInferingDone")
}
if (result) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.data[0]})
} else if (result.error) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "Error on request" })
}
}
async code_explaining(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'Explain Code: Waiting for Solcoder answer...'})
let result
try {
result = await(
await fetch(this.api_url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({"data":[prompt, "code_explaining", false,2000,0.9,0.8,50]}),
})
).json()
if (result) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.data[0]})
}
return result.data[0]
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
return
}finally {
this.emit("aiInferingDone")
}
}
async code_completion(prompt, options:SuggestOptions=null): Promise<any> {
this.emit("aiInfering")
let result
try {
result = await(
await fetch(this.completion_url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({"data": !options? [
prompt, // string in 'context_code' Textbox component
"code_completion",
"", // string in 'comment' Textbox component
false, // boolean in 'stream_result' Checkbox component
30, // number (numeric value between 0 and 2000) in 'max_new_tokens' Slider component
0.9, // number (numeric value between 0.01 and 1) in 'temperature' Slider component
0.90, // number (numeric value between 0 and 1) in 'top_p' Slider component
50, // number (numeric value between 1 and 200) in 'top_k' Slider component
] : [
prompt,
"code_completion",
"",
options.stream_result,
options.max_new_tokens,
options.temperature,
options.top_p,
options.top_k
]}),
})
).json()
if ("error" in result){
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.error })
return result
}
return result.data
} catch (e) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `Unable to get a response ${e.message}` })
return
} finally {
this.emit("aiInferingDone")
}
}
}

@ -23,8 +23,10 @@
"editor.generateDocumentation2": "Generate documentation for the function \"{name}\"", "editor.generateDocumentation2": "Generate documentation for the function \"{name}\"",
"editor.generateDocumentationByAI": "solidity code: {content}\n Generate the documentation for the function {currentFunction} using the Doxygen style syntax", "editor.generateDocumentationByAI": "solidity code: {content}\n Generate the documentation for the function {currentFunction} using the Doxygen style syntax",
"editor.explainFunction": "Explain this function", "editor.explainFunction": "Explain this function",
"editor.explainFunctionSol": "Explain this code",
"editor.explainFunction2": "Explain the function \"{name}\"", "editor.explainFunction2": "Explain the function \"{name}\"",
"editor.explainFunctionByAI": "solidity code: {content}\n Explain the function {currentFunction}", "editor.explainFunctionByAI": "solidity code: {content}\n Explain the function {currentFunction}",
"editor.explainFunctionByAISol": "solidity code: {content}\n Explain the function {currentFunction}",
"editor.executeFreeFunction": "Run a free function", "editor.executeFreeFunction": "Run a free function",
"editor.executeFreeFunction2": "Run the free function \"{name}\"", "editor.executeFreeFunction2": "Run the free function \"{name}\"",
"editor.toastText1": "This can only execute free function", "editor.toastText1": "This can only execute free function",

@ -1,7 +1,12 @@
{ {
"remixUiTabs.tooltipText1": "Run script (CTRL + SHIFT + S)", "remixUiTabs.tooltipText1": "Run script (CTRL + SHIFT + S)",
"remixUiTabs.tooltipText2": "Compile CTRL + S", "remixUiTabs.tooltipText2": "Compile CTRL + S",
"remixUiTabs.tooltipText3": "Select .sol, .vy or .yul file to compile or a .ts or .js file and run it", "remixUiTabs.tooltipText3": "Select .sol or .yul file to compile or a .ts or .js file and run it",
"remixUiTabs.tooltipText4": "Select .sol file to use AI tools [BETA]",
"remixUiTabs.tooltipText5": "Explain the contract(s) in current file [BETA]",
"remixUiTabs.tooltipText6": "Enable AI Copilot [BETA]",
"remixUiTabs.tooltipText7": "Disable AI Copilot [BETA]",
"remixUiTabs.tooltipText8": "AI Documentation [BETA]",
"remixUiTabs.zoomOut": "Zoom out", "remixUiTabs.zoomOut": "Zoom out",
"remixUiTabs.zoomIn": "Zoom in" "remixUiTabs.zoomIn": "Zoom in"
} }

@ -15,7 +15,7 @@ const _paq = (window._paq = window._paq || [])
const profile = { const profile = {
name: 'settings', name: 'settings',
displayName: 'Settings', displayName: 'Settings',
methods: ['get'], methods: ['get', 'updateCopilotChoice'],
events: [], events: [],
icon: 'assets/img/settings.webp', icon: 'assets/img/settings.webp',
description: 'Remix-IDE settings', description: 'Remix-IDE settings',
@ -36,6 +36,7 @@ module.exports = class SettingsTab extends ViewPlugin {
} }
element: HTMLDivElement element: HTMLDivElement
public useMatomoAnalytics: any public useMatomoAnalytics: any
public useCopilot: any
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor(config, editor) { constructor(config, editor) {
super(profile) super(profile)
@ -51,6 +52,7 @@ module.exports = class SettingsTab extends ViewPlugin {
this.element = document.createElement('div') this.element = document.createElement('div')
this.element.setAttribute('id', 'settingsTab') this.element.setAttribute('id', 'settingsTab')
this.useMatomoAnalytics = null this.useMatomoAnalytics = null
this.useCopilot = false
} }
setDispatch(dispatch: React.Dispatch<any>) { setDispatch(dispatch: React.Dispatch<any>) {
@ -58,6 +60,9 @@ module.exports = class SettingsTab extends ViewPlugin {
this.renderComponent() this.renderComponent()
} }
onActivation(): void {
}
render() { render() {
return ( return (
<div id="settingsTab"> <div id="settingsTab">
@ -74,6 +79,7 @@ module.exports = class SettingsTab extends ViewPlugin {
editor={state.editor} editor={state.editor}
_deps={state._deps} _deps={state._deps}
useMatomoAnalytics={state.useMatomoAnalytics} useMatomoAnalytics={state.useMatomoAnalytics}
useCopilot={state.useCopilot}
themeModule={state._deps.themeModule} themeModule={state._deps.themeModule}
localeModule={state._deps.localeModule} localeModule={state._deps.localeModule}
/> />
@ -88,6 +94,14 @@ module.exports = class SettingsTab extends ViewPlugin {
return this.config.get(key) return this.config.get(key)
} }
updateCopilotChoice(isChecked) {
this.config.set('settings/copilot/suggest/activate', isChecked)
this.useCopilot = isChecked
this.dispatch({
...this
})
}
updateMatomoAnalyticsChoice(isChecked) { updateMatomoAnalyticsChoice(isChecked) {
this.config.set('settings/matomo-analytics', isChecked) this.config.set('settings/matomo-analytics', isChecked)
this.useMatomoAnalytics = isChecked this.useMatomoAnalytics = isChecked

@ -10,6 +10,7 @@
* Copyright 2011-2020 Twitter, Inc. * Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root { */:root {
--ai: #da2de4;
--blue:#033c73; --blue:#033c73;
--indigo:#6610f2; --indigo:#6610f2;
--purple:#6f42c1; --purple:#6f42c1;
@ -8373,6 +8374,9 @@ a.text-danger:focus,a.text-danger:hover {
a.text-light:focus,a.text-light:hover { a.text-light:focus,a.text-light:hover {
color:#cbd3da!important color:#cbd3da!important
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color:#343a40!important color:#343a40!important
} }

@ -11,6 +11,7 @@
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/@import url(https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap); */@import url(https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap);
:root { :root {
--ai: #2de7f3;
--blue:#2a9fd6; --blue:#2a9fd6;
--indigo:#6610f2; --indigo:#6610f2;
--purple:#6f42c1; --purple:#6f42c1;
@ -8375,6 +8376,9 @@ a.text-danger:focus,a.text-danger:hover {
a.text-light:focus,a.text-light:hover { a.text-light:focus,a.text-light:hover {
color:#000!important color:#000!important
} }
.text-dark {
color: #babbcc !important;
}
.text-dark { .text-dark {
color:#adafae!important color:#adafae!important
} }

@ -10,7 +10,8 @@
* Copyright 2011-2020 Twitter, Inc. * Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/@import url(https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap);:root { */@import url(https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap);:root {
--blue:#2c3e50; --ai: #da2de4;
--blue:#2c3e50;
--indigo:#6610f2; --indigo:#6610f2;
--purple:#6f42c1; --purple:#6f42c1;
--pink:#e83e8c; --pink:#e83e8c;
@ -7004,6 +7005,9 @@ a.text-danger:focus,a.text-danger:hover {
a.text-light:focus,a.text-light:hover { a.text-light:focus,a.text-light:hover {
color:#c0cdd1!important color:#c0cdd1!important
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color:#7b8a8b!important color:#7b8a8b!important
} }

@ -11,6 +11,7 @@
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/@import url(https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap); */@import url(https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap);
:root { :root {
--ai: #da2de4;
--blue:#446e9b; --blue:#446e9b;
--indigo:#6610f2; --indigo:#6610f2;
--purple:#6f42c1; --purple:#6f42c1;
@ -8375,6 +8376,9 @@ a.text-danger:focus,a.text-danger:hover {
a.text-light:focus,a.text-light:hover { a.text-light:focus,a.text-light:hover {
color:#c8c8c8!important color:#c8c8c8!important
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color:#333!important color:#333!important
} }

@ -1,5 +1,6 @@
@import url('https://fonts.googleapis.com/css?family=Nunito+Sans:400,600&display=swap'); @import url('https://fonts.googleapis.com/css?family=Nunito+Sans:400,600&display=swap');
:root { :root {
--ai: #2de7f3;
--blue: #90c3f6; --blue: #90c3f6;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #9e77f6; --purple: #9e77f6;
@ -8590,6 +8591,9 @@ a.text-light:hover {
.text-dark { .text-dark {
color: #babbcc !important; color: #babbcc !important;
} }
.text-ai {
color: #2de7f3 !important;
}
a.text-dark:focus, a.text-dark:focus,
a.text-dark:hover { a.text-dark:hover {
color: #6f7087 !important; color: #6f7087 !important;

@ -1,4 +1,5 @@
:root { :root {
--ai: #da2de4;
--blue: #007bff; --blue: #007bff;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #6f42c1; --purple: #6f42c1;
@ -9364,7 +9365,9 @@ a.text-light:hover,
a.text-light:focus { a.text-light:focus {
color: #d9d9d9 !important; color: #d9d9d9 !important;
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color: #11556c !important; color: #11556c !important;
} }

@ -1,4 +1,5 @@
:root { :root {
--ai: #2de7f3;
--blue: #007aa6; --blue: #007aa6;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #9e77f6; --purple: #9e77f6;
@ -8570,6 +8571,9 @@ a.text-info:hover {
.text-warning { .text-warning {
color: #c97539 !important; color: #c97539 !important;
} }
.text-ai {
color: #2de7f3 !important;
}
a.text-warning:focus, a.text-warning:focus,
a.text-warning:hover { a.text-warning:hover {
color: #8f5227 !important; color: #8f5227 !important;

@ -1,6 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Saira:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Saira:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400&display=swap');
:root { :root {
--ai: #2de7f3;
--blue: #2cc1f7; --blue: #2cc1f7;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #6f42c1; --purple: #6f42c1;
@ -8604,6 +8605,9 @@ a.text-light:hover {
.text-dark { .text-dark {
color: #babbcc !important; color: #babbcc !important;
} }
.text-ai {
color: #2de7f3 !important;
}
a.text-dark:focus, a.text-dark:focus,
a.text-dark:hover { a.text-dark:hover {
color: #6f7087 !important; color: #6f7087 !important;

@ -1,4 +1,5 @@
:root { :root {
--ai: #da2de4;
--blue: #007bff; --blue: #007bff;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #7c47b9; --purple: #7c47b9;
@ -9360,7 +9361,9 @@ a.text-light:hover,
a.text-light:focus { a.text-light:focus {
color: #d9d9d9 !important; color: #d9d9d9 !important;
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color: #747B90 !important; color: #747B90 !important;
} }

@ -1,4 +1,5 @@
:root { :root {
--ai: #da2de4;
--blue: #007bff; --blue: #007bff;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #6f42c1; --purple: #6f42c1;
@ -9366,7 +9367,9 @@ a.text-light:hover,
a.text-light:focus { a.text-light:focus {
color: #d9d9d9 !important; color: #d9d9d9 !important;
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color: #11556c !important; color: #11556c !important;
} }

@ -1,4 +1,5 @@
:root { :root {
--ai: #da2de4;
--blue: #007bff; --blue: #007bff;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #7c47b9; --purple: #7c47b9;
@ -9360,7 +9361,9 @@ a.text-light:hover,
a.text-light:focus { a.text-light:focus {
color: #d9d9d9 !important; color: #d9d9d9 !important;
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color: #747B90 !important; color: #747B90 !important;
} }

@ -1,4 +1,5 @@
:root { :root {
--ai: #da2de4;
--blue: #007bff; --blue: #007bff;
--indigo: #6610f2; --indigo: #6610f2;
--purple: #7c47b9; --purple: #7c47b9;
@ -9356,7 +9357,9 @@ a.text-light:hover,
a.text-light:focus { a.text-light:focus {
color: #d9d9d9 !important; color: #d9d9d9 !important;
} }
.text-ai {
color: #da2de4 !important;
}
.text-dark { .text-dark {
color: #747B90 !important; color: #747B90 !important;
} }

@ -78,10 +78,10 @@ let requiredModules = [ // services + layout views + system views
'contractflattener', 'contractflattener',
'solidity-script', 'solidity-script',
'openaigpt', 'openaigpt',
'solcoder',
'home', 'home',
'doc-viewer', 'doc-viewer',
'doc-gen', 'doc-gen',
'copilot-suggestion',
'remix-templates' 'remix-templates'
] ]

@ -27,6 +27,7 @@ export class RemixEngine extends Engine {
if (name === 'filePanel') return { queueTimeout: 60000 * 20 } if (name === 'filePanel') return { queueTimeout: 60000 * 20 }
if (name === 'fileManager') return { queueTimeout: 60000 * 20 } if (name === 'fileManager') return { queueTimeout: 60000 * 20 }
if (name === 'openaigpt') return { queueTimeout: 60000 * 2 } if (name === 'openaigpt') return { queueTimeout: 60000 * 2 }
if (name === 'solcoder') return { queueTimeout: 60000 * 2 }
if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 } if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 }
return { queueTimeout: 10000 } return { queueTimeout: 10000 }
} }

@ -79,9 +79,9 @@ export default (
accelerator: 'CmdOrCtrl+0', accelerator: 'CmdOrCtrl+0',
click: function(item, focusedWindow) { click: function(item, focusedWindow) {
if (focusedWindow) if (focusedWindow)
{ {
focusedWindow.webContents.setZoomFactor(1) focusedWindow.webContents.setZoomFactor(1)
} }
} }
}, },

@ -0,0 +1,31 @@
export class CompletionTimer {
private duration: number;
private timerId: NodeJS.Timeout | null = null;
private callback: () => void;
constructor(duration: number, callback: () => void) {
this.duration = duration;
this.callback = callback;
}
start() {
if (this.timerId) {
console.error("Timer is already running.");
return;
}
this.timerId = setTimeout(() => {
this.callback();
this.timerId = null;
}, this.duration);
}
stop() {
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
} else {
console.error("Timer is not running.");
}
}
}

@ -1,6 +1,11 @@
/* eslint-disable no-control-regex */ /* eslint-disable no-control-regex */
import { EditorUIProps, monacoTypes } from '@remix-ui/editor'; import { EditorUIProps, monacoTypes } from '@remix-ui/editor';
import { CompletionTimer } from './completionTimer';
import axios, {AxiosResponse} from 'axios' import axios, {AxiosResponse} from 'axios'
import { slice } from 'lodash';
const _paq = (window._paq = window._paq || [])
const controller = new AbortController(); const controller = new AbortController();
const { signal } = controller; const { signal } = controller;
const result: string = '' const result: string = ''
@ -8,11 +13,14 @@ const result: string = ''
export class RemixInLineCompletionProvider implements monacoTypes.languages.InlineCompletionsProvider { export class RemixInLineCompletionProvider implements monacoTypes.languages.InlineCompletionsProvider {
props: EditorUIProps props: EditorUIProps
monaco: any monaco: any
completionEnabled: boolean
constructor(props: any, monaco: any) { constructor(props: any, monaco: any) {
this.props = props this.props = props
this.monaco = monaco this.monaco = monaco
this.completionEnabled = true
} }
async provideInlineCompletions(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.InlineCompletionContext, token: monacoTypes.CancellationToken): Promise<monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>> { async provideInlineCompletions(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.InlineCompletionContext, token: monacoTypes.CancellationToken): Promise<monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>> {
if (context.selectedSuggestionInfo) { if (context.selectedSuggestionInfo) {
return; return;
@ -25,12 +33,14 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
endColumn: position.column, endColumn: position.column,
}); });
if (!word.endsWith(' ') && !word.endsWith('\n') && !word.endsWith(';') && !word.endsWith('.')) { if (!word.endsWith(' ') &&
!word.endsWith('.') &&
!word.endsWith('(')) {
return; return;
} }
try { try {
const isActivate = await this.props.plugin.call('copilot-suggestion', 'isActivate') const isActivate = await await this.props.plugin.call('settings', 'get', 'settings/copilot/suggest/activate')
if (!isActivate) return if (!isActivate) return
} catch (err) { } catch (err) {
return; return;
@ -41,9 +51,13 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
if (split.length < 2) return if (split.length < 2) return
const ask = split[split.length - 2].trimStart() const ask = split[split.length - 2].trimStart()
if (split[split.length - 1].trim() === '' && ask.startsWith('///')) { if (split[split.length - 1].trim() === '' && ask.startsWith('///')) {
// use the code generation model // use the code generation model, only take max 1000 word as context
const {data} = await axios.post('https://gpt-chat.remixproject.org/infer', {comment: ask.replace('///', '')}) this.props.plugin.call('terminal', 'log', {type: 'aitypewriterwarning', value: 'Solcoder - generating code for following comment: ' + ask.replace('///', '')})
const parsedData = JSON.parse(data).trimStart()
const data = await this.props.plugin.call('solcoder', 'code_generation', word)
_paq.push(['trackEvent', 'ai', 'solcoder', 'code_generation'])
const parsedData = data[0].trimStart() //JSON.parse(data).trimStart()
const item: monacoTypes.languages.InlineCompletion = { const item: monacoTypes.languages.InlineCompletion = {
insertText: parsedData insertText: parsedData
}; };
@ -54,41 +68,72 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return
} }
if (word.split('\n').at(-1).trimStart().startsWith('//') ||
word.split('\n').at(-1).trimStart().startsWith('/*') ||
word.split('\n').at(-1).trimStart().startsWith('*') ||
word.split('\n').at(-1).trimStart().startsWith('*/') ||
word.split('\n').at(-1).endsWith(';')
){
return; // do not do completion on single and multiline comment
}
// abort if there is a signal // abort if there is a signal
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return return
} }
// abort if the completion is not enabled
if (!this.completionEnabled) {
return
}
let result let result
try { try {
result = await this.props.plugin.call('copilot-suggestion', 'suggest', word) const output = await this.props.plugin.call('solcoder', 'code_completion', word)
_paq.push(['trackEvent', 'ai', 'solcoder', 'code_completion'])
const generatedText = output[0]
let clean = generatedText
if (generatedText.indexOf('@custom:dev-run-script./') !== -1) {
clean = generatedText.replace('@custom:dev-run-script', '@custom:dev-run-script ')
}
clean = clean.replace(word, '').trimStart()
clean = this.process_completion(clean)
const item: monacoTypes.languages.InlineCompletion = {
insertText: clean
};
// handle the completion timer by locking suggestions request for 2 seconds
this.completionEnabled = false
const handleCompletionTimer = new CompletionTimer(2000, () => { this.completionEnabled = true });
handleCompletionTimer.start()
return {
items: [item],
enableForwardStability: true
}
} catch (err) { } catch (err) {
return return
} }
}
const generatedText = (result as any).output[0].generated_text as string process_completion(data: any) {
// the generated text remove a space from the context... let clean = data.split('\n')[0].startsWith('\n') ? [data.split('\n')[0], data.split('\n')[1]].join('\n'): data.split('\n')[0]
let clean = generatedText
if (generatedText.indexOf('@custom:dev-run-script./') !== -1) {
clean = generatedText.replace('@custom:dev-run-script', '@custom:dev-run-script ')
}
clean = clean.replace(word, '')
const item: monacoTypes.languages.InlineCompletion = {
insertText: clean
};
// abort if there is a signal // if clean starts with a comment, remove it
if (token.isCancellationRequested) { if (clean.startsWith('//') || clean.startsWith('/*') || clean.startsWith('*') || clean.startsWith('*/')){
return return ""
} }
return { // remove comment inline
items: [item], clean = clean.split('//')[0].trimEnd()
enableForwardStability: true return clean
}
} }
handleItemDidShow?(completions: monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>, item: monacoTypes.languages.InlineCompletion, updatedInsertText: string): void { handleItemDidShow?(completions: monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>, item: monacoTypes.languages.InlineCompletion, updatedInsertText: string): void {
} }

@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from 'react-intl'
import { isArray } from 'lodash' import { isArray } from 'lodash'
import Editor, { loader, Monaco } from '@monaco-editor/react' import Editor, { loader, Monaco } from '@monaco-editor/react'
import { AlertModal } from '@remix-ui/app' import { AlertModal } from '@remix-ui/app'
import { QueryParams } from '@remix-project/remix-lib' import { ConsoleLogs, QueryParams } from '@remix-project/remix-lib'
import { reducerActions, reducerListener, initialState } from './actions/editor' import { reducerActions, reducerListener, initialState } from './actions/editor'
import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solidity' import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solidity'
import { cairoTokensProvider, cairoLanguageConfig } from './syntaxes/cairo' import { cairoTokensProvider, cairoLanguageConfig } from './syntaxes/cairo'
@ -741,6 +741,24 @@ export const EditorUI = (props: EditorUIProps) => {
}, },
} }
let solgptExplainFunctionAction
const executeSolgptExplainFunctionAction = {
id: 'solExplainFunction',
label: intl.formatMessage({id: 'editor.explainFunctionSol'}),
contextMenuOrder: 1, // choose the order
contextMenuGroupId: 'sol-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 selectedCode = editor.getModel().getValueInRange(editor.getSelection())
await props.plugin.call('solcoder', 'code_explaining', selectedCode)
_paq.push(['trackEvent', 'ai', 'solcoder', 'explainFunction'])
},
}
const freeFunctionCondition = editor.createContextKey('freeFunctionCondition', false) const freeFunctionCondition = editor.createContextKey('freeFunctionCondition', false)
let freeFunctionAction let freeFunctionAction
const executeFreeFunctionAction = { const executeFreeFunctionAction = {
@ -775,6 +793,7 @@ export const EditorUI = (props: EditorUIProps) => {
freeFunctionAction = editor.addAction(executeFreeFunctionAction) freeFunctionAction = editor.addAction(executeFreeFunctionAction)
gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction) gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction)
gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction) gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction)
solgptExplainFunctionAction = editor.addAction(executeSolgptExplainFunctionAction)
// we have to add the command because the menu action isn't always available (see onContextMenuHandlerForFreeFunction) // 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()) editor.addCommand(monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyR, () => executeFreeFunctionAction.run())
@ -794,6 +813,10 @@ export const EditorUI = (props: EditorUIProps) => {
gptExplainFunctionAction.dispose() gptExplainFunctionAction.dispose()
gptExplainFunctionAction = null gptExplainFunctionAction = null
} }
if (solgptExplainFunctionAction) {
solgptExplainFunctionAction.dispose()
solgptExplainFunctionAction = null
}
const file = await props.plugin.call('fileManager', 'getCurrentFile') const file = await props.plugin.call('fileManager', 'getCurrentFile')
if (!file.endsWith('.sol')) { if (!file.endsWith('.sol')) {
@ -813,6 +836,11 @@ export const EditorUI = (props: EditorUIProps) => {
gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction) gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction)
executegptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunction2'}, {name: functionImpl.name}) executegptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunction2'}, {name: functionImpl.name})
gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction) gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction)
executeSolgptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunctionSol'})
solgptExplainFunctionAction = editor.addAction(executeSolgptExplainFunctionAction)
}else{
executeSolgptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunctionSol'})
solgptExplainFunctionAction = editor.addAction(executeSolgptExplainFunctionAction)
} }
freeFunctionCondition.set(!!freeFunctionNode) freeFunctionCondition.set(!!freeFunctionNode)
} }

@ -101,7 +101,19 @@ export const Renderer = ({message, opt = {}, plugin}: RendererProps) => {
<span className="ml-3 pt-1 py-1" > <span className="ml-3 pt-1 py-1" >
<CopyToClipboard content={messageText} className={` p-0 m-0 far fa-copy ${classList}`} direction={'top'} /> <CopyToClipboard content={messageText} className={` p-0 m-0 far fa-copy ${classList}`} direction={'top'} />
</span> </span>
<span className="border border-success text-success btn-sm" onClick={() => { askGtp() }}>ASK GPT</span> <span
className="position-relative text-ai text-sm pl-0 pr-2"
style={{fontSize: "x-small", alignSelf: "end"}}
>
</span>
<span
className="button border text-ai btn-sm"
onClick={() => { askGtp() }}
style={{borderColor: "var(--ai)"}}
>
ASK GPT
</span>
</div> </div>
</div> </div>
)} )}

@ -38,6 +38,7 @@ export interface RemixUiSettingsProps {
editor: any editor: any
_deps: any _deps: any
useMatomoAnalytics: boolean useMatomoAnalytics: boolean
useCopilot: boolean
themeModule: ThemeModule themeModule: ThemeModule
localeModule: LocaleModule localeModule: LocaleModule
} }
@ -134,49 +135,26 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
textWrapEventAction(props.config, props.editor, event.target.checked, dispatch) textWrapEventAction(props.config, props.editor, event.target.checked, dispatch)
} }
const onchangeCopilotActivate = async (event) => { const onchangeCopilotActivate = () => {
if (!event.target.checked) { if (!props.useCopilot) {
copilotActivate(props.config, event.target.checked, dispatch) copilotActivate(props.config, props.useCopilot, dispatch)
props.plugin.call('copilot-suggestion', 'uninstall') props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot deactivated!` })
return return
} }
const message = <div>Please wait while the copilot is downloaded. <span ref={copilotDownload}>0</span>/100 .</div>
props.plugin.on('copilot-suggestion', 'loading', (data) => { const startCopilot = async () => {
if (!copilotDownload.current) return copilotActivate(props.config, true, dispatch)
const loaded = ((data.loaded / data.total) * 100).toString() props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot activated!` })
const dot = loaded.match(/(.*)\./g)
copilotDownload.current.innerText = dot ? dot[0].replace('.', '') : loaded
})
const modalActivate: AppModal = {
id: 'loadcopilotActivate',
title: 'Download Solidity copilot',
modalType: ModalTypes.default,
okLabel: 'Close',
message,
okFn: async() => {
props.plugin.off('copilot-suggestion', 'loading')
if (await props.plugin.call('copilot-suggestion', 'status')) {
copilotActivate(props.config, true, dispatch)
} else {
props.plugin.call('copilot-suggestion', 'uninstall')
copilotActivate(props.config, false, dispatch)
}
},
hideFn: async () => {
props.plugin.off('copilot-suggestion', 'loading')
if (await props.plugin.call('copilot-suggestion', 'status')) {
copilotActivate(props.config, true, dispatch)
} else {
props.plugin.call('copilot-suggestion', 'uninstall')
copilotActivate(props.config, false, dispatch)
}
}
} }
props.plugin.call('copilot-suggestion', 'init')
props.plugin.call('notification', 'modal', modalActivate)
startCopilot()
} }
useEffect(() => {
if (props.useCopilot !== null) copilotActivate(props.config, props.useCopilot, dispatch)
onchangeCopilotActivate()
}, [props.useCopilot])
const onchangeCopilotMaxNewToken = (event) => { const onchangeCopilotMaxNewToken = (event) => {
copilotMaxNewToken(props.config, parseInt(event.target.value), dispatch) copilotMaxNewToken(props.config, parseInt(event.target.value), dispatch)
} }
@ -460,16 +438,15 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const isCopilotActivated = props.config.get('settings/copilot/suggest/activate') || false const isCopilotActivated = props.config.get('settings/copilot/suggest/activate') || false
let copilotMaxnewToken = props.config.get('settings/copilot/suggest/max_new_tokens') let copilotMaxnewToken = props.config.get('settings/copilot/suggest/max_new_tokens')
if (!copilotMaxnewToken) { if (!copilotMaxnewToken) {
props.config.set('settings/copilot/suggest/max_new_tokens', 5) props.config.set('settings/copilot/suggest/max_new_tokens', 10)
copilotMaxnewToken = 5 copilotMaxnewToken = 10
} }
let copilotTemperatureValue = (props.config.get('settings/copilot/suggest/temperature')) * 100 let copilotTemperatureValue = (props.config.get('settings/copilot/suggest/temperature')) * 100
if (!copilotTemperatureValue) { if (!copilotTemperatureValue) {
props.config.set('settings/copilot/suggest/temperature', 0.5) props.config.set('settings/copilot/suggest/temperature', 0.9)
copilotTemperatureValue = 0.5 copilotTemperatureValue = 0.9
} }
if (isCopilotActivated) props.plugin.call('copilot-suggestion', 'init')
const copilotSettings = () => ( const copilotSettings = () => (
<div className="border-top"> <div className="border-top">
<div className="card-body pt-3 pb-2"> <div className="card-body pt-3 pb-2">
@ -477,19 +454,6 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
<FormattedMessage id="settings.copilot" /> <FormattedMessage id="settings.copilot" />
</h6> </h6>
<div className="pt-2 mb-0">
<div className="text-secondary mb-0 h6">
<div>
<div className="custom-control custom-checkbox mb-1">
<input onChange={onchangeCopilotActivate} id="copilot-activate" type="checkbox" className="custom-control-input" checked={isCopilotActivated} />
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/copilot/suggest/activate')}`} htmlFor="copilot-activate">
<FormattedMessage id="settings.copilot.activate" />
</label>
</div>
</div>
</div>
</div>
<div className="pt-2 mb-0"> <div className="pt-2 mb-0">
<div className="text-secondary mb-0 h6"> <div className="text-secondary mb-0 h6">
<div> <div>

@ -56,3 +56,26 @@
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.loadingExplanation {
animation: fancy-spin 2000ms;
animation-iteration-count: infinite;
}
@keyframes fancy-spin {
0% {
transform: scale(1);
}
25% {
transform: scale(1);
}
50% {
transform: scale(1.3);
}
75% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}

@ -60,8 +60,10 @@ const tabsReducer = (state: ITabsState, action: ITabsAction) => {
export const TabsUI = (props: TabsUIProps) => { export const TabsUI = (props: TabsUIProps) => {
const [tabsState, dispatch] = useReducer(tabsReducer, initialTabsState) const [tabsState, dispatch] = useReducer(tabsReducer, initialTabsState)
const currentIndexRef = useRef(-1) const currentIndexRef = useRef(-1)
const [explaining, setExplaining] = useState<boolean>(false)
const tabsRef = useRef({}) const tabsRef = useRef({})
const tabsElement = useRef(null) const tabsElement = useRef(null)
const [ai_switch, setAI_switch] = useState<boolean>(false)
const tabs = useRef(props.tabs) const tabs = useRef(props.tabs)
tabs.current = props.tabs // we do this to pass the tabs list to the onReady callbacks tabs.current = props.tabs // we do this to pass the tabs list to the onReady callbacks
@ -166,46 +168,142 @@ export const TabsUI = (props: TabsUIProps) => {
<div className="remix-ui-tabs d-flex justify-content-between border-0 header nav-tabs" data-id="tabs-component"> <div className="remix-ui-tabs d-flex justify-content-between border-0 header nav-tabs" data-id="tabs-component">
<div className="d-flex flex-row" style={{maxWidth: 'fit-content', width: '99%'}}> <div className="d-flex flex-row" style={{maxWidth: 'fit-content', width: '99%'}}>
<div className="d-flex flex-row justify-content-center align-items-center m-1 mt-1"> <div className="d-flex flex-row justify-content-center align-items-center m-1 mt-1">
<button <CustomTooltip
data-id="play-editor" placement="bottom"
className="btn text-success py-0" tooltipId="overlay-tooltip-run-script"
disabled={!(tabsState.currentExt === 'js' || tabsState.currentExt === 'ts' || tabsState.currentExt === 'sol' || tabsState.currentExt === 'circom' || tabsState.currentExt === 'vy')} tooltipText={
onClick={async () => { <span>
const path = active().substr(active().indexOf('/') + 1, active().length) {tabsState.currentExt === 'js' || tabsState.currentExt === 'ts' ? (
const content = await props.plugin.call('fileManager', 'readFile', path) <FormattedMessage id="remixUiTabs.tooltipText1" />
if (tabsState.currentExt === 'js' || tabsState.currentExt === 'ts') { ) : tabsState.currentExt === 'sol' || tabsState.currentExt === 'yul' || tabsState.currentExt === 'circom' || tabsState.currentExt === 'vy' ? (
await props.plugin.call('scriptRunner', 'execute', content, path) <FormattedMessage id="remixUiTabs.tooltipText2" />
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt]) ) : (
} else if (tabsState.currentExt === 'sol' || tabsState.currentExt === 'yul') { <FormattedMessage id="remixUiTabs.tooltipText3" />
await props.plugin.call('solidity', 'compile', path) )}
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt]) </span>
} else if (tabsState.currentExt === 'circom') { }
await props.plugin.call('circuit-compiler', 'compile', path)
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
} else if (tabsState.currentExt === 'vy') {
await props.plugin.call('vyper', 'vyperCompileCustomAction')
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
}
}}
> >
<CustomTooltip <button
placement="bottom" data-id="play-editor"
tooltipId="overlay-tooltip-run-script" className="btn text-success py-0"
tooltipText={ disabled={!(tabsState.currentExt === 'js' || tabsState.currentExt === 'ts' || tabsState.currentExt === 'sol' || tabsState.currentExt === 'circom' || tabsState.currentExt === 'vy')}
<span> onClick={async () => {
{tabsState.currentExt === 'js' || tabsState.currentExt === 'ts' ? ( const path = active().substr(active().indexOf('/') + 1, active().length)
<FormattedMessage id="remixUiTabs.tooltipText1" /> const content = await props.plugin.call('fileManager', 'readFile', path)
) : tabsState.currentExt === 'sol' || tabsState.currentExt === 'yul' || tabsState.currentExt === 'circom' || tabsState.currentExt === 'vy' ? ( if (tabsState.currentExt === 'js' || tabsState.currentExt === 'ts') {
<FormattedMessage id="remixUiTabs.tooltipText2" /> await props.plugin.call('scriptRunner', 'execute', content, path)
) : ( _paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
<FormattedMessage id="remixUiTabs.tooltipText3" /> } else if (tabsState.currentExt === 'sol' || tabsState.currentExt === 'yul') {
)} await props.plugin.call('solidity', 'compile', path)
</span> _paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
} } else if (tabsState.currentExt === 'circom') {
await props.plugin.call('circuit-compiler', 'compile', path)
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
} else if (tabsState.currentExt === 'vy') {
await props.plugin.call('vyper', 'vyperCompileCustomAction')
_paq.push(['trackEvent', 'editor', 'clickRunFromEditor', tabsState.currentExt])
}
}}
> >
<i className="fad fa-play"></i> <i className="fad fa-play"></i>
</CustomTooltip> </button>
</button> </CustomTooltip>
<CustomTooltip
placement="bottom"
tooltipId="overlay-tooltip-explaination"
tooltipText={
<span>
{tabsState.currentExt === 'sol'? (
<FormattedMessage id="remixUiTabs.tooltipText5" />
) : (
<FormattedMessage id="remixUiTabs.tooltipText4" />
)}
</span>
}
>
<button
data-id="explain-editor"
id='explain_btn'
className='btn py-0 text-ai px-0 d-flex'
disabled={!(tabsState.currentExt === 'sol') || explaining}
onClick={async () => {
const path = active().substr(active().indexOf('/') + 1, active().length)
const content = await props.plugin.call('fileManager', 'readFile', path)
if (tabsState.currentExt === 'sol') {
setExplaining(true)
await props.plugin.call('solcoder', 'code_explaining', content)
setExplaining(false)
_paq.push(['trackEvent', 'ai', 'solcoder', 'explain_file'])
}
}}
>
<i className={`fa-solid fa-user-robot ${explaining ? 'loadingExplanation' : ''}`}></i>
<span
className="position-relative text-ai text-sm pl-1"
style={{fontSize: "x-small", alignSelf: "end"}}
>
AI
</span>
</button>
</CustomTooltip>
<CustomTooltip
placement="bottom"
tooltipId="overlay-tooltip-copilot"
tooltipText={
<span>
{ tabsState.currentExt === 'sol'? (
!ai_switch ? (
<FormattedMessage id="remixUiTabs.tooltipText6" />
) : (<FormattedMessage id="remixUiTabs.tooltipText7" />)
) : (
<FormattedMessage id="remixUiTabs.tooltipText4" />
)}
</span>
}
>
<button
data-id="remix_ai_switch"
id='remix_ai_switch'
className="btn ai-switch text-ai pl-2 pr-0 py-0 d-flex"
disabled={!(tabsState.currentExt === 'sol' )}
onClick={async () => {
await props.plugin.call('settings', 'updateCopilotChoice', !ai_switch)
setAI_switch(!ai_switch)
ai_switch ? _paq.push(['trackEvent', 'ai', 'solcoder', 'copilot_enabled']) : _paq.push(['trackEvent', 'ai', 'solcoder', 'copilot_disabled'])
}}
>
<i
className={ai_switch ? "fa-solid fa-toggle-on" : "fa-solid fa-toggle-off"}
></i>
<span
className="position-relative text-ai text-sm pl-1"
style={{fontSize: "x-small", alignSelf: "end"}}
>
AI
</span>
</button>
</CustomTooltip>
<CustomTooltip placement="bottom" tooltipId="overlay-tooltip-aiDocumentation" tooltipText={<FormattedMessage id="remixUiTabs.tooltipText8" />}>
<span
data-id="remix_ai_docs"
id="remix_ai_docs"
className="btn ai-docs"
role='link'
onClick={()=>{
window.open("https://remix-ide.readthedocs.io/en/latest/security.html")
_paq.push(['trackEvent', 'ai', 'solcoder', 'documentation'])
}}
>
<i className="fa-solid fa-book text-ai"></i>
<span
className="position-relative text-ai text-sm pl-1"
style={{fontSize: "x-small", alignSelf: "end"}}
>
AI
</span>
</span>
</CustomTooltip>
<CustomTooltip placement="bottom" tooltipId="overlay-tooltip-zoom-out" tooltipText={<FormattedMessage id="remixUiTabs.zoomOut" />}> <CustomTooltip placement="bottom" tooltipId="overlay-tooltip-zoom-out" tooltipText={<FormattedMessage id="remixUiTabs.zoomOut" />}>
<span data-id="tabProxyZoomOut" className="btn btn-sm px-2 fas fa-search-minus text-dark" onClick={() => props.onZoomOut()}></span> <span data-id="tabProxyZoomOut" className="btn btn-sm px-2 fas fa-search-minus text-dark" onClick={() => props.onZoomOut()}></span>
</CustomTooltip> </CustomTooltip>

@ -1,4 +1,4 @@
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, TOGGLE, SEARCH, SET_ISVM, SET_OPEN} from '../types/terminalTypes' import {CLEAR_CONSOLE, CMD_HISTORY, EMPTY_BLOCK, ERROR, HTML, INFO, KNOWN_TRANSACTION, LISTEN_ON_NETWORK, LOG, TYPEWRITERLOG, TYPEWRITERWARNING, AITYPEWRITERWARNING, TYPEWRITERSUCCESS, NEW_TRANSACTION, SCRIPT, UNKNOWN_TRANSACTION, WARN, TOGGLE, SEARCH, SET_ISVM, SET_OPEN} from '../types/terminalTypes'
export const initialState = { export const initialState = {
journalBlocks: [], journalBlocks: [],
@ -181,6 +181,11 @@ export const registerScriptRunnerReducer = (state, action) => {
...state, ...state,
journalBlocks: initialState.journalBlocks.push({message: action.payload.message, typewriter: true, style: 'text-log', provider: action.payload.provider}), journalBlocks: initialState.journalBlocks.push({message: action.payload.message, typewriter: true, style: 'text-log', provider: action.payload.provider}),
} }
case AITYPEWRITERWARNING:
return {
...state,
journalBlocks: initialState.journalBlocks.push({ message: action.payload.message, typewriter: true, style: 'text-ai', provider: action.payload.provider })
}
case TYPEWRITERWARNING: case TYPEWRITERWARNING:
return { return {
...state, ...state,

@ -239,7 +239,11 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
call('terminal', 'log',{ type: 'warn', value: `> ${script}` }) call('terminal', 'log',{ type: 'warn', value: `> ${script}` })
await call('openaigpt', 'message', script) await call('openaigpt', 'message', script)
_paq.push(['trackEvent', 'ai', 'openai', 'askFromTerminal']) _paq.push(['trackEvent', 'ai', 'openai', 'askFromTerminal'])
} else { } else if (script.trim().startsWith('sol-gpt')) {
call('terminal', 'log',{ type: 'warn', value: `> ${script}` })
await call('solcoder', 'solidity_answer', script)
_paq.push(['trackEvent', 'ai', 'solcoder', 'askFromTerminal'])
}else {
await call('scriptRunner', 'execute', script) await call('scriptRunner', 'execute', script)
} }
done() done()

@ -57,6 +57,9 @@ const TerminalWelcomeMessage = ({packageJson, storage}) => {
<li key="gpt"> <li key="gpt">
gpt <i>&lt;your question here&gt;</i> {' '} gpt <i>&lt;your question here&gt;</i> {' '}
</li> </li>
<li key="sol-gpt">
sol-gpt <i>&lt;your Solidity question here&gt;</i> {' '}
</li>
</ul> </ul>
<div> <div>
<FormattedMessage id="terminal.welcomeText10" />. <FormattedMessage id="terminal.welcomeText10" />.

@ -17,6 +17,7 @@ export const HTML = 'html'
export const LOG = 'log' export const LOG = 'log'
export const TYPEWRITERLOG = 'typewriterlog' export const TYPEWRITERLOG = 'typewriterlog'
export const TYPEWRITERWARNING = 'typewriterwarning' export const TYPEWRITERWARNING = 'typewriterwarning'
export const AITYPEWRITERWARNING = 'aitypewriterwarning'
export const TYPEWRITERSUCCESS = 'typewritersuccess' export const TYPEWRITERSUCCESS = 'typewritersuccess'
export const INFO = 'info' export const INFO = 'info'
export const WARN = 'warn' export const WARN = 'warn'

@ -1,5 +1,5 @@
export const wrapScript = (script) => { export const wrapScript = (script) => {
const isKnownScript = ['remix.', 'console.', 'git', 'gpt'].some(prefix => script.trim().startsWith(prefix)) const isKnownScript = ['remix.', 'console.', 'git', 'gpt', 'sol-gpt'].some(prefix => script.trim().startsWith(prefix))
if (isKnownScript) return script if (isKnownScript) return script
return ` return `
try { try {

@ -139,6 +139,7 @@
"@ethereumjs/util": "9.0.3", "@ethereumjs/util": "9.0.3",
"@ethereumjs/vm": "8.0.0", "@ethereumjs/vm": "8.0.0",
"@ethersphere/bee-js": "^3.2.0", "@ethersphere/bee-js": "^3.2.0",
"@gradio/client": "^0.10.1",
"@isomorphic-git/lightning-fs": "^4.4.1", "@isomorphic-git/lightning-fs": "^4.4.1",
"@microlink/react-json-view": "^1.23.0", "@microlink/react-json-view": "^1.23.0",
"@openzeppelin/contracts": "^5.0.0", "@openzeppelin/contracts": "^5.0.0",

@ -2926,6 +2926,15 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@gradio/client@^0.10.1":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@gradio/client/-/client-0.10.1.tgz#cdd90efbc0156d8e338af61031d2c88f21134f11"
integrity sha512-C3uWIWEqlpTuG3sfPw3K3+26Fkr+jXPL8U2lC1u7DlBm25rHdGMVX17o8ApW7XcFtznfaLceVtpnDPkDpQTJlw==
dependencies:
bufferutil "^4.0.7"
semiver "^1.1.0"
ws "^8.13.0"
"@humanwhocodes/config-array@^0.11.10": "@humanwhocodes/config-array@^0.11.10":
version "0.11.10" version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
@ -9912,6 +9921,13 @@ bufferutil@4.0.7:
dependencies: dependencies:
node-gyp-build "^4.3.0" node-gyp-build "^4.3.0"
bufferutil@^4.0.7:
version "4.0.8"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea"
integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==
dependencies:
node-gyp-build "^4.3.0"
builtin-modules@^1.0.0, builtin-modules@^1.1.1: builtin-modules@^1.0.0, builtin-modules@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@ -26092,6 +26108,11 @@ selfsigned@^2.1.1:
dependencies: dependencies:
node-forge "^1" node-forge "^1"
semiver@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/semiver/-/semiver-1.1.0.tgz#9c97fb02c21c7ce4fcf1b73e2c7a24324bdddd5f"
integrity sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==
semver-compare@^1.0.0: semver-compare@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
@ -30254,6 +30275,11 @@ ws@^7.3.1, ws@^7.4.6, ws@^7.5.1:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^8.13.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
ws@^8.4.2: ws@^8.4.2:
version "8.9.0" version "8.9.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"

Loading…
Cancel
Save