diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 3c195c99cd..c96f702339 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -244,13 +244,14 @@ module.exports = { .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/hhLogs_test.sol', 60000) - .assert.containsText('#journal > div:nth-child(3) > span', 'Before all:') - .assert.containsText('#journal > div:nth-child(3) > span', 'Inside beforeAll') - .assert.containsText('#journal > div:nth-child(4) > span', 'Check sender:') - .assert.containsText('#journal > div:nth-child(4) > span', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') - .assert.containsText('#journal > div:nth-child(5) > span', 'Check int logs:') - .assert.containsText('#journal > div:nth-child(5) > span', '10 20') - .assert.containsText('#journal > div:nth-child(5) > span', 'Number is 25') + .pause(2000) + .assert.containsText('#journal > div:nth-child(4) > span', 'Before all:') + .assert.containsText('#journal > div:nth-child(4) > span', 'Inside beforeAll') + .assert.containsText('#journal > div:nth-child(5) > span', 'Check sender:') + .assert.containsText('#journal > div:nth-child(5) > span', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') + .assert.containsText('#journal > div:nth-child(6) > span', 'Check int logs:') + .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') .removeFile('tests/hhLogs_test.sol', 'workspace_new') }, diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index d78768c6a2..8bea66ca02 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -59,6 +59,7 @@ import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin' import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin' import {OpenAIGpt} from './app/plugins/openaigpt' +import {SolCoder} from './app/plugins/solcoderAI' const isElectron = require('is-electron') @@ -67,7 +68,6 @@ const remixLib = require('@remix-project/remix-lib') import { QueryParams } from '@remix-project/remix-lib' import { SearchPlugin } from './app/tabs/search' import { ElectronProvider } from './app/files/electronProvider' -import { CopilotSuggestion } from './app/plugins/copilot/suggestion-service/copilot-suggestion' const Storage = remixLib.Storage const RemixDProvider = require('./app/files/remixDProvider') @@ -233,7 +233,7 @@ class AppComponent { // ----------------- AI -------------------------------------- const openaigpt = new OpenAIGpt() - const copilotSuggestion = new CopilotSuggestion() + const solcoder = new SolCoder() // ----------------- import content service ------------------------ const contentImport = new CompilerImports() @@ -362,7 +362,7 @@ class AppComponent { solidityScript, templates, openaigpt, - copilotSuggestion + solcoder, ]) //---- fs plugin @@ -517,7 +517,9 @@ class AppComponent { } ) await this.appManager.activatePlugin(['solidity-script', 'openaigpt']) + await this.appManager.activatePlugin(['solcoder']) + await this.appManager.activatePlugin(['filePanel']) // Set workspace after initial activation diff --git a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts b/apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts deleted file mode 100644 index ac1798b400..0000000000 --- a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts +++ /dev/null @@ -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() - } -} diff --git a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts b/apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts deleted file mode 100644 index b85647cb9f..0000000000 --- a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts +++ /dev/null @@ -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) - } - }) - } -} diff --git a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js b/apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js deleted file mode 100644 index a5affc48b1..0000000000 --- a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js +++ /dev/null @@ -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, - }); - } -}); \ No newline at end of file diff --git a/apps/remix-ide/src/app/plugins/openaigpt.tsx b/apps/remix-ide/src/app/plugins/openaigpt.tsx index 9d1e121722..cc120a0d1e 100644 --- a/apps/remix-ide/src/app/plugins/openaigpt.tsx +++ b/apps/remix-ide/src/app/plugins/openaigpt.tsx @@ -19,7 +19,7 @@ export class OpenAIGpt extends Plugin { async message(prompt): Promise { 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 try { result = await ( @@ -38,11 +38,11 @@ export class OpenAIGpt extends Plugin { } 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) { - this.call('terminal', 'log', { type: 'typewriterwarning', value: result.error }) + this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.error }) } else { - this.call('terminal', 'log', { type: 'typewriterwarning', value: 'No response...' }) + this.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'No response...' }) } return result.data } diff --git a/apps/remix-ide/src/app/plugins/solcoderAI.tsx b/apps/remix-ide/src/app/plugins/solcoderAI.tsx new file mode 100644 index 0000000000..f4da44af3f --- /dev/null +++ b/apps/remix-ide/src/app/plugins/solcoderAI.tsx @@ -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 { + 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 { + 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 { + 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 { + 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") + } + } + + +} diff --git a/apps/remix-ide/src/app/tabs/locales/en/editor.json b/apps/remix-ide/src/app/tabs/locales/en/editor.json index fac7f7a60e..33643eb0bb 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/editor.json +++ b/apps/remix-ide/src/app/tabs/locales/en/editor.json @@ -23,8 +23,10 @@ "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.explainFunction": "Explain this function", + "editor.explainFunctionSol": "Explain this code", "editor.explainFunction2": "Explain the function \"{name}\"", "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.executeFreeFunction2": "Run the free function \"{name}\"", "editor.toastText1": "This can only execute free function", diff --git a/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json b/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json index df7628736a..46c5181a84 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json +++ b/apps/remix-ide/src/app/tabs/locales/en/remixUiTabs.json @@ -1,7 +1,12 @@ { "remixUiTabs.tooltipText1": "Run script (CTRL + SHIFT + 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.zoomIn": "Zoom in" } diff --git a/apps/remix-ide/src/app/tabs/settings-tab.tsx b/apps/remix-ide/src/app/tabs/settings-tab.tsx index 8bbf8b2c8a..1f69e113b6 100644 --- a/apps/remix-ide/src/app/tabs/settings-tab.tsx +++ b/apps/remix-ide/src/app/tabs/settings-tab.tsx @@ -15,7 +15,7 @@ const _paq = (window._paq = window._paq || []) const profile = { name: 'settings', displayName: 'Settings', - methods: ['get'], + methods: ['get', 'updateCopilotChoice'], events: [], icon: 'assets/img/settings.webp', description: 'Remix-IDE settings', @@ -36,6 +36,7 @@ module.exports = class SettingsTab extends ViewPlugin { } element: HTMLDivElement public useMatomoAnalytics: any + public useCopilot: any dispatch: React.Dispatch = () => {} constructor(config, editor) { super(profile) @@ -51,6 +52,7 @@ module.exports = class SettingsTab extends ViewPlugin { this.element = document.createElement('div') this.element.setAttribute('id', 'settingsTab') this.useMatomoAnalytics = null + this.useCopilot = false } setDispatch(dispatch: React.Dispatch) { @@ -58,6 +60,9 @@ module.exports = class SettingsTab extends ViewPlugin { this.renderComponent() } + onActivation(): void { + } + render() { return (
@@ -74,6 +79,7 @@ module.exports = class SettingsTab extends ViewPlugin { editor={state.editor} _deps={state._deps} useMatomoAnalytics={state.useMatomoAnalytics} + useCopilot={state.useCopilot} themeModule={state._deps.themeModule} localeModule={state._deps.localeModule} /> @@ -88,6 +94,14 @@ module.exports = class SettingsTab extends ViewPlugin { return this.config.get(key) } + updateCopilotChoice(isChecked) { + this.config.set('settings/copilot/suggest/activate', isChecked) + this.useCopilot = isChecked + this.dispatch({ + ...this + }) + } + updateMatomoAnalyticsChoice(isChecked) { this.config.set('settings/matomo-analytics', isChecked) this.useMatomoAnalytics = isChecked diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css index 3ed6161e19..eac0b0f94f 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css @@ -10,6 +10,7 @@ * Copyright 2011-2020 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */:root { + --ai: #da2de4; --blue:#033c73; --indigo:#6610f2; --purple:#6f42c1; @@ -8373,6 +8374,9 @@ a.text-danger:focus,a.text-danger:hover { a.text-light:focus,a.text-light:hover { color:#cbd3da!important } +.text-ai { + color: #da2de4 !important; +} .text-dark { color:#343a40!important } diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css index 47c7fa9b91..0c74e7f730 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css @@ -11,6 +11,7 @@ * 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); :root { + --ai: #2de7f3; --blue:#2a9fd6; --indigo:#6610f2; --purple:#6f42c1; @@ -8375,6 +8376,9 @@ a.text-danger:focus,a.text-danger:hover { a.text-light:focus,a.text-light:hover { color:#000!important } +.text-dark { + color: #babbcc !important; +} .text-dark { color:#adafae!important } diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css index e9b14b99af..45d3dfe8eb 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css @@ -10,7 +10,8 @@ * Copyright 2011-2020 Twitter, Inc. * 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 { - --blue:#2c3e50; + --ai: #da2de4; + --blue:#2c3e50; --indigo:#6610f2; --purple:#6f42c1; --pink:#e83e8c; @@ -7004,6 +7005,9 @@ a.text-danger:focus,a.text-danger:hover { a.text-light:focus,a.text-light:hover { color:#c0cdd1!important } +.text-ai { + color: #da2de4 !important; +} .text-dark { color:#7b8a8b!important } diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css index 3129408286..dc4d761c61 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css @@ -11,6 +11,7 @@ * 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); :root { + --ai: #da2de4; --blue:#446e9b; --indigo:#6610f2; --purple:#6f42c1; @@ -8375,6 +8376,9 @@ a.text-danger:focus,a.text-danger:hover { a.text-light:focus,a.text-light:hover { color:#c8c8c8!important } +.text-ai { + color: #da2de4 !important; +} .text-dark { color:#333!important } diff --git a/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css b/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css index 103957dc53..8af0836f0b 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css +++ b/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css @@ -1,5 +1,6 @@ @import url('https://fonts.googleapis.com/css?family=Nunito+Sans:400,600&display=swap'); :root { + --ai: #2de7f3; --blue: #90c3f6; --indigo: #6610f2; --purple: #9e77f6; @@ -8590,6 +8591,9 @@ a.text-light:hover { .text-dark { color: #babbcc !important; } +.text-ai { + color: #2de7f3 !important; +} a.text-dark:focus, a.text-dark:hover { color: #6f7087 !important; diff --git a/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css b/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css index 73574f8f5f..aba42e3ac6 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css +++ b/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css @@ -1,4 +1,5 @@ :root { + --ai: #da2de4; --blue: #007bff; --indigo: #6610f2; --purple: #6f42c1; @@ -9364,7 +9365,9 @@ a.text-light:hover, a.text-light:focus { color: #d9d9d9 !important; } - +.text-ai { + color: #da2de4 !important; +} .text-dark { color: #11556c !important; } diff --git a/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css b/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css index de97d73757..1708371250 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css +++ b/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css @@ -1,4 +1,5 @@ :root { + --ai: #2de7f3; --blue: #007aa6; --indigo: #6610f2; --purple: #9e77f6; @@ -8570,6 +8571,9 @@ a.text-info:hover { .text-warning { color: #c97539 !important; } +.text-ai { + color: #2de7f3 !important; +} a.text-warning:focus, a.text-warning:hover { color: #8f5227 !important; diff --git a/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css b/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css index 0ef4225d57..6c9ecb9534 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css +++ b/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css @@ -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=JetBrains+Mono:ital,wght@0,400;0,700;1,400&display=swap'); :root { + --ai: #2de7f3; --blue: #2cc1f7; --indigo: #6610f2; --purple: #6f42c1; @@ -8604,6 +8605,9 @@ a.text-light:hover { .text-dark { color: #babbcc !important; } +.text-ai { + color: #2de7f3 !important; +} a.text-dark:focus, a.text-dark:hover { color: #6f7087 !important; diff --git a/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css b/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css index b99025172c..3d47c2031c 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css +++ b/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css @@ -1,4 +1,5 @@ :root { + --ai: #da2de4; --blue: #007bff; --indigo: #6610f2; --purple: #7c47b9; @@ -9360,7 +9361,9 @@ a.text-light:hover, a.text-light:focus { color: #d9d9d9 !important; } - +.text-ai { + color: #da2de4 !important; +} .text-dark { color: #747B90 !important; } diff --git a/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css b/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css index f64bc8d01b..0b6cb171f4 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css +++ b/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css @@ -1,4 +1,5 @@ :root { + --ai: #da2de4; --blue: #007bff; --indigo: #6610f2; --purple: #6f42c1; @@ -9366,7 +9367,9 @@ a.text-light:hover, a.text-light:focus { color: #d9d9d9 !important; } - +.text-ai { + color: #da2de4 !important; +} .text-dark { color: #11556c !important; } diff --git a/apps/remix-ide/src/assets/css/themes/remix-unicorn.css b/apps/remix-ide/src/assets/css/themes/remix-unicorn.css index ebf508ff83..a7f4196f71 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-unicorn.css +++ b/apps/remix-ide/src/assets/css/themes/remix-unicorn.css @@ -1,4 +1,5 @@ :root { + --ai: #da2de4; --blue: #007bff; --indigo: #6610f2; --purple: #7c47b9; @@ -9360,7 +9361,9 @@ a.text-light:hover, a.text-light:focus { color: #d9d9d9 !important; } - +.text-ai { + color: #da2de4 !important; +} .text-dark { color: #747B90 !important; } diff --git a/apps/remix-ide/src/assets/css/themes/remix-violet.css b/apps/remix-ide/src/assets/css/themes/remix-violet.css index 275d0f0907..85b855c862 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-violet.css +++ b/apps/remix-ide/src/assets/css/themes/remix-violet.css @@ -1,4 +1,5 @@ :root { + --ai: #da2de4; --blue: #007bff; --indigo: #6610f2; --purple: #7c47b9; @@ -9356,7 +9357,9 @@ a.text-light:hover, a.text-light:focus { color: #d9d9d9 !important; } - +.text-ai { + color: #da2de4 !important; +} .text-dark { color: #747B90 !important; } diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 9eaca5b06d..0e1d78fdce 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -78,10 +78,10 @@ let requiredModules = [ // services + layout views + system views 'contractflattener', 'solidity-script', 'openaigpt', + 'solcoder', 'home', 'doc-viewer', 'doc-gen', - 'copilot-suggestion', 'remix-templates' ] diff --git a/apps/remix-ide/src/remixEngine.js b/apps/remix-ide/src/remixEngine.js index 2eac1e085d..948eca47bc 100644 --- a/apps/remix-ide/src/remixEngine.js +++ b/apps/remix-ide/src/remixEngine.js @@ -27,6 +27,7 @@ export class RemixEngine extends Engine { if (name === 'filePanel') return { queueTimeout: 60000 * 20 } if (name === 'fileManager') return { queueTimeout: 60000 * 20 } if (name === 'openaigpt') return { queueTimeout: 60000 * 2 } + if (name === 'solcoder') return { queueTimeout: 60000 * 2 } if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 } return { queueTimeout: 10000 } } diff --git a/apps/remixdesktop/src/menus/view.ts b/apps/remixdesktop/src/menus/view.ts index 0a36d5367b..e488cfa50e 100644 --- a/apps/remixdesktop/src/menus/view.ts +++ b/apps/remixdesktop/src/menus/view.ts @@ -79,9 +79,9 @@ export default ( accelerator: 'CmdOrCtrl+0', click: function(item, focusedWindow) { if (focusedWindow) - { - focusedWindow.webContents.setZoomFactor(1) - } + { + focusedWindow.webContents.setZoomFactor(1) + } } }, diff --git a/libs/remix-ui/editor/src/lib/providers/completionTimer.ts b/libs/remix-ui/editor/src/lib/providers/completionTimer.ts new file mode 100644 index 0000000000..beb5613898 --- /dev/null +++ b/libs/remix-ui/editor/src/lib/providers/completionTimer.ts @@ -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."); + } + } +} \ No newline at end of file diff --git a/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts b/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts index 7f047833e1..196fcd6d63 100644 --- a/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts +++ b/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts @@ -1,6 +1,11 @@ /* eslint-disable no-control-regex */ import { EditorUIProps, monacoTypes } from '@remix-ui/editor'; +import { CompletionTimer } from './completionTimer'; + import axios, {AxiosResponse} from 'axios' +import { slice } from 'lodash'; +const _paq = (window._paq = window._paq || []) + const controller = new AbortController(); const { signal } = controller; const result: string = '' @@ -8,11 +13,14 @@ const result: string = '' export class RemixInLineCompletionProvider implements monacoTypes.languages.InlineCompletionsProvider { props: EditorUIProps monaco: any + completionEnabled: boolean constructor(props: any, monaco: any) { this.props = props this.monaco = monaco + this.completionEnabled = true } + async provideInlineCompletions(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.InlineCompletionContext, token: monacoTypes.CancellationToken): Promise> { if (context.selectedSuggestionInfo) { return; @@ -25,12 +33,14 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli endColumn: position.column, }); - if (!word.endsWith(' ') && !word.endsWith('\n') && !word.endsWith(';') && !word.endsWith('.')) { + if (!word.endsWith(' ') && + !word.endsWith('.') && + !word.endsWith('(')) { return; } 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 } catch (err) { return; @@ -41,9 +51,13 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli if (split.length < 2) return const ask = split[split.length - 2].trimStart() if (split[split.length - 1].trim() === '' && ask.startsWith('///')) { - // use the code generation model - const {data} = await axios.post('https://gpt-chat.remixproject.org/infer', {comment: ask.replace('///', '')}) - const parsedData = JSON.parse(data).trimStart() + // use the code generation model, only take max 1000 word as context + this.props.plugin.call('terminal', 'log', {type: 'aitypewriterwarning', value: 'Solcoder - generating code for following comment: ' + ask.replace('///', '')}) + + 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 = { insertText: parsedData }; @@ -54,41 +68,72 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli } } catch (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 if (token.isCancellationRequested) { return } + // abort if the completion is not enabled + if (!this.completionEnabled) { + return + } + let result 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) { return } + } - const generatedText = (result as any).output[0].generated_text as string - // the generated text remove a space from the context... - 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 - }; + process_completion(data: any) { + let clean = data.split('\n')[0].startsWith('\n') ? [data.split('\n')[0], data.split('\n')[1]].join('\n'): data.split('\n')[0] - // abort if there is a signal - if (token.isCancellationRequested) { - return + // if clean starts with a comment, remove it + if (clean.startsWith('//') || clean.startsWith('/*') || clean.startsWith('*') || clean.startsWith('*/')){ + return "" } - return { - items: [item], - enableForwardStability: true - } - + // remove comment inline + clean = clean.split('//')[0].trimEnd() + return clean } + handleItemDidShow?(completions: monacoTypes.languages.InlineCompletions, item: monacoTypes.languages.InlineCompletion, updatedInsertText: string): void { } 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 f3fd27a798..823d6d290a 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from 'react-intl' import { isArray } from 'lodash' import Editor, { loader, Monaco } from '@monaco-editor/react' 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 { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solidity' 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) let freeFunctionAction const executeFreeFunctionAction = { @@ -775,6 +793,7 @@ export const EditorUI = (props: EditorUIProps) => { freeFunctionAction = editor.addAction(executeFreeFunctionAction) gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction) gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction) + solgptExplainFunctionAction = editor.addAction(executeSolgptExplainFunctionAction) // 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()) @@ -794,6 +813,10 @@ export const EditorUI = (props: EditorUIProps) => { gptExplainFunctionAction.dispose() gptExplainFunctionAction = null } + if (solgptExplainFunctionAction) { + solgptExplainFunctionAction.dispose() + solgptExplainFunctionAction = null + } const file = await props.plugin.call('fileManager', 'getCurrentFile') if (!file.endsWith('.sol')) { @@ -813,6 +836,11 @@ export const EditorUI = (props: EditorUIProps) => { gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction) executegptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunction2'}, {name: functionImpl.name}) 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) } diff --git a/libs/remix-ui/renderer/src/lib/renderer.tsx b/libs/remix-ui/renderer/src/lib/renderer.tsx index 433c91f7ae..58663a5a41 100644 --- a/libs/remix-ui/renderer/src/lib/renderer.tsx +++ b/libs/remix-ui/renderer/src/lib/renderer.tsx @@ -101,7 +101,19 @@ export const Renderer = ({message, opt = {}, plugin}: RendererProps) => { - { askGtp() }}>ASK GPT + + + { askGtp() }} + style={{borderColor: "var(--ai)"}} + > + ASK GPT + +
)} diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index 57f0cc7dd1..706b2912a6 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -38,6 +38,7 @@ export interface RemixUiSettingsProps { editor: any _deps: any useMatomoAnalytics: boolean + useCopilot: boolean themeModule: ThemeModule localeModule: LocaleModule } @@ -76,7 +77,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { } useEffect(() => initValue(), [resetState, props.config]) useEffect(() => initValue(), []) - + useEffect(() => { const token = props.config.get('settings/' + labels['gist'].key) if (token) { @@ -124,7 +125,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { useEffect(() => { if (props.useMatomoAnalytics !== null) useMatomoAnalytics(props.config, props.useMatomoAnalytics, dispatch) - }, [props.useMatomoAnalytics]) + }, [props.useMatomoAnalytics]) const onchangeGenerateContractMetadata = (event) => { generateContractMetadat(props.config, event.target.checked, dispatch) @@ -134,49 +135,26 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { textWrapEventAction(props.config, props.editor, event.target.checked, dispatch) } - const onchangeCopilotActivate = async (event) => { - if (!event.target.checked) { - copilotActivate(props.config, event.target.checked, dispatch) - props.plugin.call('copilot-suggestion', 'uninstall') + const onchangeCopilotActivate = () => { + if (!props.useCopilot) { + copilotActivate(props.config, props.useCopilot, dispatch) + props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot deactivated!` }) return - } - const message =
Please wait while the copilot is downloaded. 0/100 .
- props.plugin.on('copilot-suggestion', 'loading', (data) => { - if (!copilotDownload.current) return - const loaded = ((data.loaded / data.total) * 100).toString() - 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) - } - } + } + + const startCopilot = async () => { + copilotActivate(props.config, true, dispatch) + props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot activated!` }) } - 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) => { 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 let copilotMaxnewToken = props.config.get('settings/copilot/suggest/max_new_tokens') if (!copilotMaxnewToken) { - props.config.set('settings/copilot/suggest/max_new_tokens', 5) - copilotMaxnewToken = 5 + props.config.set('settings/copilot/suggest/max_new_tokens', 10) + copilotMaxnewToken = 10 } let copilotTemperatureValue = (props.config.get('settings/copilot/suggest/temperature')) * 100 if (!copilotTemperatureValue) { - props.config.set('settings/copilot/suggest/temperature', 0.5) - copilotTemperatureValue = 0.5 + props.config.set('settings/copilot/suggest/temperature', 0.9) + copilotTemperatureValue = 0.9 } - if (isCopilotActivated) props.plugin.call('copilot-suggestion', 'init') const copilotSettings = () => (
@@ -477,19 +454,6 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { -
-
-
-
- - -
-
-
-
-
diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.css b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.css index e769523822..8ecd601f66 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.css +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.css @@ -56,3 +56,26 @@ -ms-overflow-style: none; /* IE and Edge */ 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); + } +} \ No newline at end of file diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index b11d1306ae..dfc024684b 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -60,8 +60,10 @@ const tabsReducer = (state: ITabsState, action: ITabsAction) => { export const TabsUI = (props: TabsUIProps) => { const [tabsState, dispatch] = useReducer(tabsReducer, initialTabsState) const currentIndexRef = useRef(-1) + const [explaining, setExplaining] = useState(false) const tabsRef = useRef({}) const tabsElement = useRef(null) + const [ai_switch, setAI_switch] = useState(false) const tabs = useRef(props.tabs) 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) => {
- + + + + {tabsState.currentExt === 'sol'? ( + + ) : ( + + )} + + } + > + + + + { tabsState.currentExt === 'sol'? ( + !ai_switch ? ( + + ) : () + ) : ( + + )} + + } + > + + + }> + { + window.open("https://remix-ide.readthedocs.io/en/latest/security.html") + _paq.push(['trackEvent', 'ai', 'solcoder', 'documentation']) + }} + > + + + AI + + + + }> props.onZoomOut()}> diff --git a/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts b/libs/remix-ui/terminal/src/lib/reducers/terminalReducer.ts index 3b1ed4d469..ea76680775 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, 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 = { journalBlocks: [], @@ -181,6 +181,11 @@ export const registerScriptRunnerReducer = (state, action) => { ...state, 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: 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 3a16832b18..ab0a7a768f 100644 --- a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -239,7 +239,11 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { call('terminal', 'log',{ type: 'warn', value: `> ${script}` }) await call('openaigpt', 'message', script) _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) } done() diff --git a/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx b/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx index 24c37b400e..3531b9ede2 100644 --- a/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx +++ b/libs/remix-ui/terminal/src/lib/terminalWelcome.tsx @@ -57,6 +57,9 @@ const TerminalWelcomeMessage = ({packageJson, storage}) => {
  • gpt <your question here> {' '}
  • +
  • + sol-gpt <your Solidity question here> {' '} +
  • . diff --git a/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts b/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts index 52e667badb..0de5941117 100644 --- a/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts +++ b/libs/remix-ui/terminal/src/lib/types/terminalTypes.ts @@ -17,6 +17,7 @@ export const HTML = 'html' export const LOG = 'log' export const TYPEWRITERLOG = 'typewriterlog' export const TYPEWRITERWARNING = 'typewriterwarning' +export const AITYPEWRITERWARNING = 'aitypewriterwarning' export const TYPEWRITERSUCCESS = 'typewritersuccess' export const INFO = 'info' export const WARN = 'warn' diff --git a/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts b/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts index 26ded2f581..41d027b12d 100644 --- a/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts +++ b/libs/remix-ui/terminal/src/lib/utils/wrapScript.ts @@ -1,5 +1,5 @@ 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 return ` try { diff --git a/package.json b/package.json index b7adab73c1..0cae87a40e 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "@ethereumjs/util": "9.0.3", "@ethereumjs/vm": "8.0.0", "@ethersphere/bee-js": "^3.2.0", + "@gradio/client": "^0.10.1", "@isomorphic-git/lightning-fs": "^4.4.1", "@microlink/react-json-view": "^1.23.0", "@openzeppelin/contracts": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 38e930493f..9650c62d8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2926,6 +2926,15 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" 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": version "0.11.10" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" @@ -9912,6 +9921,13 @@ bufferutil@4.0.7: dependencies: 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: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -26092,6 +26108,11 @@ selfsigned@^2.1.1: dependencies: 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: version "1.0.0" 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" 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: version "8.9.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"