diff --git a/.gitignore b/.gitignore index d9426a7bf7f..6599f59e045 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,9 @@ stats.json release .env .env.local - +.idea/ +.yalc/ +.yalc # compiled output /dist diff --git a/apps/remix-ide-e2e/src/commands/addFile.ts b/apps/remix-ide-e2e/src/commands/addFile.ts index f2040f749fd..9b94eed1dd0 100644 --- a/apps/remix-ide-e2e/src/commands/addFile.ts +++ b/apps/remix-ide-e2e/src/commands/addFile.ts @@ -3,8 +3,8 @@ import EventEmitter from 'events' class AddFile extends EventEmitter { command(this: NightwatchBrowser, name: string, content: NightwatchContractContent, readMeFile?:string): NightwatchBrowser { - if(!readMeFile) - readMeFile = 'README.txt' + if (!readMeFile) + readMeFile = 'README.txt' this.api.perform((done) => { addFile(this.api, name, content, readMeFile, () => { done() @@ -63,7 +63,7 @@ function addFile(browser: NightwatchBrowser, name: string, content: NightwatchCo }) .setEditorValue(content.content) .getEditorValue((result) => { - if(result != content.content) { + if (result != content.content) { browser.setEditorValue(content.content) } }) diff --git a/apps/remix-ide-e2e/src/commands/assistantAddContext.ts b/apps/remix-ide-e2e/src/commands/assistantAddContext.ts new file mode 100644 index 00000000000..ff0956f5eb0 --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/assistantAddContext.ts @@ -0,0 +1,40 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class AssistantAddCtx extends EventEmitter { + command(this: NightwatchBrowser, prompt: string): NightwatchBrowser { + this.api.perform((done) => { + selectCtx(this.api, prompt, () => { + done() + this.emit('complete') + }) + }) + return this + } +} + +function selectCtx(browser: NightwatchBrowser, ctx: string, done: VoidFunction) { + browser + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .waitForElementVisible('*[data-id="composer-ai-add-context"]') + .click('*[data-id="composer-ai-add-context"]') + .waitForElementVisible('*[data-id="currentFile-context-option"]') + .perform(async ()=> { + switch (ctx) { + case 'currentFile': + browser.click('*[data-id="currentFile-context-option"]'); + break; + case 'workspace': + browser.click('*[data-id="workspace-context-option"]'); + break; + case 'openedFiles': + browser.click('*[data-id="allOpenedFiles-context-option"]'); + break; + default: + break; + } + }) + done() +} + +module.exports = AssistantAddCtx; \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/commands/assistantGenerate.ts b/apps/remix-ide-e2e/src/commands/assistantGenerate.ts new file mode 100644 index 00000000000..2841fbce60f --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/assistantGenerate.ts @@ -0,0 +1,32 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class AssistantGenerate extends EventEmitter { + command(this: NightwatchBrowser, prompt: string, provider: string): NightwatchBrowser { + this.api.perform((done) => { + generate(this.api, prompt, provider, () => { + done() + this.emit('complete') + }) + }) + return this + } +} + +function generate(browser: NightwatchBrowser, prompt: string, provider: string, done: VoidFunction) { + browser + .click('*[data-id="composer-textarea"]') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantSetProvider(provider) + .pause(1000) + .execute(function (prompt) { + (window as any).sendChatMessage(`/generate ${prompt}`); + }, [prompt]) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: `//*[@data-id="remix-ai-assistant" and contains(.,"/generate ${prompt}")]` + }) + done() +} + +module.exports = AssistantGenerate; \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/commands/assistantSetProvider.ts b/apps/remix-ide-e2e/src/commands/assistantSetProvider.ts new file mode 100644 index 00000000000..a64bb574c60 --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/assistantSetProvider.ts @@ -0,0 +1,30 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class SetAssistantProvider extends EventEmitter { + command(this: NightwatchBrowser, provider: string): NightwatchBrowser { + this.api.perform((done) => { + setAssistant( this.api, provider, () => { + done() + this.emit('complete') + }) + }) + return this + } +} + +function setAssistant(browser: NightwatchBrowser, provider: string, done: VoidFunction) { + browser + .waitForElementVisible('*[data-id="composer-textarea"]') + .execute(function (provider) { + (window as any).sendChatMessage(`/setAssistant ${provider}`); + }, [provider]) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"AI Provider set to")]', + timeout: 50000 + }) + .perform(() => done()) +} + +module.exports = SetAssistantProvider diff --git a/apps/remix-ide-e2e/src/commands/assistantWorkspace.ts b/apps/remix-ide-e2e/src/commands/assistantWorkspace.ts new file mode 100644 index 00000000000..100c4fc4635 --- /dev/null +++ b/apps/remix-ide-e2e/src/commands/assistantWorkspace.ts @@ -0,0 +1,28 @@ +import { NightwatchBrowser } from 'nightwatch' +import EventEmitter from 'events' + +class AssistantWorkspace extends EventEmitter { + command(this: NightwatchBrowser, prompt: string, provider: string): NightwatchBrowser { + this.api.perform((done) => { + workspaceGenerate(this.api, prompt, provider, () => { + done() + this.emit('complete') + }) + }) + return this + } +} + +function workspaceGenerate(browser: NightwatchBrowser, prompt: string, provider: string, done: VoidFunction) { + browser + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantSetProvider(provider) + .pause(1000) + .execute(function (prompt) { + (window as any).sendChatMessage(`/workspace ${prompt}`); + }, [prompt]) + done() +} + +module.exports = AssistantWorkspace; \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/commands/enableClipBoard.ts b/apps/remix-ide-e2e/src/commands/enableClipBoard.ts index 4f5a8ed8da2..1a704db9097 100644 --- a/apps/remix-ide-e2e/src/commands/enableClipBoard.ts +++ b/apps/remix-ide-e2e/src/commands/enableClipBoard.ts @@ -2,10 +2,10 @@ import { NightwatchBrowser } from 'nightwatch' import EventEmitter from 'events' class EnableClipBoard extends EventEmitter { - command (this: NightwatchBrowser, remember:boolean, accept: boolean): NightwatchBrowser { + command (this: NightwatchBrowser,): NightwatchBrowser { const browser = this.api - - if(browser.browserName.indexOf('chrome') > -1){ + + if (browser.browserName.indexOf('chrome') > -1){ const chromeBrowser = (browser as any).chrome chromeBrowser.setPermission('clipboard-read', 'granted') chromeBrowser.setPermission('clipboard-write', 'granted') @@ -13,7 +13,6 @@ class EnableClipBoard extends EventEmitter { browser.executeAsyncScript(function (done) { navigator.clipboard.writeText('test').then(function () { navigator.clipboard.readText().then(function (text) { - console.log('Pasted content: ', text) done(text) }).catch(function (err) { console.error('Failed to read clipboard contents: ', err) diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index 809824bd9ac..893d9c97a0a 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -15,7 +15,6 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url .url(url || 'http://127.0.0.1:8080') .pause(5000) .switchBrowserTab(0) - .hidePopupPanel() .perform((done) => { if (!loadPlugin) return done() browser diff --git a/apps/remix-ide-e2e/src/tests/ai_panel.test.ts b/apps/remix-ide-e2e/src/tests/ai_panel.test.ts index 4b4cf4a81ad..21614c93511 100644 --- a/apps/remix-ide-e2e/src/tests/ai_panel.test.ts +++ b/apps/remix-ide-e2e/src/tests/ai_panel.test.ts @@ -19,34 +19,158 @@ module.exports = { browser .addFile('Untitled.sol', sources[0]['Untitled.sol']) }, - 'Explain the contract': function (browser: NightwatchBrowser) { - browser - .waitForElementVisible('*[data-id="explain-editor"]') - .click('*[data-id="explain-editor"]') - .waitForElementVisible('*[data-id="popupPanelPluginsContainer"]') - .waitForElementVisible('*[data-id="aichat-view"]') - .waitForElementVisible({ - locateStrategy: 'xpath', - selector: '//*[@data-id="aichat-view" and contains(.,"Explain the current code")]' - }) - }, - 'close the popup': function (browser: NightwatchBrowser) { - browser - .waitForElementVisible('*[data-id="popupPanelToggle"]') - .click('*[data-id="popupPanelToggle"]') - .waitForElementNotVisible('*[data-id="popupPanelPluginsContainer"]') - }, - 'Add a bad contract': function (browser: NightwatchBrowser) { - browser - .addFile('Bad.sol', { content: 'errors' }) - .clickLaunchIcon('solidity') - .waitForElementVisible('.ask-remix-ai-button') - .click('.ask-remix-ai-button') - .waitForElementVisible('*[data-id="popupPanelPluginsContainer"]') - .waitForElementVisible('*[data-id="aichat-view"]') - .waitForElementVisible({ - locateStrategy: 'xpath', - selector: '//*[@data-id="aichat-view" and contains(.,"Explain the error")]' - }) - } + 'Should explain the contract': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="explain-editor"]') + .click('*[data-id="explain-editor"]') + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"Explain the current code")]' + }) + .pause(20000) + }, + 'Should add a bad contract and explain using RemixAI': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .addFile('Bad.sol', { content: 'errors' }) + .clickLaunchIcon('solidity') + .waitForElementVisible('.ask-remix-ai-button') + .click('.ask-remix-ai-button') + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"Explain the error")]' + }) + .pause(20000) + }, + 'Should select the AI assistant provider': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .assistantSetProvider('mistralai') + }, + 'Should add current file as context to the AI assistant': function (browser: NightwatchBrowser) { + browser + .addFile('Untitled.sol', sources[0]['Untitled.sol']) + .openFile('Untitled.sol') + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantAddContext('currentFile') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: `//*[@data-id="composer-context-holder" and contains(.,"Untitled.sol")]` + }) + }, + 'Should add workspace as context to the AI assistant': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantAddContext('workspace') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="composer-context-holder" and contains(.,"@workspace")]' + }) + }, + 'Should add opened files as context to the AI assistant': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .addFile('anotherFile.sol', sources[0]['Untitled.sol']) + .assistantAddContext('openedFiles') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="composer-context-holder" and contains(.,"anotherFile.sol")]' + }) + }, + 'Should generate new workspace contract code with the AI assistant': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantGenerate('a simple ERC20 contract', 'mistralai') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"New workspace created:")]' + }, 60000) + }, + 'Should lead to Workspace generation with the AI assistant': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantWorkspace('comment all function', 'mistralai') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and (contains(.,"Modified Files") or contains(.,"No Changes applied"))]' + }, 60000) + }, + 'Should create a new workspace using the AI assistant button in the composer': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .waitForElementVisible('*[data-id="composer-ai-workspace-generate"]') + .click('*[data-id="composer-ai-workspace-generate"]') + .waitForElementVisible('*[data-id="TemplatesSelectionModalDialogModalBody-react"]') + .click('*[data-id="modalDialogCustomPromptTextCreate"]') + .setValue('*[data-id="modalDialogCustomPromptTextCreate"]', 'a simple ERC20 contract') + .click('*[data-id="TemplatesSelection-modal-footer-ok-react"]') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"New workspace created:")]' + }, 6000) + }, + 'Workspace generation with all AI assistant provider': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantWorkspace('remove all comments', 'openai') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and (contains(.,"Modified Files") or contains(.,"No Changes applied"))]' + }, 60000) + .pause(20000) + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantWorkspace('remove all comments', 'anthropic') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and (contains(.,"Modified Files") or contains(.,"No Changes applied"))]' + }, 60000) + .pause(20000) + + }, + 'Generate new workspaces code with all AI assistant providers': function (browser: NightwatchBrowser) { + browser + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantGenerate('a simple ERC20 contract', 'openai') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"New workspace created:")]' + }, 60000) + .pause(20000) + + .refreshPage() + .clickLaunchIcon('remixaiassistant') + .waitForElementVisible('*[data-id="composer-textarea"]') + .assistantGenerate('a simple ERC20 contract', 'anthropic') + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="remix-ai-assistant" and contains(.,"New workspace created:")]' + }, 60000) + }, + "Should close the AI assistant": function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="remix-ai-assistant"]') + .click('*[data-id="movePluginToLeft"]') + .clickLaunchIcon('filePanel') + .waitForElementNotVisible('*[data-id="remix-ai-assistant"]', 5000) + }, } \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/templates.test.ts b/apps/remix-ide-e2e/src/tests/templates.test.ts index 068aa668575..8ae93a61bd7 100644 --- a/apps/remix-ide-e2e/src/tests/templates.test.ts +++ b/apps/remix-ide-e2e/src/tests/templates.test.ts @@ -19,7 +19,7 @@ const templatesToCheck = [ }, { value: "simpleEip7702", - displayName: "Simple EIP-7702", + displayName: "Simple EIP 7702", checkSelectors: ['*[data-id="treeViewLitreeViewItemcontracts/Example7702.sol"]'] }, { diff --git a/apps/remix-ide-e2e/src/types/index.d.ts b/apps/remix-ide-e2e/src/types/index.d.ts index ab7ba806881..f17f79813e9 100644 --- a/apps/remix-ide-e2e/src/types/index.d.ts +++ b/apps/remix-ide-e2e/src/types/index.d.ts @@ -1,6 +1,6 @@ // Merge custom command types with nightwatch types /* eslint-disable no-use-before-define */ -import {NightwatchBrowser} from 'nightwatch' // eslint-disable-line @typescript-eslint/no-unused-vars +import { NightwatchBrowser } from 'nightwatch' // eslint-disable-line @typescript-eslint/no-unused-vars export type callbackCheckVerifyCallReturnValue = (values: string[]) => {message: string; pass: boolean} declare module 'nightwatch' { @@ -25,7 +25,7 @@ declare module 'nightwatch' { journalLastChildIncludes(val: string): NightwatchBrowser executeScriptInTerminal(script: string): NightwatchBrowser clearEditableContent(cssSelector: string): NightwatchBrowser - journalChildIncludes(val: string, opts = {shouldHaveOnlyOneOccurrence: boolean}): NightwatchBrowser + journalChildIncludes(val: string, opts = { shouldHaveOnlyOneOccurrence: boolean }): NightwatchBrowser debugTransaction(index: number): NightwatchBrowser checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser openFile(name: string): NightwatchBrowser @@ -74,7 +74,11 @@ declare module 'nightwatch' { connectToExternalHttpProvider: (url: string, identifier: string) => NightwatchBrowser waitForElementNotContainsText: (id: string, value: string, timeout: number = 10000) => NightwatchBrowser hideToolTips: (this: NightwatchBrowser) => NightwatchBrowser - hidePopupPanel: (this: NightwatchBrowser) => NightwatchBrowser + // hidePopupPanel: (this: NightwatchBrowser) => NightwatchBrowser + assistantSetProvider: (provider: string) => NightwatchBrowser + assistantAddContext: (context: string) => NightwatchBrowser + assistantGenerate: (prompt: string, provider: string) => NightwatchBrowser + assistantWorkspace: (prompt: string, provider: string) => NightwatchBrowser enableClipBoard: () => NightwatchBrowser addFileSnekmate: (name: string, content: NightwatchContractContent) => NightwatchBrowser selectFiles: (selelectedElements: any[]) => NightwatchBrowser diff --git a/apps/remix-ide/src/app.ts b/apps/remix-ide/src/app.ts index e0e384e849d..93f1f237a5b 100644 --- a/apps/remix-ide/src/app.ts +++ b/apps/remix-ide/src/app.ts @@ -21,6 +21,7 @@ import { AstWalker } from '@remix-project/remix-astwalker' import { LinkLibraries, DeployLibraries, OpenZeppelinProxy } from '@remix-project/core-plugin' import { CodeParser } from './app/plugins/parser/code-parser' import { SolidityScript } from './app/plugins/solidity-script' +import { RemixAIAssistant } from './app/plugins/remix-ai-assistant' import { WalkthroughService } from './walkthroughService' @@ -314,6 +315,7 @@ class AppComponent { // ----------------- AI -------------------------------------- const remixAI = new RemixAIPlugin(isElectron()) + const remixAiAssistant = new RemixAIAssistant() // ----------------- import content service ------------------------ const contentImport = new CompilerImports() @@ -446,6 +448,7 @@ class AppComponent { templateSelection, scriptRunnerUI, remixAI, + remixAiAssistant, walletConnect ]) @@ -603,7 +606,8 @@ class AppComponent { 'contentImport', 'gistHandler', 'compilerloader', - 'remixAI' + 'remixAI', + 'remixaiassistant' ]) await this.appManager.activatePlugin(['settings']) diff --git a/apps/remix-ide/src/app/components/side-panel.tsx b/apps/remix-ide/src/app/components/side-panel.tsx index 8aa5183b446..01549e1d21f 100644 --- a/apps/remix-ide/src/app/components/side-panel.tsx +++ b/apps/remix-ide/src/app/components/side-panel.tsx @@ -70,7 +70,7 @@ export class SidePanel extends AbstractPanel { } async pinView (profile) { - await this.call('pinnedPanel', 'pinView', profile, this.plugins[profile.name].view) + await this.call('pinnedPanel', 'pinView', profile, this.plugins[profile.name]?.view) if (this.plugins[profile.name].active) this.call('menuicons', 'select', 'filePanel') super.remove(profile.name) this.renderComponent() @@ -93,6 +93,9 @@ export class SidePanel extends AbstractPanel { */ async showContent(name) { super.showContent(name) + if (name === 'remixaiassistant') { // TODO: should this be a plugin feature? + this.pinView(this.plugins['remixaiassistant'].profile) + } this.emit('focusChanged', name) this.renderComponent() } diff --git a/apps/remix-ide/src/app/components/vertical-icons.tsx b/apps/remix-ide/src/app/components/vertical-icons.tsx index 865c4fa4684..ecf336c99f7 100644 --- a/apps/remix-ide/src/app/components/vertical-icons.tsx +++ b/apps/remix-ide/src/app/components/vertical-icons.tsx @@ -29,7 +29,7 @@ export class VerticalIcons extends Plugin { } renderComponent() { - const fixedOrder = ['filePanel', 'search', 'solidity', 'udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager'] + const fixedOrder = ['remixaiassistant', 'filePanel', 'search', 'solidity', 'udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager'] const divived = Object.values(this.icons) .map((value) => { diff --git a/apps/remix-ide/src/app/panels/layout.ts b/apps/remix-ide/src/app/panels/layout.ts index 2ed6541d9eb..fb91f888fbe 100644 --- a/apps/remix-ide/src/app/panels/layout.ts +++ b/apps/remix-ide/src/app/panels/layout.ts @@ -34,10 +34,13 @@ export class Layout extends Plugin { maximized: { [key: string]: boolean } constructor () { super(profile) - this.maximized = {} + this.maximized = { + // 'remixaiassistant': true + } this.enhanced = { 'dgit': true, - 'LearnEth': true + 'LearnEth': true, + 'remixaiassistant': true } this.event = new EventEmitter() } diff --git a/apps/remix-ide/src/app/plugins/remix-ai-assistant.tsx b/apps/remix-ide/src/app/plugins/remix-ai-assistant.tsx new file mode 100644 index 00000000000..b41f8c53dd0 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/remix-ai-assistant.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { ViewPlugin } from '@remixproject/engine-web' +import * as packageJson from '../../../../../package.json' +import { PluginViewWrapper } from '@remix-ui/helper' +import { RemixUiRemixAiAssistant } from '@remix-ui/remix-ai-assistant' +import { EventEmitter } from 'events' +const profile = { + name: 'remixaiassistant', + displayName: 'Remix AI Assistant', + icon: 'assets/img/remixai-logoDefault.webp', + description: 'AI code assistant for Remix IDE', + kind: 'remixaiassistant', + location: 'sidePanel', + documentation: 'https://remix-ide.readthedocs.io/en/latest/run.html', + version: packageJson.version, + maintainedBy: 'Remix', + permission: true, + events: [], + methods: [] +} + +export class RemixAIAssistant extends ViewPlugin { + element: HTMLDivElement + dispatch: React.Dispatch = () => {} + event: any + constructor() { + super(profile) + this.event = new EventEmitter() + this.element = document.createElement('div') + this.element.setAttribute('id', 'remix-ai-assistant') + } + + async onActivation() { + const currentActivePlugin = await this.call('pinnedPanel', 'currentFocus') + if (currentActivePlugin === 'remixaiassistant') { + await this.call('sidePanel', 'pinView', profile) + await this.call('layout', 'maximiseSidePanel') + } + } + + onDeactivation() { + + } + + async makePluginCall (pluginName: string, methodName: string, payload: any) { + try { + const result = await this.call(pluginName, methodName, payload) + return result + } catch (error) { + if (pluginName === 'fileManager' && methodName === 'getCurrentFile') { + await this.call('notification', 'alert', 'No file is open') + return null + } + console.error(error) + return null + } + } + + setDispatch(dispatch: React.Dispatch) { + this.dispatch = dispatch + this.renderComponent() + } + + renderComponent() { + this.dispatch({ + ...this + }) + } + + render() { + return ( +
+ +
+ ) + } + + updateComponent(state: any) { + return ( + + ) + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx b/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx index d68c63336f2..b15e2ddf09b 100644 --- a/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx +++ b/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx @@ -2,11 +2,15 @@ import * as packageJson from '../../../../../package.json' import { ViewPlugin } from '@remixproject/engine-web' import { Plugin } from '@remixproject/engine'; import { RemixAITab, ChatApi } from '@remix-ui/remix-ai' +import { RemixAiAssistantChatApi } from '@remix-ui/remix-ai-assistant' import React, { useCallback } from 'react'; -import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, CodeExplainAgent, SecurityAgent } from '@remix/remix-ai-core'; +import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, AssistantParams, CodeExplainAgent, SecurityAgent } from '@remix/remix-ai-core'; import { CustomRemixApi } from '@remix-api' import { PluginViewWrapper } from '@remix-ui/helper' -import { CodeCompletionAgent, IContextType } from '@remix/remix-ai-core'; +import { AlertModal } from '@remix-ui/app'; +import { CodeCompletionAgent, ContractAgent, workspaceAgent, IContextType } from '@remix/remix-ai-core'; +import axios from 'axios'; +import { endpointUrls } from "@remix-endpoints-helper" const _paq = (window._paq = window._paq || []) type chatRequestBufferT = { @@ -17,9 +21,10 @@ const profile = { name: 'remixAI', displayName: 'RemixAI', methods: ['code_generation', 'code_completion', 'setContextFiles', - "solidity_answer", "code_explaining", - "code_insertion", "error_explaining", "vulnerability_check", - "initialize", 'chatPipe', 'ProcessChatRequestBuffer', 'isChatRequestPending'], + "solidity_answer", "code_explaining", "generateWorkspace", "fixWorspaceErrors", + "code_insertion", "error_explaining", "vulnerability_check", 'generate', + "initialize", 'chatPipe', 'ProcessChatRequestBuffer', 'isChatRequestPending', + 'resetChatRequestBuffer'], events: [], icon: 'assets/img/remix-logo-blue.png', description: 'RemixAI provides AI services to Remix IDE.', @@ -40,6 +45,9 @@ export class RemixAIPlugin extends ViewPlugin { chatRequestBuffer: chatRequestBufferT = null codeExpAgent: CodeExplainAgent securityAgent: SecurityAgent + contractor: ContractAgent + workspaceAgent: workspaceAgent + assistantProvider: string = 'mistralai' useRemoteInferencer:boolean = false dispatch: any completionAgent: CodeCompletionAgent @@ -47,7 +55,6 @@ export class RemixAIPlugin extends ViewPlugin { constructor(inDesktop:boolean) { super(profile) this.isOnDesktop = inDesktop - this.codeExpAgent = new CodeExplainAgent(this) // user machine dont use ressource for remote inferencing } @@ -66,6 +73,9 @@ export class RemixAIPlugin extends ViewPlugin { } this.completionAgent = new CodeCompletionAgent(this) this.securityAgent = new SecurityAgent(this) + this.codeExpAgent = new CodeExplainAgent(this) + this.contractor = ContractAgent.getInstance(this) + this.workspaceAgent = workspaceAgent.getInstance(this) } async initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean){ @@ -121,7 +131,9 @@ export class RemixAIPlugin extends ViewPlugin { } async solidity_answer(prompt: string, params: IParams=GenerationParams): Promise { - const newPrompt = await this.codeExpAgent.chatCommand(prompt) + let newPrompt = await this.codeExpAgent.chatCommand(prompt) + // add workspace context + newPrompt = !this.workspaceAgent.ctxFiles ? newPrompt : "Using the following context: ```\n" + this.workspaceAgent.ctxFiles + "```\n\n" + newPrompt let result if (this.isOnDesktop && !this.useRemoteInferencer) { @@ -174,6 +186,90 @@ export class RemixAIPlugin extends ViewPlugin { return this.securityAgent.getReport(file) } + /** + * Generates a new remix IDE workspace based on the provided user prompt, optionally using Retrieval-Augmented Generation (RAG) context. + * - If `useRag` is `true`, the function fetches additional context from a RAG API and prepends it to the user prompt. + */ + async generate(userPrompt: string, params: IParams=AssistantParams, newThreadID:string="", useRag:boolean=false): Promise { + params.stream_result = false // enforce no stream result + params.threadId = newThreadID + params.provider = this.assistantProvider + + if (useRag) { + try { + let ragContext = "" + const options = { headers: { 'Content-Type': 'application/json', } } + const response = await axios.post(endpointUrls.rag, { query: userPrompt, endpoint:"query" }, options) + if (response.data) { + ragContext = response.data.response + userPrompt = "Using the following context: ```\n\n" + JSON.stringify(ragContext) + "```\n\n" + userPrompt + } else { + console.log('Invalid response from RAG context API:', response.data) + } + } catch (error) { + console.log('RAG context error:', error) + } + } + // Evaluate if this function requires any context + // console.log('Generating code for prompt:', userPrompt, 'and threadID:', newThreadID) + let result + if (this.isOnDesktop && !this.useRemoteInferencer) { + result = await this.call(this.remixDesktopPluginName, 'generate', userPrompt, params) + } else { + result = await this.remoteInferencer.generate(userPrompt, params) + } + + const genResult = this.contractor.writeContracts(result, userPrompt) + return genResult + } + + /** + * Performs any user action on the entire curren workspace or updates the workspace based on a user prompt, optionally using Retrieval-Augmented Generation (RAG) for additional context. + * + */ + async generateWorkspace (userPrompt: string, params: IParams=AssistantParams, newThreadID:string="", useRag:boolean=false): Promise { + params.stream_result = false // enforce no stream result + params.threadId = newThreadID + params.provider = this.assistantProvider + if (useRag) { + try { + let ragContext = "" + const options = { headers: { 'Content-Type': 'application/json', } } + const response = await axios.post(endpointUrls.rag, { query: userPrompt, endpoint:"query" }, options) + if (response.data) { + ragContext = response.data.response + userPrompt = "Using the following context: ```\n\n" + ragContext + "```\n\n" + userPrompt + } + else { + console.log('Invalid response from RAG context API:', response.data) + } + } catch (error) { + console.log('RAG context error:', error) + } + } + const files = !this.workspaceAgent.ctxFiles ? await this.workspaceAgent.getCurrentWorkspaceFiles() : this.workspaceAgent.ctxFiles + userPrompt = "Using the following workspace context: ```\n" + files + "```\n\n" + userPrompt + + let result + if (this.isOnDesktop && !this.useRemoteInferencer) { + result = await this.call(this.remixDesktopPluginName, 'generateWorkspace', userPrompt, params) + } else { + result = await this.remoteInferencer.generateWorkspace(userPrompt, params) + } + return (result !== undefined) ? this.workspaceAgent.writeGenerationResults(result) : "### No Changes applied!" + } + + async fixWorspaceErrors(continueGeneration=false): Promise { + try { + if (continueGeneration) { + return this.contractor.continueCompilation() + } else { + return this.contractor.fixWorkspaceCompilationErrors(this.workspaceAgent) + } + } catch (error) { + } + } + async code_insertion(msg_pfx: string, msg_sfx: string): Promise { if (this.completionAgent.indexer == null || this.completionAgent.indexer == undefined) await this.completionAgent.indexWorkspace() @@ -194,12 +290,12 @@ export class RemixAIPlugin extends ViewPlugin { prompt: prompt, context: context } - if (pipeMessage) ChatApi.composer.send(pipeMessage) + if (pipeMessage) RemixAiAssistantChatApi.composer.send(pipeMessage) else { - if (fn === "code_explaining") ChatApi.composer.send("Explain the current code") - else if (fn === "error_explaining") ChatApi.composer.send("Explain the error") - else if (fn === "solidity_answer") ChatApi.composer.send("Answer the following question") - else if (fn === "vulnerability_check") ChatApi.composer.send("Is there any vulnerability in the pasted code?") + if (fn === "code_explaining") RemixAiAssistantChatApi.composer.send("Explain the current code") + else if (fn === "error_explaining") RemixAiAssistantChatApi.composer.send("Explain the error") + else if (fn === "solidity_answer") RemixAiAssistantChatApi.composer.send("Answer the following question") + else if (fn === "vulnerability_check") RemixAiAssistantChatApi.composer.send("Is there any vulnerability in the pasted code?") else console.log("chatRequestBuffer function name not recognized.") } } @@ -222,12 +318,17 @@ export class RemixAIPlugin extends ViewPlugin { } async setContextFiles(context: IContextType) { + this.workspaceAgent.setCtxFiles(context) } isChatRequestPending(){ return this.chatRequestBuffer != null } + resetChatRequestBuffer() { + this.chatRequestBuffer = null + } + setDispatch(dispatch) { this.dispatch = dispatch this.renderComponent() @@ -253,8 +354,8 @@ export class RemixAIPlugin extends ViewPlugin { } updateComponent(state) { - return ( - - ) + // return ( + // + // ) } } diff --git a/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx b/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx index 7e22e0d327a..cff87fd8a86 100644 --- a/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx +++ b/apps/remix-ide/src/app/plugins/templates-selection/templates-selection-plugin.tsx @@ -1,8 +1,8 @@ -import React from 'react' +import React, { useEffect, useRef } from 'react' import { FormattedMessage } from 'react-intl' import { CustomTooltip } from "@remix-ui/helper" -import { AppModal } from '@remix-ui/app' +import { AlertModal, AppModal } from '@remix-ui/app' import { ViewPlugin } from '@remixproject/engine-web' import { PluginViewWrapper } from '@remix-ui/helper' import { RemixUIGridView } from '@remix-ui/remix-ui-grid-view' @@ -12,6 +12,8 @@ import isElectron from 'is-electron' import type { Template, TemplateGroup } from '@remix-ui/workspace' import './templates-selection-plugin.css' import { templates } from './templates' +import { AssistantParams } from '@remix/remix-ai-core' +import { RemixAiAssistantChatApi } from '@remix-ui/remix-ai-assistant' import { TEMPLATE_METADATA } from '@remix-ui/workspace' //@ts-ignore @@ -22,8 +24,8 @@ const profile = { displayName: 'Template Selection', description: 'templateSelection', location: 'mainPanel', - methods: [], - events: [], + methods: ['aiWorkspaceGenerate'], + events: ['onTemplateSelectionResult'], maintainedBy: 'Remix', } @@ -31,6 +33,7 @@ export class TemplatesSelectionPlugin extends ViewPlugin { templates: Array dispatch: React.Dispatch = () => { } opts: any = {} + aiState: any = { prompt: '' } constructor() { super(profile) @@ -71,6 +74,56 @@ export class TemplatesSelectionPlugin extends ViewPlugin { }) } + async aiWorkspaceGenerate () { + const generateAIWorkspace = async () => { + const okAction = async () => { + RemixAiAssistantChatApi.composer.send( '/generate ' + this.aiState.prompt) + } + const aiTemplateModal: AppModal = { + id: 'TemplatesSelection', + title: window._intl.formatMessage({ id: !isElectron() ? 'filePanel.workspace.create': 'filePanel.workspace.create.desktop' }), + message: aiModalTemplate((value) => this.aiState.prompt = value), + okLabel: window._intl.formatMessage({ id: !isElectron() ? 'filePanel.ok':'filePanel.selectFolder' }), + okFn: okAction + } + const modalResult = await this.call('notification', 'modal', aiTemplateModal) + const alertModal: AlertModal = { + id: 'TemplatesSelectionAiAlert', + message:
+ Ai alert +

Your request is being processed. Please wait while I generate the workspace for you. It won't be long.

+
, + title: 'Generating Workspace' + } + this.on('remixAI', 'generateWorkspace', async () => { + await this.call('notification', 'alert', alertModal) + }) + if (modalResult === undefined) { + } + } + + const aiModalTemplate = (onChangeTemplateName: (value) => void) => { + return ( + <> +
+ + onChangeTemplateName(e.target.value)} + onInput={(e) => onChangeTemplateName((e.target as any).value)} + /> +
+ + ) + } + generateAIWorkspace() + } + updateComponent() { const errorCallback = async (error, data) => { @@ -95,6 +148,7 @@ export class TemplatesSelectionPlugin extends ViewPlugin { const gitNotSet = !username || !email let workspaceName = defaultName let initGit = false + this.opts = {} const modal: AppModal = { id: 'TemplatesSelection', @@ -113,6 +167,7 @@ export class TemplatesSelectionPlugin extends ViewPlugin { } const modalResult = await this.call('notification', 'modal', modal) + console.log('modalResult', modalResult) if (!modalResult) return _paq.push(['trackEvent', 'template-selection', 'createWorkspace', item.value]) this.emit('createWorkspaceReducerEvent', workspaceName, item.value, this.opts, false, errorCallback, initGit) @@ -234,7 +289,11 @@ export class TemplatesSelectionPlugin extends ViewPlugin { { - createWorkspace(item, template) + if ((item.value as string).toLowerCase().includes('ai')) { + this.aiWorkspaceGenerate() + } else { + createWorkspace(item, template) + } }} className="btn btn-sm mr-2 border border-primary" > diff --git a/apps/remix-ide/src/app/plugins/templates-selection/templates.ts b/apps/remix-ide/src/app/plugins/templates-selection/templates.ts index eddc77bbe51..a74ee8a66ab 100644 --- a/apps/remix-ide/src/app/plugins/templates-selection/templates.ts +++ b/apps/remix-ide/src/app/plugins/templates-selection/templates.ts @@ -7,9 +7,10 @@ export const templates = (intl: any, plugin: any): TemplateGroup[] => { items: [ { value: "remixDefault", tagList: ["Solidity"], displayName: intl.formatMessage({ id: 'filePanel.basic' }), description: 'The default project' }, { value: "blank", displayName: intl.formatMessage({ id: 'filePanel.blank' }), IsArtefact: true, description: 'A blank project' }, - { value: "simpleEip7702", displayName: 'Simple EIP-7702', IsArtefact: true, description: 'Pectra upgrade allowing externally owned accounts (EOAs) to run contract code' }, + { value: "simpleEip7702", displayName: 'Simple EIP 7702', IsArtefact: true, description: 'Pectra upgrade allowing externally owned accounts (EOAs) to run contract code.' }, + { value: "accountAbstraction", displayName: 'Account Abstraction', IsArtefact: true, description: 'A repo about ERC-4337 and EIP-7702' }, + { value: 'remixAiTemplate', tagList: ['AI'], displayName: 'RemixAI Template Generation', IsArtefact: true, description: 'AI generated workspace. Workspace gets generated with a user prompt.' }, { value: "introToEIP7702", displayName: 'Intro to EIP-7702', IsArtefact: true, description: 'A contract for demoing EIP-7702' }, - { value: "accountAbstraction", displayName: 'Account Abstraction', IsArtefact: true, description: 'A repo about ERC-4337 and EIP-7702' } ] }, { diff --git a/apps/remix-ide/src/assets/img/remixai-logoAI.webp b/apps/remix-ide/src/assets/img/remixai-logoAI.webp new file mode 100644 index 00000000000..5ddb40775a4 Binary files /dev/null and b/apps/remix-ide/src/assets/img/remixai-logoAI.webp differ diff --git a/apps/remix-ide/src/assets/img/remixai-logoDefault.webp b/apps/remix-ide/src/assets/img/remixai-logoDefault.webp new file mode 100644 index 00000000000..4dbf4abd774 Binary files /dev/null and b/apps/remix-ide/src/assets/img/remixai-logoDefault.webp differ diff --git a/apps/remix-ide/src/remixAppManager.ts b/apps/remix-ide/src/remixAppManager.ts index 2e048de25e0..d8b77efda57 100644 --- a/apps/remix-ide/src/remixAppManager.ts +++ b/apps/remix-ide/src/remixAppManager.ts @@ -91,7 +91,8 @@ let requiredModules = [ 'walletconnect', 'popupPanel', 'remixAI', - 'remixAID' + 'remixAID', + 'remixaiassistant' ] // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) @@ -153,7 +154,8 @@ export function isNative(name) { 'contract-verification', 'popupPanel', 'LearnEth', - 'noir-compiler' + 'noir-compiler', + 'remixaiassistant' ] return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) || isScriptRunner(name) } diff --git a/apps/remix-ide/src/types/index.d.ts b/apps/remix-ide/src/types/index.d.ts index 113a2fd2964..32510a61f0c 100644 --- a/apps/remix-ide/src/types/index.d.ts +++ b/apps/remix-ide/src/types/index.d.ts @@ -62,3 +62,93 @@ export interface IRemixAppManager { registeredPlugins(): Promise registerContextMenuItems(): Promise } + +export type PluginNames = 'manager' | + 'config' | + 'compilerArtefacts' | + 'compilerMetadata' | + 'compilerloader' | + 'contextualListener' | + 'editor' | + 'offsetToLineColumnConverter' | + 'network' | + 'theme' | + 'locale' | + 'fileManager' | + 'contentImport' | + 'blockchain' | + 'web3Provider' | + 'scriptRunner' | + 'scriptRunnerBridge' | + 'fetchAndCompile' | + 'mainPanel' | + 'hiddenPanel' | + 'sidePanel' | + 'menuicons' | + 'filePanel' | + 'terminal' | + 'statusBar' | + 'settings' | + 'pluginManager' | + 'tabs' | + 'udapp' | + 'dgitApi' | + 'solidity' | + 'solidity-logic' | + 'gistHandler' | + 'layout' | + 'notification' | + 'permissionhandler' | + 'walkthrough' | + 'storage' | + 'restorebackupzip' | + 'link-libraries' | + 'deploy-libraries' | + 'openzeppelin-proxy' | + 'hardhat-provider' | + 'ganache-provider' | + 'foundry-provider' | + 'basic-http-provider' | + 'vm-custom-fork' | + 'vm-goerli-fork' | + 'vm-mainnet-fork' | + 'vm-sepolia-fork' | + 'vm-paris' | + 'vm-london' | + 'vm-berlin' | + 'vm-shanghai' | + 'compileAndRun' | + 'search' | + 'recorder' | + 'fileDecorator' | + 'codeParser' | + 'codeFormatter' | + 'solidityumlgen' | + 'compilationDetails' | + 'vyperCompilationDetails' | + 'contractflattener' | + 'solidity-script' | + 'home' | + 'doc-viewer' | + 'doc-gen' | + 'remix-templates' | + 'remixAID' | + 'solhint' | + 'dgit' | + 'pinnedPanel' | + 'pluginStateLogger' | + 'environmentExplorer' | + 'templateSelection' | + 'matomo' | + 'walletconnect' | + 'popupPanel' | + 'remixAI' | + 'remixAID' | + 'remixaiassistant' | + 'doc-gen' | + 'doc-viewer' | + 'contract-verification' | + 'vyper' | + 'solhint' | + 'circuit-compiler' | + 'learneth' \ No newline at end of file diff --git a/apps/remixdesktop/src/lib/InferenceServerManager.ts b/apps/remixdesktop/src/lib/InferenceServerManager.ts index 4ae69100134..1f6dd6a5784 100644 --- a/apps/remixdesktop/src/lib/InferenceServerManager.ts +++ b/apps/remixdesktop/src/lib/InferenceServerManager.ts @@ -526,4 +526,16 @@ export class InferenceManager implements ICompletions { } } + async generate(userPrompt, options:IParams=GenerationParams): Promise { + const payload = { prompt: userPrompt, "endpoint":"generate", ...options } + if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL) + else return this._makeRequest(payload, AIRequestType.GENERAL) + } + + async generateWorkspace(userPrompt, options:IParams=GenerationParams): Promise { + const payload = { prompt: userPrompt, "endpoint":"workspace", ...options } + if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL) + else return this._makeRequest(payload, AIRequestType.GENERAL) + } + } diff --git a/apps/remixdesktop/src/plugins/remixAIDektop.ts b/apps/remixdesktop/src/plugins/remixAIDektop.ts index e8b11199694..9dbb35d1384 100644 --- a/apps/remixdesktop/src/plugins/remixAIDektop.ts +++ b/apps/remixdesktop/src/plugins/remixAIDektop.ts @@ -32,7 +32,7 @@ const clientProfile: Profile = { description: 'RemixAI provides AI services to Remix IDE Desktop.', kind: '', documentation: 'https://remix-ide.readthedocs.io/en/latest/ai.html', - methods: ['initializeModelBackend', 'code_completion', 'code_insertion', 'code_generation', 'code_explaining', 'error_explaining', 'solidity_answer'] + methods: ['initializeModelBackend', 'code_completion', 'code_insertion', 'code_generation', 'code_explaining', 'error_explaining', 'solidity_answer', 'generate'] } class RemixAIDesktopPluginClient extends ElectronBasePluginClient { @@ -107,6 +107,10 @@ class RemixAIDesktopPluginClient extends ElectronBasePluginClient { return this.desktopInferencer.solidity_answer(prompt) } + async generate(userPrompt): Promise { + return this.desktopInferencer.generate(userPrompt) + } + changemodel(newModel: any){ /// dereference the current static inference object /// set new one diff --git a/libs/remix-ai-core/src/agents/contractAgent.ts b/libs/remix-ai-core/src/agents/contractAgent.ts new file mode 100644 index 00000000000..e891103110f --- /dev/null +++ b/libs/remix-ai-core/src/agents/contractAgent.ts @@ -0,0 +1,153 @@ +import { AssistantParams } from "../types/models"; +import { workspaceAgent } from "./workspaceAgent"; +import { CompilationResult } from "../types/types"; +import { compilecontracts } from "../helpers/compile"; + +const COMPILATION_WARNING_MESSAGE = '⚠️**Warning**: The compilation failed. Please check the compilation errors in the Remix IDE. Enter `/continue` or `/c` if you want Remix AI to try again until a compilable solution is generated?' + +export class ContractAgent { + plugin: any; + readonly generationAttempts: number = 5 + nAttempts: number = 0 + generationThreadID: string= '' + workspaceName: string = '' + contracts: any = {} + performCompile: boolean = false + overrideWorkspace: boolean = false + static instance + oldPayload: any = undefined + mainPrompt: string + + private constructor(props) { + this.plugin = props; + AssistantParams.provider = this.plugin.assistantProvider + } + + public static getInstance(props) { + if (ContractAgent.instance) return ContractAgent.instance + ContractAgent.instance = new ContractAgent(props) + return ContractAgent.instance + } + + /* + * Write the result of the generation to the workspace. Compiles the contracts one time and creates a new workspace. + * @param payload - The payload containing the generated files + * @param userPrompt - The user prompt used to generate the files + */ + async writeContracts(payload, userPrompt) { + const currentWorkspace = await this.plugin.call('filePanel', 'getCurrentWorkspace') + + const writeAIResults = async (parsedResults) => {await this.createWorkspace(this.workspaceName) + if (!this.overrideWorkspace) await this.plugin.call('filePanel', 'switchToWorkspace', { name: this.workspaceName, isLocalHost: false }) + const dirCreated = [] + for (const file of parsedResults.files) { + const dir = file.fileName.split('/').slice(0, -1).join('/') + if (!dirCreated.includes(dir) && dir) { + await this.plugin.call('fileManager', 'mkdir', dir) + dirCreated.push(dir) + } + // check if file already exists + await this.plugin.call('fileManager', 'writeFile', file.fileName, file.content) + await this.plugin.call('codeFormatter', 'format', file.fileName) + } + return "New workspace created: **" + this.workspaceName + "**\nUse the Hamburger menu to select it!" + } + + try { + if (payload === undefined) { + this.nAttempts += 1 + if (this.nAttempts > this.generationAttempts) { + this.performCompile = false + if (this.oldPayload) { + return await writeAIResults(this.oldPayload) + } + return "Max attempts reached! Please try again with a different prompt." + } + return "No payload, try again while considering changing the assistant provider with the command `/setAssistant `" + } + this.contracts = {} + const parsedFiles = payload + this.oldPayload = payload + this.generationThreadID = parsedFiles['threadID'] + this.workspaceName = parsedFiles['projectName'] + + this.nAttempts += 1 + if (this.nAttempts === 1) this.mainPrompt = userPrompt + + if (this.nAttempts > this.generationAttempts) { + return await writeAIResults(parsedFiles) + } + + for (const file of parsedFiles.files) { + if (file.fileName.endsWith('.sol')) { + this.contracts[file.fileName] = { content: file.content } + } + } + + const result:CompilationResult = await compilecontracts(this.contracts, this.plugin) + if (!result.compilationSucceeded && this.performCompile) { + // console.log('Compilation failed, trying again recursively ...') + const newPrompt = `Payload:\n${JSON.stringify(result.errfiles)}}\n\nWhile considering this compilation error: Here is the error message\n. Try this again:${this.mainPrompt}\n ` + return await this.plugin.generate(newPrompt, AssistantParams, this.generationThreadID); // reuse the same thread + } + + return result.compilationSucceeded ? await writeAIResults(parsedFiles) : await writeAIResults(parsedFiles) + "\n\n" + COMPILATION_WARNING_MESSAGE + } catch (error) { + this.deleteWorkspace(this.workspaceName ) + this.nAttempts = 0 + await this.plugin.call('filePanel', 'switchToWorkspace', currentWorkspace) + return "Failed to generate secure code on user prompt! Please try again with a different prompt." + } finally { + this.nAttempts = 0 + } + } + + async createWorkspace(workspaceName) { + // create random workspace surfixed with timestamp + const timestamp = new Date().getTime() + const wsp_name = workspaceName + '-' + timestamp + await this.plugin.call('filePanel', 'createWorkspace', wsp_name, true) + this.workspaceName = wsp_name + } + + async deleteWorkspace(workspaceName) { + await this.plugin.call('filePanel', 'deleteWorkspace', workspaceName) + } + + async fixWorkspaceCompilationErrors(wspAgent:workspaceAgent) { + try { + const wspfiles = JSON.parse(await wspAgent.getCurrentWorkspaceFiles()) + const compResult:CompilationResult = await compilecontracts(wspfiles, this.plugin) + // console.log('fix workspace Compilation result:', compResult) + + if (compResult.compilationSucceeded) { + console.log('Compilation succeeded, no errors to fix') + return 'Compilation succeeded, no errors to fix' + } + + const newPrompt = `Payload:\n${JSON.stringify(compResult.errfiles)}}\n\n Fix the compilation errors above\n` + return await this.plugin.generateWorkspace(newPrompt, AssistantParams, this.generationThreadID); // reuse the same thread, pass the paylod to the diff checker + + } catch (error) { + } finally { + } + } + + async continueCompilation(){ + try { + if (this.oldPayload === undefined) { + return "No payload, try again while considering changing the assistant provider with the command `/setAssistant `" + } + + this.performCompile = true + this.overrideWorkspace = true + return await this.writeContracts(this.oldPayload, this.mainPrompt) + } catch (error) { + return "Error during continue compilation. Please try again." + } finally { + this.performCompile = false + this.overrideWorkspace = false + } + } + +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/agents/workspaceAgent.ts b/libs/remix-ai-core/src/agents/workspaceAgent.ts new file mode 100644 index 00000000000..8fa51b2fadd --- /dev/null +++ b/libs/remix-ai-core/src/agents/workspaceAgent.ts @@ -0,0 +1,92 @@ +import { IContextType } from "../types/types"; + +enum SupportedFileExtensions { + solidity = '.sol', + vyper = '.vy', + circom = '.circom', + tests_ts = '.test.ts', + tests_js = '.test.js', +} + +export class workspaceAgent { + plugin: any + currentWorkspace: string = '' + static instance + ctxFiles:any + + private constructor(props) { + this.plugin = props; + } + + public static getInstance(props) { + if (workspaceAgent.instance) return workspaceAgent.instance + workspaceAgent.instance = new workspaceAgent(props) + return workspaceAgent.instance + } + + async getCurrentWorkspaceFiles() { + try { + let files = '{\n' + const jsonDirsContracts = await this.plugin.call('fileManager', 'copyFolderToJson', '/').then((res) => res.contracts); + for (const file in jsonDirsContracts.children) { + if (!Object.values(SupportedFileExtensions).some(ext => file.endsWith(ext))) continue; + + files += `"${file}": ${JSON.stringify(jsonDirsContracts.children[file].content)}},` + } + return files + '\n}' + } catch (error) { console.error('Error getting current workspace files:', error); } + } + + async writeGenerationResults(payload) { + try { + let modifiedFilesMarkdown = '## Modified Files\n' + for (const file of payload.files) { + if (!Object.values(SupportedFileExtensions).some(ext => file.fileName.endsWith(ext))) continue; + await this.plugin.call('fileManager', 'writeFile', file.fileName, file.content); + // await this.plugin.call('codeFormatter', 'format', fileName); + modifiedFilesMarkdown += `- ${file.fileName}\n` + } + return modifiedFilesMarkdown + } catch (error) { + console.error('Error writing generation results:', error); + return 'No files modified' + } + } + + async setCtxFiles (context: IContextType) { + this.ctxFiles = "" + switch (context.context) { + case 'currentFile': { + const file = await this.plugin.call('fileManager', 'getCurrentFile') + const content = await this.plugin.call('fileManager', 'readFile', file) + this.ctxFiles = `"${file}": ${JSON.stringify(content)}` + break + } + case 'workspace': + this.ctxFiles = await this.getCurrentWorkspaceFiles() + break + case 'openedFiles': { + this.ctxFiles = "{\n" + const openedFiles = await this.plugin.call('fileManager', 'getOpenedFiles') + Object.keys(openedFiles).forEach(key => { + if (!Object.values(SupportedFileExtensions).some(ext => key.endsWith(ext))) return; + this.ctxFiles += `"${key}": ${JSON.stringify(openedFiles[key])},\n` + }); + this.ctxFiles += "\n}" + break + } + default: + console.log('Invalid context type') + this.ctxFiles = "" + break + } + + if (context.files){ + for (const file of context.files) { + if (!Object.values(SupportedFileExtensions).some(ext => file.fileName.endsWith(ext))) continue; + this.ctxFiles += `"${file.fileName}": ${JSON.stringify(file.content)},\n` + } + + } + } +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/helpers/chatCommandParser.ts b/libs/remix-ai-core/src/helpers/chatCommandParser.ts new file mode 100644 index 00000000000..fa82ff09d05 --- /dev/null +++ b/libs/remix-ai-core/src/helpers/chatCommandParser.ts @@ -0,0 +1,138 @@ +import { isOllamaAvailable, listModels } from "../inferencers/local/ollama"; +import { OllamaInferencer } from "../inferencers/local/ollamaInferencer"; +import { GenerationParams } from "../types/models"; + +type CommandHandler = (args: string, reference: any) => void; + +export class ChatCommandParser { + private commands: Map = new Map(); + private props: any + + constructor(props: any) { + this.registerDefaultCommands(); + this.props = props; + } + + private registerDefaultCommands() { + this.register("@generate", this.handleGenerate); + this.register("@workspace", this.handleWorkspace); + this.register("@setAssistant", this.handleAssistant); + this.register("@ollama", this.handleOllama); + this.register("/generate", this.handleGenerate); + this.register("/g", this.handleGenerate); + this.register("/workspace", this.handleWorkspace); + this.register("/w", this.handleWorkspace); + this.register("/setAssistant", this.handleAssistant); + this.register("/continue", this.handleContinueGeneration); + this.register("/c", this.handleContinueGeneration); + // Add more default commands as needed + } + + public register(command: string, handler: CommandHandler) { + this.commands.set(command.toLowerCase(), handler); + } + + public async parse(input: string) { + const commandPattern = /^[@/](\w{1,})\s*(.*)/; + const match = input.match(commandPattern); + + if (!match) { + return ""; + } + + const commandName = `/${match[1].toLowerCase()}`; + const rawArgs = match[2].trim(); + const strippedInput = input.slice(match[0].indexOf(match[2])).trim(); // remove command prefix + + const handler = this.commands.get(commandName); + if (handler) { + return handler(rawArgs, this); + } else { + console.log(`Unknown command: ${commandName}`); + return ""; + } + } + + private async handleGenerate(prompt: string, ref) { + try { + GenerationParams.return_stream_response = false + GenerationParams.stream_result = false + return await ref.props.call('remixAI', 'generate', "generate " + prompt, GenerationParams, "", true); + } catch (error) { + return "Generation failed. Please try again."; + } + } + + private async handleWorkspace(prompt: string, ref) { + try { + GenerationParams.return_stream_response = false + GenerationParams.stream_result = false + return await ref.props.call('remixAI', 'generateWorkspace', prompt, GenerationParams, "", false); + } catch (error) { + return "Workspace generation failed. Please try again."; + } + } + + private async handleContinueGeneration(prompt: string, ref) { + try { + GenerationParams.return_stream_response = false + GenerationParams.stream_result = false + return await ref.props.call('remixAI', 'fixWorspaceErrors', true); + } catch (error) { + return "Error while generating. Please try again."; + } + } + + private async handleAssistant(provider: string, ref: { props: { assistantProvider: string; }; }) { + if (provider === 'openai' || provider === 'mistralai' || provider === 'anthropic') { + ref.props.assistantProvider = provider + return "AI Provider set to `" + provider + "` successfully! " + } else { + return "Invalid AI Provider. Please use `openai`, `mistralai`, or `anthropic`." + } + } + + private async handleOllama(prompt: string, ref: any) { + try { + if (prompt === "start") { + const available = await isOllamaAvailable(); + if (!available) { + return '❌ Ollama is not available. Consider enabling the (Ollama CORS)[https://objectgraph.com/blog/ollama-cors/]' + } + const models = await listModels(); + const res = "Available models: " + models.map((model: any) => `\`${model}\``).join("\n"); + return res + "\n\nOllama is now set up. You can use the command `/ollama select ` to start a conversation with a specific model. Make sure the model is being run on your local machine. See ollama run for more details."; + } else if (prompt.trimStart().startsWith("select")) { + const model = prompt.split(" ")[1]; + if (!model) { + return "Please provide a model name to select."; + } + const available = await isOllamaAvailable(); + if (!available) { + return '❌ Ollama is not available. Consider enabling the (Ollama CORS)[https://objectgraph.com/blog/ollama-cors/]' + } + const models = await listModels(); + if (models.includes(model)) { + // instantiate ollama in remixai + ref.props.remoteInferencer = new OllamaInferencer() + ref.props.remoteInferencer.event.on('onInference', () => { + ref.props.isInferencing = true + }) + ref.props.remoteInferencer.event.on('onInferenceDone', () => { + ref.props.isInferencing = false + }) + return `Model set to \`${model}\`. You can now start chatting with it.`; + } else { + return `Model \`${model}\` is not available. Please check the list of available models.`; + } + } else if (prompt === "stop") { + return "Ollama generation stopped."; + } else { + return "Invalid command. Use `/ollama start` to initialize Ollama, `/ollama select ` to select a model, or `/ollama stop` to stop the generation."; + } + } catch (error) { + return "Ollama generation failed. Please try again."; + } + } +} + diff --git a/libs/remix-ai-core/src/helpers/compile.ts b/libs/remix-ai-core/src/helpers/compile.ts new file mode 100644 index 00000000000..7dc2a5445c5 --- /dev/null +++ b/libs/remix-ai-core/src/helpers/compile.ts @@ -0,0 +1,68 @@ +import { CompilationResult } from '../types/types' + +const compilationParams = { + optimize: false, + evmVersion: null, + language: 'Solidity', + version: '0.8.29+commit.ab55807c' +} + +export const compilecontracts = async (contracts, plugin): Promise => { + // do not compile tests files + try { + // console.log('Compiling contracts:', contracts) + const result = await plugin.call('solidity' as any, 'compileWithParameters', contracts, compilationParams) + console.log('Compilation result:', result) + const data = result.data + let error = false + + if (data.errors) { + // console.log('Compilation errors:', data.errors) + error = data.errors.find((error) => error.type !== 'Warning') + } + + const errorFiles:{ [key: string]: any } = {}; + const fillErrors = (err) => { + if (errorFiles[err.sourceLocation.file]) { + errorFiles[err.sourceLocation.file].errors.push({ + errorStart : err.sourceLocation.start, + errorEnd : err.sourceLocation.end, + errorMessage : err.formattedMessage + }) + } else { + errorFiles[err.sourceLocation.file] = { + content : contracts[err.sourceLocation.file].content, + errors : [{ + errorStart : err.sourceLocation.start, + errorEnd : err.sourceLocation.end, + errorMessage : err.formattedMessage + }] + } + } + } + if (data.errors && data.errors.length && error) { + for (const error of data.errors) { + if (error.type === 'Warning') continue + fillErrors(error); + } + + const msg = ` + - Compilation errors: ${data.errors.map((e) => e.formattedMessage)}. + ` + return { compilationSucceeded: false, errors: msg, errfiles: errorFiles } + } + + if (data.error) { + errorFiles['contracts'] = contracts + errorFiles['error'] = data.error + const msg = ` + - Compilation errors: ${data.error}. + ` + return { compilationSucceeded: false, errors: msg, errfiles: errorFiles } + } + + return { compilationSucceeded: true, errors: null } + } catch (err) { + return { compilationSucceeded: false, errors: 'An unexpected error occurred during compilation.' } + } +} diff --git a/libs/remix-ai-core/src/index.ts b/libs/remix-ai-core/src/index.ts index d24ac6abae6..77b9e908a15 100644 --- a/libs/remix-ai-core/src/index.ts +++ b/libs/remix-ai-core/src/index.ts @@ -4,18 +4,18 @@ import { IModel, IModelResponse, IModelRequest, InferenceModel, ICompletions, IParams, ChatEntry, AIRequestType, IRemoteModel, RemoteBackendOPModel, IStreamResponse } from './types/types' import { ModelType } from './types/constants' -import { DefaultModels, InsertionParams, CompletionParams, GenerationParams } from './types/models' +import { DefaultModels, InsertionParams, CompletionParams, GenerationParams, AssistantParams } from './types/models' import { getCompletionPrompt, getInsertionPrompt } from './prompts/completionPrompts' import { buildSolgptPrompt, PromptBuilder } from './prompts/promptBuilder' import { RemoteInferencer } from './inferencers/remote/remoteInference' import { ChatHistory } from './prompts/chat' import { downloadLatestReleaseExecutable } from './helpers/inferenceServerReleases' - +import { ChatCommandParser } from './helpers/chatCommandParser' export { - IModel, IModelResponse, IModelRequest, InferenceModel, + IModel, IModelResponse, IModelRequest, InferenceModel, ChatCommandParser, ModelType, DefaultModels, ICompletions, IParams, IRemoteModel, getCompletionPrompt, getInsertionPrompt, IStreamResponse, buildSolgptPrompt, - RemoteInferencer, InsertionParams, CompletionParams, GenerationParams, + RemoteInferencer, InsertionParams, CompletionParams, GenerationParams, AssistantParams, ChatEntry, AIRequestType, RemoteBackendOPModel, ChatHistory, downloadLatestReleaseExecutable } @@ -24,3 +24,5 @@ export * from './helpers/streamHandler' export * from './agents/codeExplainAgent' export * from './agents/completionAgent' export * from './agents/securityAgent' +export * from './agents/contractAgent' +export * from './agents/workspaceAgent' diff --git a/libs/remix-ai-core/src/inferencers/local/ollama.ts b/libs/remix-ai-core/src/inferencers/local/ollama.ts new file mode 100644 index 00000000000..39e564dc1c4 --- /dev/null +++ b/libs/remix-ai-core/src/inferencers/local/ollama.ts @@ -0,0 +1,39 @@ +import axios from 'axios'; + +const OLLAMA_HOST = 'http://localhost:11434'; + +export async function isOllamaAvailable(): Promise { + try { + const res = await axios.get(`${OLLAMA_HOST}/api/tags`); + return res.status === 200; + } catch (error) { + return false; + } +} + +export async function listModels(): Promise { + const res = await axios.get(`${OLLAMA_HOST}/api/tags`); + return res.data.models.map((model: any) => model.name); +} + +export async function setSystemPrompt(model: string, prompt: string): Promise { + const payload = { + model, + system: prompt, + messages: [], + }; + const res = await axios.post(`${OLLAMA_HOST}/api/chat`, payload); + return res.data; +} + +export async function chatWithModel(model: string, systemPrompt: string, userMessage: string): Promise { + const payload = { + model, + system: systemPrompt, + messages: [ + { role: 'user', content: userMessage } + ], + }; + const res = await axios.post(`${OLLAMA_HOST}/api/chat`, payload); + return res.data.message?.content || '[No response]'; +} diff --git a/libs/remix-ai-core/src/inferencers/local/ollamaInferencer.ts b/libs/remix-ai-core/src/inferencers/local/ollamaInferencer.ts new file mode 100644 index 00000000000..c3bd9462686 --- /dev/null +++ b/libs/remix-ai-core/src/inferencers/local/ollamaInferencer.ts @@ -0,0 +1,126 @@ +import { AIRequestType, ICompletions, IGeneration, IParams } from "../../types/types"; +import { CompletionParams, GenerationParams } from "../../types/models"; +import EventEmitter from "events"; +import { ChatHistory } from "../../prompts/chat"; +import { isOllamaAvailable } from "./ollama"; +import axios from "axios"; +import { RemoteInferencer } from "../remote/remoteInference"; + +const defaultErrorMessage = `Unable to get a response from Ollama server`; + +export class OllamaInferencer extends RemoteInferencer implements ICompletions { + ollama_api_url: string = "http://localhost:11434/api/generate"; + model_name: string = "llama2:13b"; // Default model + + constructor(modelName?: string) { + super(); + this.api_url = this.ollama_api_url; + this.model_name = modelName || this.model_name; + } + + override async _makeRequest(payload: any, rType:AIRequestType): Promise { + this.event.emit("onInference"); + payload['stream'] = false; + payload['model'] = this.model_name; + console.log("calling _makeRequest Ollama API URL:", this.api_url); + try { + const result = await axios.post(this.api_url, payload, { + headers: { "Content-Type": "application/json" }, + }); + + if (result.status === 200) { + const text = result.data.message?.content || ""; + return text; + } else { + return defaultErrorMessage; + } + } catch (e: any) { + console.error("Error making Ollama request:", e.message); + return defaultErrorMessage; + } finally { + this.event.emit("onInferenceDone"); + } + } + + override async _streamInferenceRequest(payload: any, rType:AIRequestType) { + this.event.emit("onInference"); + payload['model'] = this.model_name; + console.log("payload in stream request", payload); + console.log("calling _streammakeRequest Ollama API URL:", this.api_url); + + const response = await fetch(this.api_url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + stream: true, + model: this.model_name, + messages: [{ role: "user", content: payload.prompt }], + }), + }); + + console.log("response in stream request", response); + // if (payload.return_stream_response) { + // return response + // } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + let resultText = ""; + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + console.log("chunk", chunk); + resultText += chunk; + this.event.emit("onStreamResult", chunk); + } + return resultText; + } catch (e: any) { + console.error("Streaming error from Ollama:", e.message); + return defaultErrorMessage; + } finally { + this.event.emit("onInferenceDone"); + } + } + + private _buildPayload(prompt: string, system?: string) { + return { + model: this.model_name, + system: system || "You are a helpful assistant.", + messages: [{ role: "user", content: prompt }], + }; + } + + // async code_completion(context: any, ctxFiles: any, fileName: any, options: IParams = CompletionParams) { + // } + + // async code_insertion(prompt: string, options: IParams = GenerationParams) { + // } + + // async code_generation(prompt: string, options: IParams = GenerationParams) { + // } + + // async generate(userPrompt: string, options: IParams = GenerationParams): Promise { + // } + + // async generateWorkspace(prompt: string, options: IParams = GenerationParams): Promise { + // } + + // async solidity_answer(prompt: string, options: IParams = GenerationParams): Promise { + // } + + // async code_explaining(prompt, context:string="", options:IParams=GenerationParams): Promise { + // } + + // async error_explaining(prompt, options:IParams=GenerationParams): Promise { + + // } + + // async vulnerability_check(prompt: string, options: IParams = GenerationParams): Promise { + // } +} diff --git a/libs/remix-ai-core/src/inferencers/remote/remoteInference.ts b/libs/remix-ai-core/src/inferencers/remote/remoteInference.ts index 8bbdab66596..9d5bb061fdd 100644 --- a/libs/remix-ai-core/src/inferencers/remote/remoteInference.ts +++ b/libs/remix-ai-core/src/inferencers/remote/remoteInference.ts @@ -1,4 +1,4 @@ -import { ICompletions, IParams, AIRequestType, RemoteBackendOPModel, JsonStreamParser } from "../../types/types"; +import { ICompletions, IGeneration, IParams, AIRequestType, RemoteBackendOPModel, JsonStreamParser } from "../../types/types"; import { GenerationParams, CompletionParams, InsertionParams } from "../../types/models"; import { buildSolgptPrompt } from "../../prompts/promptBuilder"; import EventEmitter from "events"; @@ -7,7 +7,7 @@ import axios from 'axios'; import { endpointUrls } from "@remix-endpoints-helper" const defaultErrorMessage = `Unable to get a response from AI server` -export class RemoteInferencer implements ICompletions { +export class RemoteInferencer implements ICompletions, IGeneration { api_url: string completion_url: string max_history = 7 @@ -22,7 +22,7 @@ export class RemoteInferencer implements ICompletions { this.event = new EventEmitter() } - private async _makeRequest(payload, rType:AIRequestType){ + async _makeRequest(payload, rType:AIRequestType){ this.event.emit("onInference") const requestURL = rType === AIRequestType.COMPLETION ? this.completion_url : this.api_url @@ -57,7 +57,7 @@ export class RemoteInferencer implements ICompletions { } } - private async _streamInferenceRequest(payload, rType:AIRequestType){ + async _streamInferenceRequest(payload, rType:AIRequestType){ let resultText = "" try { this.event.emit('onInference') @@ -153,4 +153,16 @@ export class RemoteInferencer implements ICompletions { if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL) else return this._makeRequest(payload, AIRequestType.GENERAL) } + + async generate(userPrompt, options:IParams=GenerationParams): Promise { + const payload = { prompt: userPrompt, "endpoint":"generate", ...options } + if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL) + else return this._makeRequest(payload, AIRequestType.GENERAL) + } + + async generateWorkspace(userPrompt, options:IParams=GenerationParams): Promise { + const payload = { prompt: userPrompt, "endpoint":"workspace", ...options } + if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL) + else return this._makeRequest(payload, AIRequestType.GENERAL) + } } diff --git a/libs/remix-ai-core/src/types/models.ts b/libs/remix-ai-core/src/types/models.ts index af64d2cb7ed..e564216c053 100644 --- a/libs/remix-ai-core/src/types/models.ts +++ b/libs/remix-ai-core/src/types/models.ts @@ -70,6 +70,8 @@ const InsertionParams:IParams = { topP: 0.92, max_new_tokens: 150, stream_result: false, + stream: false, + model: "", } const GenerationParams:IParams = { @@ -78,8 +80,13 @@ const GenerationParams:IParams = { topP: 0.92, max_new_tokens: 2000, stream_result: false, + stream: false, + model: "", repeat_penalty: 1.2, terminal_output: false, } -export { DefaultModels, CompletionParams, InsertionParams, GenerationParams } +const AssistantParams:IParams = GenerationParams +AssistantParams.provider = 'openai' + +export { DefaultModels, CompletionParams, InsertionParams, GenerationParams, AssistantParams } diff --git a/libs/remix-ai-core/src/types/remix-project.code-workspace b/libs/remix-ai-core/src/types/remix-project.code-workspace deleted file mode 100644 index 01fe49386a3..00000000000 --- a/libs/remix-ai-core/src/types/remix-project.code-workspace +++ /dev/null @@ -1,10 +0,0 @@ -{ - "folders": [ - { - "path": "../../../.." - }, - { - "path": "../../../../../remix-wildcard" - } - ] -} \ No newline at end of file diff --git a/libs/remix-ai-core/src/types/types.ts b/libs/remix-ai-core/src/types/types.ts index e2244c9b0d8..87b2c19c54a 100644 --- a/libs/remix-ai-core/src/types/types.ts +++ b/libs/remix-ai-core/src/types/types.ts @@ -11,7 +11,8 @@ export interface IModelRequirements{ } export interface IContextType { - context: 'currentFile' | 'workspace'|'openedFiles' + context: 'currentFile' | 'workspace'|'openedFiles' | 'none' + files?: { fileName: string; content: string }[] } export interface IModel { @@ -57,6 +58,15 @@ export interface ICompletions{ code_completion(context, ctxFiles, fileName, params:IParams): Promise; code_insertion(msg_pfx, msg_sfx, ctxFiles, fileName, params:IParams): Promise; } +export interface IGeneration{ + code_generation(prompt, params:IParams): Promise; + code_explaining(prompt, context:string, params:IParams): Promise; + error_explaining(prompt, params:IParams): Promise; + solidity_answer(prompt, params:IParams): Promise; + generate(prompt, params:IParams): Promise; + generateWorkspace(prompt, params:IParams): Promise; + vulnerability_check(prompt, params:IParams): Promise; +} export interface IParams { temperature?: number; @@ -77,6 +87,10 @@ export interface IParams { temp?: number; return_stream_response?: boolean; terminal_output?: boolean; + threadId?: string; + provider?: string; + stream?: boolean; + model?: string; } export enum AIRequestType { @@ -136,3 +150,9 @@ export class JsonStreamParser { return JSON.parse(this.buffer); } } + +export interface CompilationResult { + compilationSucceeded: boolean + errors: string + errfiles?: { [key: string]: any } +} diff --git a/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx b/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx index 7bab81df793..184b4f943fc 100644 --- a/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/dragbar/dragbar.tsx @@ -126,7 +126,6 @@ const DragBar = (props: IRemixDragBarUi) => { }, 300) } } - } function startDrag() { diff --git a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx index 0c4a58ffa3f..b099c33a585 100644 --- a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx @@ -280,7 +280,7 @@ const RemixApp = (props: IRemixAppUi) => { }
{props.app.hiddenPanel.render()}
-
{props.app.popupPanel.render()}
+ {/*
{props.app.popupPanel.render()}
*/}
{props.app.statusBar.render()}
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 a557bfda5d9..81c2b3b5599 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -9,7 +9,6 @@ import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solid import { cairoTokensProvider, cairoLanguageConfig } from './syntaxes/cairo' import { zokratesTokensProvider, zokratesLanguageConfig } from './syntaxes/zokrates' import { moveTokenProvider, moveLanguageConfig } from './syntaxes/move' -import { vyperTokenProvider, vyperLanguageConfig } from './syntaxes/vyper' import { tomlLanguageConfig, tomlTokenProvider } from './syntaxes/toml' import { monacoTypes } from '@remix-ui/editor' import { loadTypes } from './web-types' @@ -372,8 +371,6 @@ export const EditorUI = (props: EditorUIProps) => { monacoRef.current.editor.setModelLanguage(file.model, 'remix-toml') } else if (file.language === 'noir') { monacoRef.current.editor.setModelLanguage(file.model, 'remix-noir') - } else if (file.language === 'python') { - monacoRef.current.editor.setModelLanguage(file.model, 'remix-vyper') } }, [props.currentFile, props.isDiff]) @@ -1031,7 +1028,6 @@ export const EditorUI = (props: EditorUIProps) => { monacoRef.current.languages.register({ id: 'remix-zokrates' }) monacoRef.current.languages.register({ id: 'remix-move' }) monacoRef.current.languages.register({ id: 'remix-circom' }) - monacoRef.current.languages.register({ id: 'remix-vyper' }) monacoRef.current.languages.register({ id: 'remix-toml' }) monacoRef.current.languages.register({ id: 'remix-noir' }) @@ -1056,7 +1052,6 @@ export const EditorUI = (props: EditorUIProps) => { monacoRef.current.languages.setMonarchTokensProvider('remix-circom', circomTokensProvider as any) monacoRef.current.languages.setLanguageConfiguration('remix-circom', circomLanguageConfig(monacoRef.current) as any) - monacoRef.current.languages.registerInlineCompletionsProvider('remix-circom', inlineCompletionProvider) monacoRef.current.languages.setMonarchTokensProvider('remix-toml', tomlTokenProvider as any) monacoRef.current.languages.setLanguageConfiguration('remix-toml', tomlLanguageConfig as any) @@ -1072,11 +1067,6 @@ export const EditorUI = (props: EditorUIProps) => { monacoRef.current.languages.registerInlineCompletionsProvider('remix-solidity', inlineCompletionProvider) monaco.languages.registerCodeActionProvider('remix-solidity', new RemixCodeActionProvider(props, monaco)) - monacoRef.current.languages.setMonarchTokensProvider('remix-vyper', vyperTokenProvider as any) - monacoRef.current.languages.setLanguageConfiguration('remix-vyper', vyperLanguageConfig as any) - monacoRef.current.languages.registerCompletionItemProvider('remix-vyper', new RemixCompletionProvider(props, monaco)) - monacoRef.current.languages.registerInlineCompletionsProvider('remix-vyper', inlineCompletionProvider) - loadTypes(monacoRef.current) } diff --git a/libs/remix-ui/remix-ai-assistant/src/components/DefaultResponseContent.tsx b/libs/remix-ui/remix-ai-assistant/src/components/DefaultResponseContent.tsx new file mode 100644 index 00000000000..e741bc2c94f --- /dev/null +++ b/libs/remix-ui/remix-ai-assistant/src/components/DefaultResponseContent.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +export default function DefaultResponseContent() { + return ( + <> +
RemixAI
+

RemixAI provides you personalized guidance as you build. It can break down concepts, answer questions about blockchain technology and assist you with your smart contracts.

+
+ + + +
+

+ Discover all functionalities +

+ + ) +} \ No newline at end of file diff --git a/libs/remix-ui/remix-ai-assistant/src/components/Responsezone.tsx b/libs/remix-ui/remix-ai-assistant/src/components/Responsezone.tsx new file mode 100644 index 00000000000..391ae37d649 --- /dev/null +++ b/libs/remix-ui/remix-ai-assistant/src/components/Responsezone.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import DefaultResponseContent from './DefaultResponseContent' + +export default function ResponseZone({ response, responseId }: { response: string[], responseId: string }) { + return ( +
+ {response.map((item, index) => ( + + {item} + + ))} +
+ ) +} \ No newline at end of file diff --git a/libs/remix-ui/remix-ai-assistant/src/components/promptzone.tsx b/libs/remix-ui/remix-ai-assistant/src/components/promptzone.tsx new file mode 100644 index 00000000000..64e7b365c34 --- /dev/null +++ b/libs/remix-ui/remix-ai-assistant/src/components/promptzone.tsx @@ -0,0 +1,189 @@ +// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries +import { RenderIf, RenderIfNot } from '@remix-ui/helper' +// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries +import { PluginNames } from 'apps/remix-ide/src/types' +import React, { useReducer, useState } from 'react' +import { FormattedMessage } from 'react-intl' + +interface PromptZoneProps { + value?: string + onChangeHandler?: (prompt: string) => void + onSubmitHandler?: () => void + makePluginCall: (pluginName: PluginNames, methodName: string, payload?: any) => Promise +} + +type PromptState = { + selectContext: boolean + files: string[] | '@workspace' + currentSelection: 'workspace' | 'currentFile' | 'allOpenedFiles' | '' +} + +type PromptAction = { + type: 'CURRENT_FILE' + payload: { + file: string + selection: 'workspace' | 'currentFile' | 'allOpenedFiles' + selectContext: boolean + } +} | { + type: 'ALL_OPENED_FILES' + payload: { + files: string[] + selection: 'workspace' | 'currentFile' | 'allOpenedFiles' + selectContext: boolean + } +} | { + type: 'WORKSPACE' + payload: { + files: '@workspace' + selection: 'workspace' | 'currentFile' | 'allOpenedFiles' + selectContext: boolean + } +} | { + type: 'ADD_CONTEXT' + payload: boolean +} | { + type: 'REMOVE_FILE' + payload: string +} | { + type: 'REMOVE_ALL_FILES' +} + +const initialState: PromptState = { + selectContext: false, + currentSelection: '', + files: [], +} + +const stripFileName = (file: string) => { + return file.split('/').pop() +} + +function promptReducer(state:PromptState, action: PromptAction) { + switch (action.type) { + case 'CURRENT_FILE': + return { ...state, files: [...state.files, stripFileName(action.payload.file)], currentSelection: action.payload.selection, selectContext: action.payload.selectContext } + case 'ALL_OPENED_FILES': + return { ...state, files: action.payload.files.map(stripFileName), currentSelection: action.payload.selection, selectContext: action.payload.selectContext } + case 'WORKSPACE': + return { ...state, files: '@workspace', currentSelection: 'workspace', selectContext: action.payload.selectContext } + case 'ADD_CONTEXT': + return { ...state, selectContext: action.payload } + case 'REMOVE_FILE': + return { + ...state, + files: Array.isArray(state.files) ? state.files.filter((file) => file !== action.payload) : [] + } + case 'REMOVE_ALL_FILES': + return { ...state, files: [], currentSelection: '', selectContext: false } + default: + return state + } +} + +export default function PromptZone(props: PromptZoneProps) { + const [promptState, promptDispatch] = useReducer(promptReducer, initialState) + + const removeFile = (file: string) => { + console.log('removing file', file) + promptDispatch({ type: 'REMOVE_FILE', payload: file }) + } + const removeAllFiles = () => { + promptDispatch({ type: 'REMOVE_ALL_FILES' }) + } + const addFile = (file: string) => {} + const addWorkspace = () => {} + + return ( +
+ {promptState.selectContext &&
+
Add context files
+
    +
  • +
    + { + await props.makePluginCall('remixAI', 'setContextFiles', { context: 'currentFile' }) + const result = await props.makePluginCall('fileManager', 'getCurrentFile') + promptDispatch({ type: 'CURRENT_FILE', payload: { + file: result, selection: 'currentFile', selectContext: !promptState.selectContext + } }) + }} checked={promptState.currentSelection === 'currentFile'} /> + +
    +
  • +
  • +
    + { + await props.makePluginCall('remixAI', 'setContextFiles', { context: 'openedFiles' }) + const result = await props.makePluginCall('fileManager', 'getOpenedFiles') + promptDispatch({ type: 'ALL_OPENED_FILES', payload: { files: Object.keys(result), selection: 'allOpenedFiles', selectContext: !promptState.selectContext } }) + }} checked={promptState.currentSelection === 'allOpenedFiles'} /> + +
    +
  • +
  • +
    + { + await props.makePluginCall('remixAI', 'setContextFiles', { context: 'workspace' }) + promptDispatch({ type: 'WORKSPACE', payload: { files: '@workspace', selection: 'workspace', selectContext: !promptState.selectContext } }) + }} checked={promptState.currentSelection === 'workspace'} /> + +
    +
  • +
+
} +
+
+ +
+
+