Merge branch 'master' into desktoptestmd

pull/5239/head
STetsing 2 months ago committed by GitHub
commit 32f5e2a34e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .gitignore
  2. 6
      apps/circuit-compiler/src/app/components/container.tsx
  3. 492
      apps/remix-ide-e2e/src/tests/matomo.test.ts
  4. 18
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  5. 38
      apps/remix-ide/src/app.js
  6. 2
      apps/remix-ide/src/app/components/preload.tsx
  7. 33
      apps/remix-ide/src/app/plugins/electron/remixAIDesktopPlugin.tsx
  8. 169
      apps/remix-ide/src/app/plugins/remixAIPlugin.tsx
  9. 283
      apps/remix-ide/src/app/plugins/solcoderAI.tsx
  10. 2
      apps/remix-ide/src/app/tabs/locales/en/editor.json
  11. 4
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  12. 2
      apps/remix-ide/src/app/tabs/settings-tab.tsx
  13. 4
      apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css
  14. 3
      apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css
  15. 2
      apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css
  16. 3
      apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css
  17. 1
      apps/remix-ide/src/assets/css/themes/remix-black_undtds.css
  18. 1
      apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css
  19. 1
      apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css
  20. 1
      apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css
  21. 1
      apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css
  22. 1
      apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css
  23. 1
      apps/remix-ide/src/assets/css/themes/remix-unicorn.css
  24. 1
      apps/remix-ide/src/assets/css/themes/remix-violet.css
  25. 18
      apps/remix-ide/src/assets/js/loader.js
  26. 5
      apps/remix-ide/src/remixAppManager.js
  27. 3
      apps/remix-ide/src/remixEngine.js
  28. 14684
      apps/remixdesktop/package-lock.json
  29. 3
      apps/remixdesktop/src/engine.ts
  30. 525
      apps/remixdesktop/src/lib/InferenceServerManager.ts
  31. 2
      apps/remixdesktop/src/lib/databatcher.ts
  32. 115
      apps/remixdesktop/src/plugins/remixAIDektop.ts
  33. 2
      apps/remixdesktop/src/preload.ts
  34. 6
      apps/remixdesktop/src/utils/config.ts
  35. 3
      apps/remixdesktop/tsconfig.json
  36. 2
      apps/vyper/README.md
  37. 2
      apps/vyper/src/app/utils/remix-client.tsx
  38. 1
      libs/remix-ai-core/.eslintrc
  39. 7
      libs/remix-ai-core/README.md
  40. 5344
      libs/remix-ai-core/package-lock.json
  41. 32
      libs/remix-ai-core/project.json
  42. 29
      libs/remix-ai-core/src/agents/codeExplainAgent.ts
  43. 23
      libs/remix-ai-core/src/agents/completionAgent.ts
  44. 29
      libs/remix-ai-core/src/agents/securityAgent.ts
  45. 0
      libs/remix-ai-core/src/helpers/dowload_model.ts
  46. 55
      libs/remix-ai-core/src/helpers/inferenceServerReleases.ts
  47. 20
      libs/remix-ai-core/src/index.ts
  48. 141
      libs/remix-ai-core/src/inferencers/remote/remoteInference.ts
  49. 21
      libs/remix-ai-core/src/prompts/chat.ts
  50. 18
      libs/remix-ai-core/src/prompts/completionPrompts.ts
  51. 28
      libs/remix-ai-core/src/prompts/promptBuilder.ts
  52. 9
      libs/remix-ai-core/src/types/constants.ts
  53. 81
      libs/remix-ai-core/src/types/models.ts
  54. 87
      libs/remix-ai-core/src/types/types.ts
  55. 10
      libs/remix-ai-core/tsconfig.json
  56. 15
      libs/remix-ai-core/tsconfig.lib.json
  57. 23
      libs/remix-api/src/lib/plugins/remixAIDesktop-api.ts
  58. 21
      libs/remix-api/src/lib/plugins/remixai-api.ts
  59. 1
      libs/remix-api/src/lib/plugins/terminal-api.ts
  60. 5
      libs/remix-api/src/lib/remix-api.ts
  61. 2
      libs/remix-ui/app/src/index.ts
  62. 11
      libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx
  63. 8
      libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
  64. 2
      libs/remix-ui/app/src/lib/remix-app/context/context.tsx
  65. 5
      libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
  66. 7
      libs/remix-ui/app/src/lib/remix-app/interface/index.ts
  67. 3
      libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
  68. 4
      libs/remix-ui/app/src/lib/remix-app/remix-app.tsx
  69. 9
      libs/remix-ui/app/src/lib/remix-app/types/index.ts
  70. 31
      libs/remix-ui/editor/src/lib/providers/completionTimer.ts
  71. 84
      libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts
  72. 16
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  73. 24
      libs/remix-ui/home-tab/src/lib/components/homeTabFeatured.tsx
  74. 6
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.css
  75. 19
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  76. 7
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  77. 1
      libs/remix-ui/remix-ai/src/index.ts
  78. 84
      libs/remix-ui/remix-ai/src/lib/components/Default.tsx
  79. 78
      libs/remix-ui/remix-ai/src/lib/components/ModelSelection.tsx
  80. 15
      libs/remix-ui/remix-ai/src/lib/components/RemixAI.tsx
  81. 167
      libs/remix-ui/remix-ai/src/lib/remix-ai.css
  82. 4
      libs/remix-ui/renderer/src/lib/renderer.tsx
  83. 2
      libs/remix-ui/run-tab/src/lib/components/account.tsx
  84. 2
      libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx
  85. 4
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  86. 1
      libs/remix-ui/settings/src/lib/settingsAction.ts
  87. 4
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.css
  88. 6
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  89. 8
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  90. 7
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  91. 40
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  92. 8
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  93. 12
      libs/remix-ui/workspace/src/lib/components/workspace-hamburger.tsx
  94. 2
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  95. 130
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  96. 4
      libs/remix-ui/workspace/src/lib/types/index.ts
  97. 68735
      package-lock.json
  98. 4
      package.json
  99. 10
      release-management.md
  100. 4
      release-process.md
  101. Some files were not shown because too many files have changed in this diff Show More

2
.gitignore vendored

@ -66,6 +66,6 @@ apps/remixdesktop/build*/
apps/remix-ide/src/assets/list.json
apps/remix-ide/src/assets/esbuild.wasm
apps/remixdesktop/build*
apps/remixdesktop/reports/
apps/remixdesktop/reports
apps/remixdesktop/logs/
logs

@ -74,7 +74,7 @@ export function Container () {
explain why the error occurred and how to fix it.
`
// @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message)
await circuitApp.plugin.call('remixAI', 'error_explaining', message)
} else {
const message = `
error message: ${error}
@ -82,7 +82,7 @@ export function Container () {
explain why the error occurred and how to fix it.
`
// @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message)
await circuitApp.plugin.call('remixAI', 'error_explaining', message)
}
} else {
const error = report.message
@ -92,7 +92,7 @@ export function Container () {
explain why the error occurred and how to fix it.
`
// @ts-ignore
await circuitApp.plugin.call('solcoder', 'error_explaining', message)
await circuitApp.plugin.call('remixAI', 'error_explaining', message)
}
}

@ -0,0 +1,492 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
import examples from '../examples/example-contracts'
const sources = [
{ 'Untitled.sol': { content: examples.ballot.content } }
]
module.exports = {
'@disabled': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', false)
},
'confirm Matomo #group1': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser
.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
localStorage.setItem('showMatomo', 'true')
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.execute(function () {
return (window as any)._paq
}, [], (res) => {
console.log('_paq', res)
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.pause(1000)
.click('[data-id="matomoModal-modal-footer-ok-react"]') // submitted
.execute(function () {
return (window as any)._paq
}, [], (res) => {
console.log('_paq', res)
})
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.waitForElementVisible('*[data-id="beginnerbtn"]', 10000)
.pause(1000)
.click('[data-id="beginnerbtn"]')
.waitForElementNotPresent('*[data-id="beginnerbtn"]')
.waitForElementVisible({
selector: `//*[contains(text(), 'Welcome to Remix IDE')]`,
locateStrategy: 'xpath'
})
.refreshPage()
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]')
.clickLaunchIcon('settings')
.verify.elementPresent('[id="settingsMatomoAnalytics"]:checked')
.execute(function () {
return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == true
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics is enabled')
})
},
'decline Matomo #group1': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
localStorage.setItem('showMatomo', 'true')
localStorage.removeItem('matomo-analytics-consent')
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.execute(function () {
return (window as any)._paq
}, [], (res) => {
console.log('_paq', res)
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel
.execute(function () {
return (window as any)._paq
}, [], (res) => {
console.log('_paq', res)
})
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.pause(2000)
.waitForElementNotPresent('*[data-id="beginnerbtn"]', 10000)
.clickLaunchIcon('settings')
.waitForElementNotPresent('[id="settingsMatomoAnalytics"]:checked')
.execute(function () {
return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == false
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics is disabled')
})
},
'blur matomo #group2': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
localStorage.setItem('showMatomo', 'true')
localStorage.removeItem('matomo-analytics-consent')
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.waitForElementVisible({
selector: '*[data-id="matomoModalModalDialogModalBody-react"]',
abortOnFailure: true
})
.waitForElementVisible('*[data-id="matomoModal-modal-close"]')
.click('[data-id="matomoModal-modal-close"]')
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.pause(2000)
.waitForElementNotPresent('*[data-id="beginnerbtn"]', 10000)
.clickLaunchIcon('settings')
.waitForElementNotPresent('[id="settingsMatomoAnalytics"]:checked')
.execute(function () {
return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == undefined
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics is undefined')
})
},
'matomo should reappear #group2': function (browser: NightwatchBrowser) {
browser
.refreshPage()
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.waitForElementVisible({
selector: '*[data-id="matomoModalModalDialogModalBody-react"]',
abortOnFailure: true
})
.waitForElementVisible('*[data-id="matomoModal-modal-close"]')
.click('[data-id="matomoModal-modal-close"]')
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'change settings #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('settings')
.waitForElementVisible('*[data-id="label-matomo-settings"]')
.pause(1000)
.click('*[data-id="label-matomo-settings"]')
.refreshPage()
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'should get enter dialog again #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="beginnerbtn"]', 10000)
.pause(1000)
.click('[data-id="beginnerbtn"]')
.waitForElementNotPresent('*[data-id="beginnerbtn"]')
.waitForElementVisible({
selector: `//*[contains(text(), 'Welcome to Remix IDE')]`,
locateStrategy: 'xpath'
})
.waitForElementVisible('*[id="remixTourSkipbtn"]')
.click('*[id="remixTourSkipbtn"]')
.clickLaunchIcon('settings')
.waitForElementPresent('[id="settingsMatomoAnalytics"]:checked')
.execute(function () {
return JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics'] == true
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics is enabled')
})
},
'decline Matomo and check timestamp #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
localStorage.setItem('showMatomo', 'true')
localStorage.removeItem('matomo-analytics-consent')
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
// output the contents of the storage
.execute(function () {
return {
consent: window.localStorage.getItem('matomo-analytics-consent'),
config: window.localStorage.getItem('config-v0.8:.remix.config'),
showMatomo: window.localStorage.getItem('showMatomo')
}
}, [], (res) => {
console.log('res', res)
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.pause(2000)
.execute(function () {
const timestamp = window.localStorage.getItem('matomo-analytics-consent');
if (timestamp) {
const consentDate = new Date(Number(timestamp));
// validate it is actually a date
if (isNaN(consentDate.getTime())) {
return false;
}
const now = new Date();
console.log('timestamp', timestamp, consentDate, now.getTime())
const diffInMinutes = (now.getTime() - consentDate.getTime()) / (1000 * 60);
console.log('diffInMinutes', diffInMinutes)
return diffInMinutes < 2;
}
return false;
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is set')
})
},
'check old timestamp and reappear Matomo #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
const oldTimestamp = new Date()
oldTimestamp.setMonth(oldTimestamp.getMonth() - 7)
localStorage.setItem('matomo-analytics-consent', oldTimestamp.getTime().toString())
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.execute(function () {
const timestamp = window.localStorage.getItem('matomo-analytics-consent');
if (timestamp) {
const consentDate = new Date(Number(timestamp));
// validate it is actually a date
if (isNaN(consentDate.getTime())) {
return false;
}
// validate it's older than 6 months
const now = new Date();
const diffInMonths = (now.getFullYear() - consentDate.getFullYear()) * 12 + now.getMonth() - consentDate.getMonth();
console.log('timestamp', timestamp, consentDate, now.getTime())
console.log('diffInMonths', diffInMonths)
return diffInMonths > 6;
}
return false;
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is set')
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'check recent timestamp and do not reappear Matomo #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
const recentTimestamp = new Date()
recentTimestamp.setMonth(recentTimestamp.getMonth() - 1)
localStorage.setItem('matomo-analytics-consent', recentTimestamp.getTime().toString())
}, [])
.refreshPage()
.perform(done())
})
// check if timestamp is younger than 6 months
.execute(function () {
const timestamp = window.localStorage.getItem('matomo-analytics-consent');
if (timestamp) {
const consentDate = new Date(Number(timestamp));
// validate it is actually a date
if (isNaN(consentDate.getTime())) {
return false;
}
// validate it's younger than 2 months
const now = new Date();
const diffInMonths = (now.getFullYear() - consentDate.getFullYear()) * 12 + now.getMonth() - consentDate.getMonth();
console.log('timestamp', timestamp, consentDate, now.getTime())
console.log('diffInMonths', diffInMonths)
return diffInMonths < 2;
}
return false;
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is set')
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.pause(2000)
.waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'accept Matomo and check timestamp #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
localStorage.setItem('showMatomo', 'true')
localStorage.removeItem('matomo-analytics-consent')
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.click('[data-id="matomoModal-modal-footer-ok-react"]') // accept
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.pause(2000)
.execute(function () {
const timestamp = window.localStorage.getItem('matomo-analytics-consent');
if (timestamp) {
const consentDate = new Date(Number(timestamp));
// validate it is actually a date
if (isNaN(consentDate.getTime())) {
return false;
}
const now = new Date();
console.log('timestamp', timestamp, consentDate, now.getTime())
const diffInMinutes = (now.getTime() - consentDate.getTime()) / (1000 * 60);
console.log('diffInMinutes', diffInMinutes)
return diffInMinutes < 1;
}
return false;
}, [], (res) => {
console.log('res', res)
browser.assert.ok((res as any).value, 'matomo analytics consent timestamp is to a recent date')
})
},
'check old timestamp and do not reappear Matomo after accept #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
const oldTimestamp = new Date()
oldTimestamp.setMonth(oldTimestamp.getMonth() - 7)
localStorage.setItem('matomo-analytics-consent', oldTimestamp.getTime().toString())
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.pause(2000)
.waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'check recent timestamp and do not reappear Matomo after accept #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
const recentTimestamp = new Date()
recentTimestamp.setMonth(recentTimestamp.getMonth() - 1)
localStorage.setItem('matomo-analytics-consent', recentTimestamp.getTime().toString())
}, [])
.refreshPage()
.perform(done())
})
.waitForElementPresent({
selector: `//*[@data-id='compilerloaded']`,
locateStrategy: 'xpath',
timeout: 120000
})
.pause(2000)
.waitForElementNotPresent('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'when there is a recent timestamp but no config the dialog should reappear #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
const recentTimestamp = new Date()
recentTimestamp.setMonth(recentTimestamp.getMonth() - 1)
localStorage.setItem('matomo-analytics-consent', recentTimestamp.getTime().toString())
}, [])
.refreshPage()
.perform(done())
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'when there is a old timestamp but no config the dialog should reappear #group3': function (browser: NightwatchBrowser) {
browser.perform((done) => {
browser.execute(function () {
localStorage.removeItem('config-v0.8:.remix.config')
const oldTimestamp = new Date()
oldTimestamp.setMonth(oldTimestamp.getMonth() - 7)
localStorage.setItem('matomo-analytics-consent', oldTimestamp.getTime().toString())
}, [])
.refreshPage()
.perform(done())
})
.waitForElementVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
.click('[data-id="matomoModal-modal-footer-cancel-react"]') // cancel
.waitForElementNotVisible('*[data-id="matomoModalModalDialogModalBody-react"]')
},
'verify Matomo events are tracked on app start #group4 #lfaky': function (browser: NightwatchBrowser) {
browser
.execute(function () {
return (window as any)._paq
}, [], (res) => {
const expectedEvents = [
["trackEvent", "Preload", "start"],
["trackEvent", "Storage", "activate", "indexedDB"],
["trackEvent", "App", "load"],
];
const actualEvents = (res as any).value;
const areEventsPresent = expectedEvents.every(expectedEvent =>
actualEvents.some(actualEvent =>
JSON.stringify(actualEvent) === JSON.stringify(expectedEvent)
)
);
browser.assert.ok(areEventsPresent, 'Matomo events are tracked correctly');
})
},
'@sources': function () {
return sources
},
'Add Ballot #group4': function (browser: NightwatchBrowser) {
browser
.addFile('Untitled.sol', sources[0]['Untitled.sol'])
},
'Deploy Ballot #group4': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('solidity')
.waitForElementVisible('*[data-id="compilerContainerCompileBtn"]')
.click('*[data-id="compilerContainerCompileBtn"]')
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
},
'verify Matomo compiler events are tracked #group4': function (browser: NightwatchBrowser) {
browser
.execute(function () {
return (window as any)._paq
}, [], (res) => {
const expectedEvent = ["trackEvent", "compiler", "compiled"];
const actualEvents = (res as any).value;
const isEventPresent = actualEvents.some(actualEvent =>
actualEvent[0] === expectedEvent[0] &&
actualEvent[1] === expectedEvent[1] &&
actualEvent[2] === expectedEvent[2] &&
actualEvent[3].startsWith("with_version_")
);
browser.assert.ok(isEventPresent, 'Matomo compiler events are tracked correctly');
})
},
}

@ -363,16 +363,14 @@ module.exports = {
}
`
if (runMasterTests) {
const path = "//*[@class='view-line' and contains(.,'resolveENS') and contains(.,'view')]//span//span[contains(.,'(')]"
browser
// .clickLaunchIcon('udapp')
.switchEnvironment('vm-mainnet-fork')
.clickLaunchIcon('filePanel')
.addFile('test_mainnet.sol', { content: script })
const path = "//*[@class='view-line' and contains(.,'resolveENS') and contains(.,'view')]//span//span[contains(.,'(')]"
const pathRunFunction = `//li//*[@aria-label='Run the free function "resolveENS"']`
browser.waitForElementVisible('#editorView')
//.waitForElementPresent(pathRunFunction)
.waitForElementVisible('#editorView')
.pause(10000) // the parser need to parse the code
.useXpath()
.scrollToLine(16)
@ -396,15 +394,15 @@ module.exports = {
console.log("test running free function");
}
`
const path = "//*[@class='view-line' and contains(.,'runSomething') and contains(.,'view')]//span//span[contains(.,'(')]"
browser
.addFile('test.sol', { content: script })
.scrollToLine(3)
const path = "//*[@class='view-line' and contains(.,'runSomething') and contains(.,'view')]//span//span[contains(.,'(')]"
const pathRunFunction = `//li//*[@aria-label='Run the free function "runSomething"']`
browser.waitForElementVisible('#editorView')
.waitForElementVisible('#editorView')
.pause(10000) // the parser need to parse the code
.useXpath()
.scrollToLine(3)
.click(path)
.pause(3000) // the parser need to parse the code
.perform(function () {
const actions = this.actions({ async: true });
return actions

@ -57,6 +57,8 @@ import { xtermPlugin } from './app/plugins/electron/xtermPlugin'
import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin'
import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin'
import { appUpdaterPlugin } from './app/plugins/electron/appUpdaterPlugin'
import { remixAIDesktopPlugin } from './app/plugins/electron/remixAIDesktopPlugin'
import { RemixAIPlugin } from './app/plugins/remixAIPlugin'
import { SlitherHandleDesktop } from './app/plugins/electron/slitherPlugin'
import { SlitherHandle } from './app/files/slither-handle'
import { FoundryHandle } from './app/files/foundry-handle'
@ -64,12 +66,9 @@ import { FoundryHandleDesktop } from './app/plugins/electron/foundryPlugin'
import { HardhatHandle } from './app/files/hardhat-handle'
import { HardhatHandleDesktop } from './app/plugins/electron/hardhatPlugin'
import { SolCoder } from './app/plugins/solcoderAI'
import { GitPlugin } from './app/plugins/git'
import { Matomo } from './app/plugins/matomo'
import { TemplatesSelectionPlugin } from './app/plugins/templates-selection/templates-selection-plugin'
const isElectron = require('is-electron')
@ -103,6 +102,7 @@ const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal')
const { TabProxy } = require('./app/panels/tab-proxy.js')
const _paq = (window._paq = window._paq || [])
export class platformApi {
get name() {
@ -178,16 +178,24 @@ class AppComponent {
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop
}
_paq.push(['trackEvent', 'App', 'load']);
this.matomoConfAlreadySet = Registry.getInstance().get('config').api.exists('settings/matomo-analytics')
this.matomoCurrentSetting = Registry.getInstance().get('config').api.get('settings/matomo-analytics')
let electronTracking = false
let electronTracking = window.electronAPI ? await window.electronAPI.canTrackMatomo() : false
if (window.electronAPI) {
electronTracking = await window.electronAPI.canTrackMatomo()
}
const lastMatomoCheck = window.localStorage.getItem('matomo-analytics-consent')
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
this.showMatamo = (matomoDomains[window.location.hostname] || electronTracking) && !this.matomoConfAlreadySet
const e2eforceMatomoToShow = window.localStorage.getItem('showMatomo') && window.localStorage.getItem('showMatomo') === 'true'
const contextShouldShowMatomo = matomoDomains[window.location.hostname] || e2eforceMatomoToShow || electronTracking
const shouldRenewConsent = this.matomoCurrentSetting === false && (!lastMatomoCheck || new Date(Number(lastMatomoCheck)) < sixMonthsAgo) // it is set to false for more than 6 months.
this.showMatomo = contextShouldShowMatomo && (!this.matomoConfAlreadySet || shouldRenewConsent)
if (this.showMatomo && shouldRenewConsent) {
_paq.push(['trackEvent', 'Matomo', 'refreshMatomoPermissions']);
}
this.walkthroughService = new WalkthroughService(appManager)
@ -261,7 +269,7 @@ class AppComponent {
const contractFlattener = new ContractFlattener()
// ----------------- AI --------------------------------------
const solcoder = new SolCoder()
const remixAI = new RemixAIPlugin(isElectron())
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
@ -384,11 +392,11 @@ class AppComponent {
contractFlattener,
solidityScript,
templates,
solcoder,
git,
pluginStateLogger,
matomo,
templateSelection
templateSelection,
remixAI
])
//---- fs plugin
@ -407,6 +415,8 @@ class AppComponent {
this.engine.register([ripgrep])
const appUpdater = new appUpdaterPlugin()
this.engine.register([appUpdater])
const remixAIDesktop = new remixAIDesktopPlugin()
this.engine.register([remixAIDesktop])
}
const compilerloader = isElectron() ? new compilerLoaderPluginDesktop() : new compilerLoaderPlugin()
@ -538,7 +548,8 @@ class AppComponent {
'fetchAndCompile',
'contentImport',
'gistHandler',
'compilerloader'
'compilerloader',
'remixAI'
])
await this.appManager.activatePlugin(['settings'])
@ -546,7 +557,7 @@ class AppComponent {
await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if (isElectron()) {
await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither', 'foundry', 'hardhat'])
await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither', 'foundry', 'hardhat', 'remixAID'])
}
this.appManager.on(
@ -561,7 +572,6 @@ class AppComponent {
}
)
await this.appManager.activatePlugin(['solidity-script'])
await this.appManager.activatePlugin(['solcoder'])
await this.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation

@ -10,6 +10,8 @@ import './styles/preload.css'
import isElectron from 'is-electron'
const _paq = (window._paq = window._paq || [])
_paq.push(['trackEvent', 'Preload', 'start'])
export const Preload = (props: any) => {
const [tip, setTip] = useState<string>('')
const [supported, setSupported] = useState<boolean>(true)

@ -0,0 +1,33 @@
import { ElectronPlugin } from '@remixproject/engine-electron'
import { IModel, ModelType, DefaultModels } from '@remix/remix-ai-core';
import axios from 'axios';
import fs from 'fs';
const desktop_profile = {
name: 'remixAID',
displayName: 'RemixAI Desktop',
maintainedBy: 'Remix',
description: 'RemixAI provides AI services to Remix IDE Desktop.',
documentation: 'https://remix-ide.readthedocs.io/en/latest/remixai.html',
icon: 'assets/img/remix-logo-blue.png',
methods: ['initializeModelBackend', 'code_completion', 'code_insertion', 'code_generation', 'code_explaining', 'error_explaining', 'solidity_answer'],
}
export class remixAIDesktopPlugin extends ElectronPlugin {
constructor() {
super(desktop_profile)
}
onActivation(): void {
this.on('remixAI', 'enabled', () => {} )
console.log('remixAIDesktopPlugin activated')
}
}
// class RemixAIPlugin extends ElectronPlugin {
// constructor() {
// super(dek)
// this.methods = ['downloadModel']
// }
// }

@ -0,0 +1,169 @@
import * as packageJson from '../../../../../package.json'
import { ViewPlugin } from '@remixproject/engine-web'
import { Plugin } from '@remixproject/engine';
import { RemixAITab } from '@remix-ui/remix-ai'
import React from 'react';
import { ICompletions, IModel, RemoteInferencer, IRemoteModel } from '@remix/remix-ai-core';
const profile = {
name: 'remixAI',
displayName: 'Remix AI',
methods: ['code_generation', 'code_completion',
"solidity_answer", "code_explaining",
"code_insertion", "error_explaining",
"initialize"],
events: [],
icon: 'assets/img/remix-logo-blue.png',
description: 'RemixAI provides AI services to Remix IDE.',
kind: '',
// location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/remixai.html',
version: packageJson.version,
maintainedBy: 'Remix'
}
export class RemixAIPlugin extends Plugin {
isOnDesktop:boolean = false
aiIsActivated:boolean = false
readonly remixDesktopPluginName = 'remixAID'
remoteInferencer:RemoteInferencer = null
isInferencing: boolean = false
constructor(inDesktop:boolean) {
super(profile)
this.isOnDesktop = inDesktop
// user machine dont use ressource for remote inferencing
}
onActivation(): void {
this.initialize(null, null, null, false)
}
async initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean){
if (this.isOnDesktop) {
// on desktop use remote inferencer -> false
console.log('initialize on desktop')
const res = await this.call(this.remixDesktopPluginName, 'initializeModelBackend', useRemote, model1, model2)
if (res) {
this.on(this.remixDesktopPluginName, 'onStreamResult', (value) => {
this.call('terminal', 'log', { type: 'log', value: value })
})
this.on(this.remixDesktopPluginName, 'onInference', () => {
this.isInferencing = true
})
this.on(this.remixDesktopPluginName, 'onInferenceDone', () => {
this.isInferencing = false
})
}
} else {
// on browser
this.remoteInferencer = new RemoteInferencer(remoteModel?.apiUrl, remoteModel?.completionUrl)
this.remoteInferencer.event.on('onInference', () => {
this.isInferencing = true
})
this.remoteInferencer.event.on('onInferenceDone', () => {
this.isInferencing = false
})
}
this.aiIsActivated = true
return true
}
async code_generation(prompt: string): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
if (this.isOnDesktop) {
return await this.call(this.remixDesktopPluginName, 'code_generation', prompt)
} else {
return await this.remoteInferencer.code_generation(prompt)
}
}
async code_completion(prompt: string): Promise<any> {
if (this.isOnDesktop) {
return await this.call(this.remixDesktopPluginName, 'code_completion', prompt)
} else {
return await this.remoteInferencer.code_completion(prompt)
}
}
async solidity_answer(prompt: string): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `\n\nWaiting for RemixAI answer...` })
let result
if (this.isOnDesktop) {
result = await this.call(this.remixDesktopPluginName, 'solidity_answer', prompt)
} else {
result = await this.remoteInferencer.solidity_answer(prompt)
}
if (result) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result })
// this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI Done" })
return result
}
async code_explaining(prompt: string): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `\n\nWaiting for RemixAI answer...` })
let result
if (this.isOnDesktop) {
result = await this.call(this.remixDesktopPluginName, 'code_explaining', prompt)
} else {
result = await this.remoteInferencer.code_explaining(prompt)
}
if (result) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result })
// this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI Done" })
return result
}
async error_explaining(prompt: string): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `\n\nWaiting for RemixAI answer...` })
let result
if (this.isOnDesktop) {
result = await this.call(this.remixDesktopPluginName, 'error_explaining', prompt)
} else {
result = await this.remoteInferencer.error_explaining(prompt)
}
if (result) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result })
// this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI Done" })
return result
}
async code_insertion(msg_pfx: string, msg_sfx: string): Promise<any> {
if (this.isOnDesktop) {
return await this.call(this.remixDesktopPluginName, 'code_insertion', msg_pfx, msg_sfx)
} else {
return await this.remoteInferencer.code_insertion(msg_pfx, msg_sfx)
}
}
// render() {
// return (
// <RemixAITab plugin={this}></RemixAITab>
// )
// }
}

@ -1,283 +0,0 @@
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", "code_insertion", "error_explaining"],
events: [],
maintainedBy: 'Remix',
}
type ChatEntry = [string, string];
enum BackendOPModel{
DeepSeek,
CodeLLama,
Mistral
}
const PromptBuilder = (inst, answr, modelop) => {
if (modelop === BackendOPModel.CodeLLama) return ""
if (modelop === BackendOPModel.DeepSeek) return "\n### INSTRUCTION:\n" + inst + "\n### RESPONSE:\n" + answr
if (modelop === BackendOPModel.Mistral) return ""
}
export class SolCoder extends Plugin {
api_url: string
completion_url: string
solgpt_chat_history:ChatEntry[]
max_history = 7
model_op = BackendOPModel.DeepSeek
constructor() {
super(profile)
this.api_url = "https://solcoder.remixproject.org"
this.completion_url = "https://completion.remixproject.org"
this.solgpt_chat_history = []
}
pushChatHistory(prompt, result){
const chat:ChatEntry = [prompt, result.data[0]]
this.solgpt_chat_history.push(chat)
if (this.solgpt_chat_history.length > this.max_history){this.solgpt_chat_history.shift()}
}
async code_generation(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
_paq.push(['trackEvent', 'ai', 'solcoder', 'code_generation'])
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()
if ("error" in result){
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.error })
return result
}
return result.data
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
return
} finally {
this.emit("aiInferingDone")
}
}
async solidity_answer(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `\n\nWaiting for RemixAI answer...` })
_paq.push(['trackEvent', 'ai', 'solcoder', 'answering'])
let result
try {
const main_prompt = this._build_solgpt_promt(prompt)
result = await(
await fetch(this.api_url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ "data":[main_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}` })
this.solgpt_chat_history = []
return
} finally {
this.emit("aiInferingDone")
}
if (result) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.data[0] })
this.pushChatHistory(prompt, result)
} else if (result.error) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "Error on request" })
}
}
async code_explaining(prompt, context:string=""): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `\n\nWaiting for RemixAI answer...` })
_paq.push(['trackEvent', 'ai', 'solcoder', 'explaining'])
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, context]}),
})
).json()
if (result) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.data[0] })
this.pushChatHistory(prompt, result)
}
return result.data[0]
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
return
} finally {
this.emit("aiInferingDone")
}
}
async code_completion(prompt, options:SuggestOptions=null): Promise<any> {
this.emit("aiInfering")
_paq.push(['trackEvent', 'ai', 'solcoder', 'code_completion'])
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){
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")
}
}
async code_insertion(msg_pfx, msg_sfx): Promise<any> {
this.emit("aiInfering")
_paq.push(['trackEvent', 'ai', 'solcoder', 'code_insertion'])
let result
try {
result = await(
await fetch(this.completion_url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ "data":[
msg_pfx, // Text before current cursor line
"code_insertion",
msg_sfx, // Text after current cursor line
1024,
0.5,
0.92,
50
]}),
})
).json()
if ("error" in result){
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")
}
}
async error_explaining(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: `\n\nWaiting for RemixAI answer...` })
_paq.push(['trackEvent', 'ai', 'solcoder', 'explaining'])
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, "error_explaining", false,2000,0.9,0.8,50]}),
})
).json()
if (result) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result.data[0] })
this.pushChatHistory(prompt, result)
}
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")
}
}
_build_solgpt_promt(user_promt:string){
if (this.solgpt_chat_history.length === 0){
return user_promt
} else {
let new_promt = ""
for (const [question, answer] of this.solgpt_chat_history) {
new_promt += PromptBuilder(question.split('sol-gpt')[1], answer, this.model_op)
}
// finaly
new_promt = "sol-gpt " + new_promt + PromptBuilder(user_promt.split('sol-gpt')[1], "", this.model_op)
return new_promt
}
}
}

@ -21,7 +21,7 @@
"editor.formatCode": "Format Code",
"editor.generateDocumentation": "Generate documentation for this function",
"editor.generateDocumentation2": "Generate documentation for the function \"{name}\"",
"editor.generateDocumentationByAI": "solidity code: {content}\n Generate the natspec documentation for the function {currentFunction} using the docstring style syntax. Only use docstring supported tags",
"editor.generateDocumentationByAI": "```solidity\n {content}\n```\n You only generate the natspec documentation for the function {currentFunction} using the docstring style syntax. Only use docstring supported tags",
"editor.explainFunction": "Explain this function",
"editor.explainFunctionSol": "Explain this code",
"editor.explainFunction2": "Explain the function \"{name}\"",

@ -9,6 +9,7 @@
"filePanel.clone": "Clone",
"filePanel.download": "Download",
"filePanel.backup": "Backup",
"filePanel.localFileSystem": "Connect to Local Filesystem",
"filePanel.restore": "Restore",
"filePanel.name": "Name",
"filePanel.save": "Save",
@ -28,6 +29,7 @@
"filePanel.workspace.chooseTemplate": "Choose a template",
"filePanel.workspace.backup": "Backup All Workspaces",
"filePanel.workspace.restore": "Restore Workspaces from the Backup",
"filePanel.workspace.localFileSystem": "Connect to Local Filesystem using Remixd",
"filePanel.workspace.clone": "Clone Git Repository",
"filePanel.workspace.cloneMessage": "Please provide a valid git repository url.",
"filePanel.workspace.enterGitUrl": "Enter git repository url",
@ -92,7 +94,7 @@
"filePanel.cancel": "Cancel",
"filePanel.selectFolder": "Select or create folder",
"filePanel.createNewWorkspace": "create a new workspace",
"filePanel.connectToLocalhost": "connect to localhost",
"filePanel.connectToLocalhost": "Connect to Local Filesystem",
"filePanel.copiedToClipboard": "Copied to clipboard {path}",
"filePanel.downloadFailed": "Download Failed",
"filePanel.downloadFailedMsg": "Unexpected error while downloading: {error}",

@ -109,6 +109,8 @@ module.exports = class SettingsTab extends ViewPlugin {
updateMatomoAnalyticsChoice(isChecked) {
this.config.set('settings/matomo-analytics', isChecked)
// set timestamp to local storage to track when the user has given consent
localStorage.setItem('matomo-analytics-consent', Date.now().toString())
this.useMatomoAnalytics = isChecked
if (!isChecked) {
// revoke tracking consent

@ -5045,8 +5045,10 @@ a.close.disabled {
}
.modal-header .close {
padding:1rem 1rem;
margin:-1rem -1rem -1rem auto
margin:-1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {
margin-bottom:0;
line-height:1.5

@ -5047,7 +5047,8 @@ a.close.disabled {
}
.modal-header .close {
padding:1rem 1rem;
margin:-1rem -1rem -1rem auto
margin:-1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {
margin-bottom:0;

@ -4153,7 +4153,7 @@ a.close.disabled {
display:-ms-flexbox; display:flex; -ms-flex-align:start; align-items:flex-start; -ms-flex-pack:justify; justify-content:space-between; padding:1rem 1rem; border-bottom:1px solid #dee2e6; border-top-left-radius:calc(.3rem - 1px); border-top-right-radius:calc(.3rem - 1px)
}
.modal-header .close {
padding:1rem 1rem; margin:-1rem -1rem -1rem auto
padding:1rem 1rem; margin:-1rem -1rem -1rem auto; cursor: pointer;
}
.modal-title {
margin-bottom:0; line-height:1.5

@ -5048,7 +5048,8 @@ a.close.disabled {
}
.modal-header .close {
padding:1rem 1rem;
margin:-1rem -1rem -1rem auto
margin:-1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {
margin-bottom:0;

@ -5080,6 +5080,7 @@ a.close.disabled {
.modal-header .close {
padding: 15px;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {
margin-bottom: 0;

@ -5540,6 +5540,7 @@ a.close.disabled {
.modal-header .close {
padding: 1rem 1rem;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {

@ -5082,6 +5082,7 @@ a.close.disabled {
.modal-header .close {
padding: 15px;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {
margin-bottom: 0;

@ -5092,6 +5092,7 @@ a.close.disabled {
.modal-header .close {
padding: 15px;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {
margin-bottom: 0;

@ -5536,6 +5536,7 @@ a.close.disabled {
.modal-header .close {
padding: 1rem 1rem;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {

@ -5542,6 +5542,7 @@ a.close.disabled {
.modal-header .close {
padding: 1rem 1rem;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {

@ -5540,6 +5540,7 @@ a.close.disabled {
.modal-header .close {
padding: 1rem 1rem;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {

@ -5536,6 +5536,7 @@ a.close.disabled {
.modal-header .close {
padding: 1rem 1rem;
margin: -1rem -1rem -1rem auto;
cursor: pointer;
}
.modal-title {

@ -18,14 +18,26 @@ function trackDomain(domainToTrack) {
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
_paq.push(['enableHeartBeatTimer']);
if (!window.localStorage.getItem('config-v0.8:.remix.config') ||
(window.localStorage.getItem('config-v0.8:.remix.config') && !window.localStorage.getItem('config-v0.8:.remix.config').includes('settings/matomo-analytics'))) {
const remixConfig = window.localStorage.getItem('config-v0.8:.remix.config');
if (!remixConfig || (remixConfig && !remixConfig.includes('settings/matomo-analytics'))) {
// require user tracking consent before processing data
_paq.push(['requireConsent']);
} else {
try {
const config = JSON.parse(remixConfig);
if (config['settings/matomo-analytics'] === true) {
// user has given consent to process their data
_paq.push(['setConsentGiven'])
_paq.push(['setConsentGiven']);
} else {
// user has not given consent to process their data
_paq.push(['requireConsent']);
}
} catch (e) {
console.error('Error parsing remix config:', e);
_paq.push(['requireConsent']);
}
}
_paq.push(['trackEvent', 'loader', 'load']);
(function () {
var u = "https://ethereumfoundation.matomo.cloud/";
_paq.push(['setTrackerUrl', u + 'matomo.php?debug=1']);

@ -72,11 +72,12 @@ let requiredModules = [ // services + layout views + system views
'vyperCompilationDetails',
'contractflattener',
'solidity-script',
'solcoder',
'home',
'doc-viewer',
'doc-gen',
'remix-templates',
'remixAID',
'remixAI',
'solhint',
'dgit',
'pinnedPanel',
@ -165,7 +166,7 @@ export class RemixAppManager extends PluginManager {
this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
this.pluginLoader = new PluginLoader()
if (Registry.getInstance().get('platform').api.isDesktop()) {
requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig', 'xterm', 'compilerloader', 'ripgrep', 'slither']
requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig', 'xterm', 'compilerloader', 'ripgrep', 'slither', 'remixAID']
}
}

@ -26,7 +26,8 @@ export class RemixEngine extends Engine {
if (name === 'compilerloader') return { queueTimeout: 60000 * 4 }
if (name === 'filePanel') return { queueTimeout: 60000 * 20 }
if (name === 'fileManager') return { queueTimeout: 60000 * 20 }
if (name === 'solcoder') return { queueTimeout: 60000 * 2 }
if (name === 'remixAID') return { queueTimeout: 60000 * 20 }
if (name === 'remixAI') return { queueTimeout: 60000 * 20 }
if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 }
if (name === 'contentImport') return { queueTimeout: 60000 * 3 }

File diff suppressed because it is too large Load Diff

@ -11,6 +11,7 @@ import { RipgrepPlugin } from './plugins/ripgrepPlugin';
import { CompilerLoaderPlugin } from './plugins/compilerLoader';
import { SlitherPlugin } from './plugins/slitherPlugin';
import { AppUpdaterPlugin } from './plugins/appUpdater';
import { RemixAIDesktopPlugin } from './plugins/remixAIDektop';
import { FoundryPlugin } from './plugins/foundryPlugin';
import { HardhatPlugin } from './plugins/hardhatPlugin';
import { isE2E } from './main';
@ -28,6 +29,7 @@ const slitherPlugin = new SlitherPlugin()
const appUpdaterPlugin = new AppUpdaterPlugin()
const foundryPlugin = new FoundryPlugin()
const hardhatPlugin = new HardhatPlugin()
const remixAIDesktopPlugin = new RemixAIDesktopPlugin()
engine.register(appManager)
engine.register(fsPlugin)
@ -41,6 +43,7 @@ engine.register(slitherPlugin)
engine.register(foundryPlugin)
engine.register(appUpdaterPlugin)
engine.register(hardhatPlugin)
engine.register(remixAIDesktopPlugin)
appManager.activatePlugin('electronconfig')
appManager.activatePlugin('fs')

@ -0,0 +1,525 @@
import path, { resolve } from 'path';
const { spawn } = require('child_process'); // eslint-disable-line
import fs from 'fs';
import axios from "axios";
import { EventEmitter } from 'events';
import { ICompletions, IModel, IParams, InsertionParams,
CompletionParams, GenerationParams, ModelType, AIRequestType,
IStreamResponse, ChatHistory, downloadLatestReleaseExecutable,
buildSolgptPromt } from "@remix/remix-ai-core"
import { platform } from 'os';
class ServerStatusTimer {
private intervalId: NodeJS.Timeout | null = null;
public interval: number;
private task: () => void;
constructor(task: () => void, interval: number) {
this.task = task;
this.interval = interval;
}
start(): void {
if (this.intervalId === null) {
this.intervalId = setInterval(() => {
this.task();
}, this.interval);
}
}
stop(): void {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
isRunning(): boolean {
return this.intervalId !== null;
}
}
export class InferenceManager implements ICompletions {
isReady: boolean = false
selectedModels: IModel[] = []
event: EventEmitter
modelCacheDir: string = undefined
serverCacheDir: string = undefined
private inferenceProcess: any=null
port = 5501
inferenceURL = 'http://127.0.0.1:' + this.port
private static instance=null
stateTimer: ServerStatusTimer
private constructor(modelDir:string) {
this.event = new EventEmitter()
this.modelCacheDir = path.join(modelDir, 'models')
this.serverCacheDir = path.join(modelDir, 'inferenceServer')
this.stateTimer= new ServerStatusTimer(() => { this._processStatus()}, 20000)
}
static getInstance(modelDir:string){
if (!InferenceManager.instance) {
// check if ther is a process already running
if (!modelDir) {
console.error('model directory is required to create InferenceManager instance')
return null
}
console.log('Creating new InferenceManager instance')
InferenceManager.instance = new InferenceManager(modelDir)
}
return InferenceManager.instance
}
// init the backend with a new model
async init(model:IModel) {
try {
await this._downloadModel(model)
if (model.downloadPath === undefined) {
console.log('Model not downloaded or not found')
return
}
console.log('Model downloaded at', model.downloadPath)
if (this.inferenceProcess === null) await this._startServer()
// check if resources are met before initializing the models
this._handleResources(true)
console.log('Initializing model request', model.modelType)
switch (model.modelType) {
case ModelType.CODE_COMPLETION_INSERTION || ModelType.CODE_COMPLETION:{
console.log('Initializing Completion Model')
const res = await this._makeRequest('init_completion', { model_path: model.downloadPath })
console.log('code completion res is', res?.data?.status)
if (res?.data?.status === "success") {
this.isReady = true
console.log('Completion Model initialized successfully')
} else {
this.isReady = false
console.error('Error initializing the model', res.data?.error)
}
break;
}
case ModelType.GENERAL:{
const res = await this._makeRequest('init', { model_path: model.downloadPath })
if (res.data?.status === "success") {
this.isReady = true
console.log('General Model initialized successfully')
} else {
this.isReady = false
console.error('Error initializing the model', res.data?.error)
}
break;
}
}
this.stateTimer.start() // double call on init completion and general
this.selectedModels.push(model)
} catch (error) {
console.error('Error initializing the model', error)
this.isReady = false
InferenceManager.instance = null
}
}
async _processStatus() {
// check if the server is running
const options = { headers: { 'Content-Type': 'application/json', } }
const state = await axios.get(this.inferenceURL+"/state", options)
if (!state.data?.status) {
console.log('Inference server not running')
InferenceManager.instance = null
this.stateTimer.interval += this.stateTimer.interval
if (this.stateTimer.interval >= 60000) {
// attempt to restart the server
console.log('Attempting to restart the server')
this.stopInferenceServer()
this._startServer()
this.stateTimer.interval = 20000
}
} else {
// Server is running with successful request
// console.log('Inference server is running')
// console.log('completion is runnig', state.data?.completion)
// console.log('general is runnig', state.data?.general)
}
// this._handleResources()
}
async _handleResources(logger:boolean=false) {
// check resrource usage
const options = { headers: { 'Content-Type': 'application/json', } }
const res = await axios.get(this.inferenceURL+"/sys", options)
if (res.data?.status) {
const max_memory = res.data.memory.total
const used_memory = res.data.memory.used
const memory_usage = res.data.memory.percent * 100
const gpu_available = res.data.gpus
for (const model of this.selectedModels) {
if (model.modelReqs.minSysMemory > max_memory) {
if (logger) console.warn('Insufficient memory for the model')
}
if (model.modelReqs.minSysMemory > used_memory) {
if (logger) console.warn('Insufficient memory for the model')
}
if (model.modelReqs.GPURequired) {
if (gpu_available.length < 1) {
if (logger)console.warn('GPU requiredfor desktop inference but not available')
}
}
}
}
}
async _downloadModel(model:IModel): Promise<string> {
if (this.modelCacheDir === undefined) {
console.log('Model cache directory not provided')
return
} else {
const outputLocationPath = path.join(this.modelCacheDir, model.modelName);
console.log('output location path is', outputLocationPath)
if (fs.existsSync(outputLocationPath)) {
model.downloadPath = outputLocationPath
console.log('Model already exists in the output location', outputLocationPath);
return;
}
console.log('Downloading model from', model.downloadUrl);
// Make a HEAD request to get the file size
const { headers } = await axios.head(model.downloadUrl);
const totalSize = parseInt(headers['content-length'], 10);
// Create a write stream to save the file
const writer = fs.createWriteStream(outputLocationPath);
// Start the file download
const response = await axios({
method: 'get',
url: model.downloadUrl,
responseType: 'stream'
});
let downloadedSize = 0;
response.data.on('data', (chunk: Buffer) => {
downloadedSize += chunk.length;
const progress = (Number((downloadedSize / totalSize) * 100).toFixed(2));
console.log(`Downloaded ${progress}%`);
this.event.emit('download', progress);
});
response.data.pipe(writer);
this.event.emit('ready')
model.downloadPath = outputLocationPath
console.log('LLama Download complete');
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
}
}
private async _downloadInferenceServer() {
const execPath = this._getServerPath()
try {
if (fs.existsSync(execPath)) {
console.log('Inference server already downloaded')
return true
} else {
downloadLatestReleaseExecutable(process.platform, this.serverCacheDir)
if (fs.existsSync(execPath)) {return true } else {return false}
}
} catch (error) {
console.error('Error downloading Inference server:', error)
return false
}
}
private _getServerPath() {
// get cpu arch
const arch = process.arch
let exec_suffix = ''
if (arch === 'x64') {
exec_suffix = 'x64'
} else if (arch === 'arm' || arch === 'arm64') {
exec_suffix = 'arm'
} else {
throw new Error('Unsupported CPU architecture')
}
// get platform name and return the path to the python script
let exec_name = ''
if (process.platform === 'win32') {
exec_name = 'InferenceServer-' + process.platform + '.exe'
} else if (process.platform === 'linux') {
exec_name = 'InferenceServer-' + process.platform + '_' + exec_suffix
} else if (process.platform === 'darwin') {
exec_name = 'InferenceServer-' + 'mac'
} else {
throw new Error('Unsupported platform')
}
return path.join(this.serverCacheDir, exec_name);
}
private async _handleExistingServer() {
// check if the server is already running, kill it
try {
const options = { headers: { 'Content-Type': 'application/json', } }
const state = await axios.get(this.inferenceURL+"/state", options)
if (state.data?.status) {
console.log('Found existing Inference server running')
this.stopInferenceServer()
await axios.post(this.inferenceURL+"/kill", options)
}
} catch (error) {
// catch connection refused
console.log('No existing Inference server running')
}
}
private async _startServer() {
const serverAvailable = await this._downloadInferenceServer()
if (!serverAvailable) {
console.error('Inference server not available for this platform')
return
}
// kill existing server if running
this._handleExistingServer()
return new Promise<void>((resolve, reject) => {
let serverPath = ""
try {
serverPath = this._getServerPath();
fs.chmodSync(serverPath, '755')
} catch (error) {
console.error('Error script path:', error);
return reject(error)
}
// Check if the file exists
if (!fs.existsSync(serverPath)) {
return reject(new Error(`Inference server not found at ${serverPath}`));
}
// Check file permissions
try {
fs.accessSync(serverPath, fs.constants.X_OK);
} catch (err) {
reject(new Error(`No execute permission on ${serverPath}`));
}
const spawnArgs = [this.port];
// console.log(`Spawning process: ${serverPath} ${spawnArgs.join(' ')}`);
this.inferenceProcess = spawn(serverPath, spawnArgs);
this.inferenceProcess.stdout.on('data', (data) => {
console.log(`Inference server output: ${data}`);
if (data.includes('Running on http://')) {
console.log('Inference server started successfully');
resolve();
}
});
this.inferenceProcess.stderr.on('data', (data) => {
console.error(`Inference log: ${data}`);
if (data.includes('Address already in use')) {
console.error(`Port ${this.port} is already in use. Please stop the existing server and try again`);
reject(new Error(`Port ${this.port} is already in use`));
}
resolve();
});
this.inferenceProcess.on('error', (err) => {
console.error('Failed to start Inference server:', err);
reject(err);
});
this.inferenceProcess.on('close', (code) => {
console.log(`Inference server process exited with code ${code}`);
if (code !== 0) {
reject(new Error(`Inference server exited with code ${code}`));
}
});
});
}
stopInferenceServer() {
if (this.inferenceProcess) {
this.inferenceProcess.kill();
this.inferenceProcess = null;
}
}
private async _makeInferenceRequest(endpoint, payload, rType:AIRequestType){
try {
this.event.emit('onInference')
const options = { headers: { 'Content-Type': 'application/json', } }
const response = await axios.post(`${this.inferenceURL}/${endpoint}`, payload, options)
const userPrompt = payload[Object.keys(payload)[0]]
this.event.emit('onInferenceDone')
if (response.data?.generatedText) {
if (rType === AIRequestType.GENERAL) {
ChatHistory.pushHistory(userPrompt, response.data.generatedText)
}
return response.data.generatedText
} else { return "" }
} catch (error) {
ChatHistory.clearHistory()
console.error('Error making request to Inference server:', error.message);
}
}
private async _streamInferenceRequest(endpoint, payload){
try {
this.event.emit('onInference')
const options = { headers: { 'Content-Type': 'application/json', } }
const response = await axios({
method: 'post',
url: `${this.inferenceURL}/${endpoint}`,
data: payload,
headers: {
"Content-Type": "application/json",
"Accept": "text/event-stream",
}
, responseType: 'stream' });
const userPrompt = payload[Object.keys(payload)[0]]
let resultText = ""
response.data.on('data', (chunk: Buffer) => {
try {
const parsedData = JSON.parse(chunk.toString());
if (parsedData.isGenerating) {
this.event.emit('onStreamResult', parsedData.generatedText);
resultText = resultText + parsedData.generatedText
} else {
resultText = resultText + parsedData.generatedText
// no additional check for streamed results
ChatHistory.pushHistory(userPrompt, resultText)
return parsedData.generatedText
}
} catch (error) {
ChatHistory.clearHistory()
console.error('Error parsing JSON:', error);
}
});
return "" // return empty string for now as payload is/will be handled in event
} catch (error) {
ChatHistory.clearHistory()
console.error('Error making stream request to Inference server:', error.message);
}
finally {
this.event.emit('onInferenceDone')
}
}
private async _makeRequest(endpoint, payload){
// makes a simple request to the inference server
try {
const options = { headers: { 'Content-Type': 'application/json', } }
const response = await axios.post(`${this.inferenceURL}/${endpoint}`, payload, options)
this.event.emit('onInferenceDone')
return response
} catch (error) {
console.error('Error making request to Inference server:', error.message);
}
}
async code_completion(context: any, params:IParams=CompletionParams): Promise<any> {
if (!this.isReady) {
console.log('model not ready yet')
return
}
// as of now no prompt required
const payload = { context_code: context, ...params }
return this._makeInferenceRequest('code_completion', payload, AIRequestType.COMPLETION)
}
async code_insertion(msg_pfx: string, msg_sfx: string, params:IParams=InsertionParams): Promise<any> {
if (!this.isReady) {
console.log('model not ready yet')
return
}
const payload = { code_pfx:msg_pfx, code_sfx:msg_sfx, ...params }
return this._makeInferenceRequest('code_insertion', payload, AIRequestType.COMPLETION)
}
async code_generation(prompt: string, params:IParams=GenerationParams): Promise<any> {
if (!this.isReady) {
console.log('model not ready yet')
return
}
return this._makeInferenceRequest('code_generation', { prompt, ...params }, AIRequestType.GENERAL)
}
async code_explaining(code:string, context:string, params:IParams=GenerationParams): Promise<any> {
if (!this.isReady) {
console.log('model not ready yet')
return
}
if (params.stream_result) {
return this._streamInferenceRequest('code_explaining', { code, context, ...params })
} else {
return this._makeInferenceRequest('code_explaining', { code, context, ...params }, AIRequestType.GENERAL)
}
}
async error_explaining(prompt: string, params:IParams=GenerationParams): Promise<any>{
if (!this.isReady) {
console.log('model not ready yet')
return ""
}
if (params.stream_result) {
return this._streamInferenceRequest('error_explaining', { prompt, ...params })
} else {
return this._makeInferenceRequest('error_explaining', { prompt, ...params }, AIRequestType.GENERAL)
}
}
async solidity_answer(userPrompt: string, params:IParams=GenerationParams): Promise<any> {
if (!this.isReady) {
console.log('model not ready yet')
return
}
let modelOP = undefined
for (const model of this.selectedModels) {
if (model.modelType === ModelType.GENERAL) {
modelOP = model.modelOP
}
}
const prompt = buildSolgptPromt(userPrompt, modelOP)
if (params.stream_result) {
return this._streamInferenceRequest('solidity_answer', { prompt, ...params })
} else {
return this._makeInferenceRequest('solidity_answer', { prompt, ...params }, AIRequestType.GENERAL)
}
}
}

@ -1,4 +1,4 @@
import {EventEmitter} from 'events';
import { EventEmitter } from 'events';
import { StringDecoder } from 'string_decoder';
// Max duration to batch session data before sending it to the renderer process.
const BATCH_DURATION_MS = 16;

@ -0,0 +1,115 @@
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import { Profile } from "@remixproject/plugin-utils"
// use remix ai core
import { InferenceManager } from "../lib/InferenceServerManager"
import { cacheDir } from "../utils/config"
import { RemoteInferencer } from "@remix/remix-ai-core"
// import { isE2E } from "../main";
const profile = {
name: 'remixAID',
displayName: 'RemixAI Desktop',
maintainedBy: 'Remix',
description: 'RemixAI provides AI services to Remix IDE Desktop.',
kind: '',
documentation: 'https://remix-ide.readthedocs.io/en/latest/remixai.html',
}
export class RemixAIDesktopPlugin extends ElectronBasePlugin {
clients: RemixAIDesktopPluginClient[] = []
constructor() {
super(profile, clientProfile, RemixAIDesktopPluginClient)
this.methods = [...super.methods]
}
}
const clientProfile: Profile = {
name: 'remixAID',
displayName: 'RemixAI Desktop',
maintainedBy: 'Remix',
description: 'RemixAI provides AI services to Remix IDE Desktop.',
kind: '',
documentation: 'https://remix-ide.readthedocs.io/en/latest/remixai.html',
methods: ['initializeModelBackend', 'code_completion', 'code_insertion', 'code_generation', 'code_explaining', 'error_explaining', 'solidity_answer']
}
class RemixAIDesktopPluginClient extends ElectronBasePluginClient {
readonly modelCacheDir: string = cacheDir
desktopInferencer:InferenceManager | RemoteInferencer = null
constructor (webContentsId: number, profile: Profile){
super(webContentsId, profile)
}
async onActivation(): Promise<void> {
this.onload(() => {
})
}
async enable (){
console.log('Remix AI desktop plugin enabled')
this.emit('enabled')
}
async initializeModelBackend(local, generalModel?, completionModel?){
if (!local){
this.desktopInferencer = new RemoteInferencer()
} else if (generalModel || completionModel){
if (!this.desktopInferencer){
this.desktopInferencer = InferenceManager.getInstance(this.modelCacheDir)
if (this.desktopInferencer instanceof InferenceManager && generalModel) await this.desktopInferencer.init(generalModel)
if (this.desktopInferencer instanceof InferenceManager && completionModel) await this.desktopInferencer.init(completionModel)
} else {
return false // do not set event listener twice
}
} else {
throw new Error('No model provided')
}
// set event listeners
this.desktopInferencer.event.on('onStreamResult', (data) => {
this.emit('onStreamResult', data)
})
this.desktopInferencer.event.on('onInference', () => {
this.emit('onInference')
})
this.desktopInferencer.event.on('onInferenceDone', () => {
this.emit('onInferenceDone')
})
return true
}
async code_completion(context: any) {
// use general purpose model
return this.desktopInferencer.code_completion(context)
}
async code_insertion(msg_pfx: string, msg_sfx: string) {
return this.desktopInferencer.code_insertion(msg_pfx, msg_sfx)
}
async code_generation(prompt: string) {
return this.desktopInferencer.code_generation(prompt)
}
async code_explaining(code:string, context?:string) {
return this.desktopInferencer.code_explaining(code, context)
}
async error_explaining(prompt: string) {
return this.desktopInferencer.error_explaining(prompt)
}
async solidity_answer(prompt: string) {
return this.desktopInferencer.solidity_answer(prompt)
}
changemodel(newModel: any){
/// dereference the current static inference object
/// set new one
}
}

@ -6,7 +6,7 @@ console.log('preload.ts', new Date().toLocaleTimeString())
/* preload script needs statically defined API for each plugin */
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater', 'slither', 'foundry', 'hardhat']
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater', 'slither', 'foundry', 'hardhat', 'remixAID']
let webContentsId: number | undefined

@ -14,6 +14,12 @@ export const createDefaultConfigLocations = async() => {
if (!fs.existsSync(cacheDir + '/compilers')) {
fs.mkdirSync(cacheDir + '/compilers')
}
if (!fs.existsSync(cacheDir + '/models')) {
fs.mkdirSync(cacheDir + '/models')
}
if (!fs.existsSync(cacheDir + '/inferenceServer')) {
fs.mkdirSync(cacheDir + '/inferenceServer')
}
if (!fs.existsSync(cacheDir + '/remixdesktop.json')) {
console.log('create config file')
fs.writeFileSync(cacheDir + '/remixdesktop.json', JSON.stringify({}))

@ -21,6 +21,9 @@
"@remix-git": [
"../../libs/remix-git/"
],
"@remix/remix-ai-core": [
"../../libs/remix-ai-core/src/index"
]
},
"typeRoots": [
"src/**/*.d.ts",

@ -4,7 +4,7 @@ Vyper Plugin for Remix IDE.
## How to get started
### Remote plugin
This plugin is hosted at https://vyper.remixproject.org .
This plugin is hosted at https://remix-ide.readthedocs.io/en/latest/vyper.html .
To use it, open the plugin manager in Remix and click the `Activate` button in front of the `Vyper` button in the plugin section.
### Local plugin

@ -72,7 +72,7 @@ export class RemixClient extends PluginClient<any, CustomRemixApi> {
${message}
can you explain why this error occurred and how to fix it?
`
await this.client.call('solcoder' as any, 'error_explaining', message)
await this.client.call('remixAI' as any, 'error_explaining', message)
} catch (err) {
console.error('unable to askGpt')
console.error(err)

@ -0,0 +1 @@
{ "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] }

@ -0,0 +1,7 @@
# remix-ai-core
[![npm version](https://badge.fury.io/js/%40remix-project%2Fremixd.svg)](https://www.npmjs.com/package/@remix-project/remixd)
[![npm](https://img.shields.io/npm/dt/@remix-project/remixd.svg?label=Total%20Downloads&logo=npm)](https://www.npmjs.com/package/@remix-project/remixd)
[![npm](https://img.shields.io/npm/dw/@remix-project/remixd.svg?logo=npm)](https://www.npmjs.com/package/@remix-project/remixd)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,32 @@
{
"name": "remix-ai-core",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/remix-ai-core/src",
"projectType": "library",
"implicitDependencies": [
],
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/remix-ai-core",
"main": "libs/remix-ai-core/src/index.ts",
"tsConfig": "libs/remix-ai-core/tsconfig.lib.json",
"updateBuildableProjectDepsInPackageJson": false,
"assets": [
"libs/remix-ai-core/*.md"
]
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/remix-ai-core/**/*.ts"],
"eslintConfig": "libs/remix-ai-core/.eslintrc"
}
}
},
"tags": []
}

@ -0,0 +1,29 @@
// interactive code explaining and highlight security vunerabilities
import * as fs from 'fs';
class CodeExplainAgent {
private codebase: string[]; // list of code base file
public currentFile: string;
constructor(codebasePath: string) {
// git or fs
this.codebase = this.loadCodebase(codebasePath);
}
private loadCodebase(path: string): string[] {
const files = fs.readdirSync(path);
return files
.filter(file => file.endsWith('.ts'))
.flatMap(file => fs.readFileSync(`${path}/${file}`, 'utf-8').split('\n'));
}
public update(currentFile, lineNumber){
}
public getExplanations(currentLine: string, numSuggestions: number = 3): string[] {
// process the code base explaining the current file and highlight some details
const suggestions: string[] = [];
return suggestions;
}
}

@ -0,0 +1,23 @@
import * as fs from 'fs';
class CodeCompletionAgent {
private codebase: string[];
constructor(codebasePath: string) {
// git or fs
this.codebase = this.loadCodebase(codebasePath);
}
private loadCodebase(path: string): string[] {
const files = fs.readdirSync(path);
return files
.filter(file => file.endsWith('.ts'))
.flatMap(file => fs.readFileSync(`${path}/${file}`, 'utf-8').split('\n'));
}
public getSuggestions(currentLine: string, numSuggestions: number = 3): string[] {
const suggestions: string[] = [];
// get `numSuggestions` from the llm
return suggestions;
}
}

@ -0,0 +1,29 @@
// security checks
import * as fs from 'fs';
class SecurityAgent {
private codebase: string[]; // list of code base file
public currentFile: string;
constructor(codebasePath: string) {
// git or fs
this.codebase = this.loadCodebase(codebasePath);
}
private loadCodebase(path: string): string[] {
const files = fs.readdirSync(path);
return files
.filter(file => file.endsWith('.ts'))
.flatMap(file => fs.readFileSync(`${path}/${file}`, 'utf-8').split('\n'));
}
public update(currentFile, lineNumber){
}
public getRecommendations(currentLine: string, numSuggestions: number = 3): string[] {
// process the code base highlighting security vunerabilities and deliver recommendations
const suggestions: string[] = [];
return suggestions;
}
}

@ -0,0 +1,55 @@
import axios from 'axios';
import fs from 'fs';
import path from 'path';
interface Asset {
name: string;
browser_download_url: string;
}
interface Release {
assets: Asset[];
}
const owner = 'remix-project-org'
const repo = 'remix_ai_tools'
async function getLatestRelease(owner: string, repo: string): Promise<Release> {
const url = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
const response = await axios.get(url);
return response.data;
}
async function downloadFile(url: string, filePath: string): Promise<void> {
const writer = fs.createWriteStream(filePath);
const response = await axios({
url,
method: 'GET',
responseType: 'stream'
});
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
}
export async function downloadLatestReleaseExecutable(platform: string, outputDir: string): Promise<void> {
try {
const release = await getLatestRelease(owner, repo);
const executables = release.assets.filter(asset =>
asset.name.includes(platform)
);
console.log(`Downloading executables for ${platform}..., ${executables} `);
for (const executable of executables) {
const filePath = path.join(outputDir, executable.name);
console.log(`Downloading ${executable.name}...`);
await downloadFile(executable.browser_download_url, filePath);
console.log(`Downloaded ${executable.name}`);
}
} catch (error) {
console.error('Error downloading executables:', error);
}
}

@ -0,0 +1,20 @@
'use strict'
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 { getCompletionPrompt, getInsertionPrompt } from './prompts/completionPrompts'
import { buildSolgptPromt, PromptBuilder } from './prompts/promptBuilder'
import { RemoteInferencer } from './inferencers/remote/remoteInference'
import { ChatHistory } from './prompts/chat'
import { downloadLatestReleaseExecutable } from './helpers/inferenceServerReleases'
export {
IModel, IModelResponse, IModelRequest, InferenceModel,
ModelType, DefaultModels, ICompletions, IParams, IRemoteModel,
getCompletionPrompt, getInsertionPrompt, IStreamResponse, buildSolgptPromt,
RemoteInferencer, InsertionParams, CompletionParams, GenerationParams,
ChatEntry, AIRequestType, RemoteBackendOPModel, ChatHistory, downloadLatestReleaseExecutable
}

@ -0,0 +1,141 @@
import { ICompletions, IParams, AIRequestType, RemoteBackendOPModel } from "../../types/types";
import { buildSolgptPromt } from "../../prompts/promptBuilder";
import axios from "axios";
import EventEmitter from "events";
import { ChatHistory } from "../../prompts/chat";
const defaultErrorMessage = `Unable to get a response from AI server`
export class RemoteInferencer implements ICompletions {
api_url: string
completion_url: string
max_history = 7
model_op = RemoteBackendOPModel.CODELLAMA // default model operation change this to llama if necessary
event: EventEmitter
constructor(apiUrl?:string, completionUrl?:string) {
this.api_url = apiUrl!==undefined ? apiUrl: "https://solcoder.remixproject.org"
this.completion_url = completionUrl!==undefined ? completionUrl : "https://completion.remixproject.org"
this.event = new EventEmitter()
}
private async _makeRequest(data, rType:AIRequestType){
this.event.emit("onInference")
const requesURL = rType === AIRequestType.COMPLETION ? this.completion_url : this.api_url
const userPrompt = data.data[0]
try {
const result = await axios(requesURL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
data: JSON.stringify(data),
})
switch (rType) {
case AIRequestType.COMPLETION:
if (result.statusText === "OK")
return result.data.data[0]
else {
return defaultErrorMessage
}
case AIRequestType.GENERAL:
if (result.statusText === "OK") {
const resultText = result.data.data[0]
ChatHistory.pushHistory(userPrompt, resultText)
return resultText
} else {
return defaultErrorMessage
}
}
} catch (e) {
ChatHistory.clearHistory()
console.error('Error making request to Inference server:', e.message)
return e
}
finally {
this.event.emit("onInferenceDone")
}
}
private async _streamInferenceRequest(data, rType:AIRequestType){
try {
this.event.emit('onInference')
const requesURL = rType === AIRequestType.COMPLETION ? this.completion_url : this.api_url
const userPrompt = data.data[0]
const response = await axios({
method: 'post',
url: requesURL,
data: data,
headers: { 'Content-Type': 'application/json', "Accept": "text/event-stream" },
responseType: 'stream'
});
let resultText = ""
response.data.on('data', (chunk: Buffer) => {
try {
const parsedData = JSON.parse(chunk.toString());
if (parsedData.isGenerating) {
this.event.emit('onStreamResult', parsedData.generatedText);
resultText = resultText + parsedData.generatedText
} else {
// stream generation is complete
resultText = resultText + parsedData.generatedText
ChatHistory.pushHistory(userPrompt, resultText)
return parsedData.generatedText
}
} catch (error) {
console.error('Error parsing JSON:', error);
ChatHistory.clearHistory()
}
});
return "" // return empty string for now as handled in event
} catch (error) {
ChatHistory.clearHistory()
console.error('Error making stream request to Inference server:', error.message);
}
finally {
this.event.emit('onInferenceDone')
}
}
async code_completion(prompt, options:IParams=null): Promise<any> {
const payload = !options?
{ "data": [prompt, "code_completion", "", false, 30, 0.9, 0.90, 50]} :
{ "data": [prompt, "code_completion", "", options.stream_result,
options.max_new_tokens, options.temperature, options.top_p, options.top_k]
}
return this._makeRequest(payload, AIRequestType.COMPLETION)
}
async code_insertion(msg_pfx, msg_sfx): Promise<any> {
const payload = { "data":[msg_pfx, "code_insertion", msg_sfx, 1024, 0.5, 0.92, 50]}
return this._makeRequest(payload, AIRequestType.COMPLETION)
}
async code_generation(prompt): Promise<any> {
const payload = { "data":[prompt, "code_completion", "", false,1000,0.9,0.92,50]}
return this._makeRequest(payload, AIRequestType.COMPLETION)
}
async solidity_answer(prompt): Promise<any> {
const main_prompt = buildSolgptPromt(prompt, this.model_op)
const payload = { "data":[main_prompt, "solidity_answer", false,2000,0.9,0.8,50]}
return this._makeRequest(payload, AIRequestType.GENERAL)
}
async code_explaining(prompt, context:string=""): Promise<any> {
const payload = { "data":[prompt, "code_explaining", false,2000,0.9,0.8,50, context]}
return this._makeRequest(payload, AIRequestType.GENERAL)
}
async error_explaining(prompt): Promise<any> {
const payload = { "data":[prompt, "error_explaining", false,2000,0.9,0.8,50]}
return this._makeRequest(payload, AIRequestType.GENERAL)
}
}

@ -0,0 +1,21 @@
import { ChatEntry } from "../types/types"
export abstract class ChatHistory{
private static chatEntries:ChatEntry[] = []
static queuSize:number = 7 // change the queue size wrt the GPU size
public static pushHistory(prompt, result){
const chat:ChatEntry = [prompt, result]
this.chatEntries.push(chat)
if (this.chatEntries.length > this.queuSize){this.chatEntries.shift()}
}
public static getHistory(){
return this.chatEntries
}
public static clearHistory(){
this.chatEntries = []
}
}

@ -0,0 +1,18 @@
import { COMPLETION_SYSTEM_PROMPT } from "../types/constants";
import { IModel } from "../types/types";
export const getInsertionPrompt = (model:IModel, msg_pfx, msg_sfx) => {
if ((model.modelType === 'code_completion_insertion') && (model.modelName.toLocaleLowerCase().includes('deepseek'))){
return `'<|fim▁begin|>' ${msg_pfx} '<|fim▁hole|>' ${msg_sfx} '<|fim▁end|>'`
}
else {
// return error model not supported yet
}
}
export const getCompletionPrompt = (model:IModel, context) => {
if ((model.modelType === 'code_completion') && (model.modelName.toLocaleLowerCase().includes('deepseek'))){
return `{COMPLETION_SYSTEM_PROMPT} \n### Instruction:\n{context}\n ### Response: `
}
}

@ -0,0 +1,28 @@
import { RemoteBackendOPModel } from "../types/types"
import { ChatHistory } from "./chat"
export const PromptBuilder = (inst, answr, modelop) => {
if (modelop === RemoteBackendOPModel.CODELLAMA) return `<|start_header_id|>user<|end_header_id|>${inst}<|eot_id|><|start_header_id|>assistant<|end_header_id|> ${answr}`
if (modelop === RemoteBackendOPModel.DEEPSEEK) return "\n### INSTRUCTION:\n" + inst + "\n### RESPONSE:\n" + answr
if (modelop === RemoteBackendOPModel.MISTRAL) return ""
}
export const buildSolgptPromt = (userPrompt:string, modelOP:RemoteBackendOPModel) => {
if (modelOP === undefined) {
console.log('WARNING: modelOP is undefined. Provide a valide model OP for chat history')
return userPrompt
}
if (ChatHistory.getHistory().length === 0){
return userPrompt
} else {
let newPrompt = ""
for (const [question, answer] of ChatHistory.getHistory()) {
if (question.startsWith('sol-gpt')) newPrompt += PromptBuilder(question.split('sol-gpt')[1], answer, modelOP)
else if (question.startsWith('gpt')) newPrompt += PromptBuilder(question.split('gpt')[1], answer, modelOP)
else newPrompt += PromptBuilder(question, answer, modelOP)
}
// finaly
newPrompt = "sol-gpt " + newPrompt + PromptBuilder(userPrompt.split('gpt')[1], "", modelOP)
return newPrompt
}
}

@ -0,0 +1,9 @@
/// constants for modelselection
export enum ModelType {
CODE_COMPLETION = 'code_completion',
GENERAL = 'general',
CODE_COMPLETION_INSERTION = 'code_completion_insertion',
}
export const COMPLETION_SYSTEM_PROMPT = "You are a Solidity AI Assistant that complete user code with provided context. You provide accurate solution and always answer as helpfully as possible, while being safe. You only provide code using this context:\n"

@ -0,0 +1,81 @@
// create a list of supported models
// create a function getModels returning a list of all supported models
// create a function getModel returning a model by its name
import { IModel, IParams, RemoteBackendOPModel } from './types';
import { ModelType } from './constants';
const DefaultModels = (): IModel[] => {
const model1:IModel = {
name: 'DeepSeek',
modelOP: RemoteBackendOPModel.DEEPSEEK,
task: 'text-generation',
modelName: 'deepseek-coder-6.7b-instruct-q4.gguf',
downloadUrl: 'https://drive.usercontent.google.com/download?id=13sz7lnEhpQ6EslABpAKl2HWZdtX3d9Nh&confirm=xxx',
modelType: ModelType.GENERAL,
modelReqs: { backend: 'llamacpp', minSysMemory: 8, GPURequired: false, MinGPUVRAM: 8 }
};
const model2: IModel = {
name: 'DeepSeek',
modelOP: RemoteBackendOPModel.DEEPSEEK,
task: 'text-generation',
modelName: 'deepseek-coder-1.3b-base-q4.gguf',
downloadUrl: 'https://drive.usercontent.google.com/download?id=13UNJuB908kP0pWexrT5n8i2LrhFaWo92&confirm=xxx',
modelType: ModelType.CODE_COMPLETION_INSERTION,
modelReqs: { backend: 'llamacpp', minSysMemory: 2, GPURequired: false, MinGPUVRAM: 2 }
};
const model3: IModel = {
name: 'llaama3.1_8B',
modelOP: RemoteBackendOPModel.CODELLAMA,
task: 'text-generation',
modelName: 'llama3_1_8B-q4_0.gguf',
downloadUrl: 'https://drive.usercontent.google.com/download?id=1I376pl8uORDnUIjfNuqhExK4NCiH3F12&confirm=xxx',
modelType: ModelType.GENERAL,
modelReqs: { backend: 'llamacpp', minSysMemory: 8, GPURequired: false, MinGPUVRAM: 8 }
};
const model4: IModel = {
name: 'llaama3.1_8B_instruct',
modelOP: RemoteBackendOPModel.CODELLAMA,
task: 'text-generation',
modelName: 'llama3_1_8B-q4_0_instruct.gguf',
downloadUrl: 'https://drive.usercontent.google.com/download?id=1P-MEH7cPxaR20v7W1qbOEPBzgiY2RDLx&confirm=xxx',
modelType: ModelType.GENERAL,
modelReqs: { backend: 'llamacpp', minSysMemory: 8, GPURequired: false, MinGPUVRAM: 8 }
};
return [model1, model2, model3, model4];
}
const getModel = async (name: string): Promise<IModel | undefined> => {
return DefaultModels().find(model => model.name === name);
}
const loadModel = async (modelname: string): Promise<void> => {
console.log(`Loading model ${modelname}`);
}
const CompletionParams:IParams = {
temperature: 0.8,
topK: 40,
topP: 0.92,
max_new_tokens: 15,
}
const InsertionParams:IParams = {
temperature: 0.8,
topK: 40,
topP: 0.92,
max_new_tokens: 150,
}
const GenerationParams:IParams = {
temperature: 0.5,
topK: 40,
topP: 0.92,
max_new_tokens: 2000,
stream_result: false,
}
export { DefaultModels, CompletionParams, InsertionParams, GenerationParams }

@ -0,0 +1,87 @@
// model implementation for the model selection component
import exp from 'constants';
import { ModelType } from './constants';
export interface IModelRequirements{
backend: string,
minSysMemory: number,
GPURequired: boolean,
MinGPUVRAM: number,
}
export interface IModel {
name: string;
task: string;
downloadUrl: string;
modelName: string;
modelType: ModelType;
modelReqs: IModelRequirements;
downloadPath?: string;
modelOP?: RemoteBackendOPModel;
}
export interface IRemoteModel {
completionUrl: string;
apiUrl: string;
}
export interface IModelResponse {
output: string;
error: string;
success: boolean;
model: IModel;
}
export interface IStreamResponse {
generatedText: string;
isGenerating: boolean;
}
export interface IModelRequest {
input: string;
model: IModel;
}
export interface InferenceModel {
model: IModel;
location: string;
isRemote: boolean;
}
export interface ICompletions{
code_completion(context, params:IParams): Promise<any>;
code_insertion(msg_pfx, msg_sfx, params:IParams): Promise<any>;
}
export interface IParams {
temperature?: number;
max_new_tokens?: number;
repetition_penalty?: number;
repeatPenalty?:any
no_repeat_ngram_size?: number;
num_beams?: number;
num_return_sequences?: number;
top_k?: number;
top_p?: number;
stream_result?: boolean;
return_full_text?: boolean;
nThreads?: number;
nTokPredict?: number;
topK?: number;
topP?: number;
temp?: number;
}
export enum AIRequestType {
COMPLETION,
GENERAL
}
export type ChatEntry = [string, string];
export enum RemoteBackendOPModel{
DEEPSEEK,
CODELLAMA,
MISTRAL
}

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node"],
"module": "commonjs",
"esModuleInterop": true,
"outDir": "./dist",
},
"include": ["**/*.ts"]
}

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"declaration": true,
"rootDir": "./src",
"types": ["node"]
},
"exclude": [
"**/*.spec.ts",
"test/"
],
"include": ["**/*.ts"]
}

@ -0,0 +1,23 @@
import { IParams } from "@remix/remix-ai-core";
import { StatusEvents } from "@remixproject/plugin-utils";
export interface IRemixAID {
events: {
activated():void,
onInference():void,
onInferenceDone():void,
onStreamResult(streamText: string):void,
} & StatusEvents,
methods: {
code_completion(context: string): Promise<string>
code_insertion(msg_pfx: string, msg_sfx: string): Promise<string>,
code_generation(prompt: string): Promise<string | null>,
code_explaining(code: string, context?: string): Promise<string | null>,
error_explaining(prompt: string): Promise<string | null>,
solidity_answer(prompt: string): Promise<string | null>,
initializeModelBackend(local: boolean, generalModel?, completionModel?): Promise<boolean>,
chatPipe(pipeMessage: string): Promise<void>,
ProcessChatRequestBuffer(params:IParams): Promise<void>,
}
}

@ -0,0 +1,21 @@
import { IModel, IParams, IRemoteModel } from "@remix/remix-ai-core";
import { StatusEvents } from "@remixproject/plugin-utils";
export interface IRemixAI {
events: {
onStreamResult(streamText: string): Promise<void>,
activated(): Promise<void>,
} & StatusEvents,
methods: {
code_completion(context: string): Promise<string>
code_insertion(msg_pfx: string, msg_sfx: string): Promise<string>,
code_generation(prompt: string): Promise<string | null>,
code_explaining(code: string, context?: string): Promise<string | null>,
error_explaining(prompt: string): Promise<string | null>,
solidity_answer(prompt: string): Promise<string | null>,
initializeModelBackend(local: boolean, generalModel?, completionModel?): Promise<void>,
chatPipe(pipeMessage: string): Promise<void>,
ProcessChatRequestBuffer(params:IParams): Promise<void>,
initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean): Promise<void>,
}
}

@ -6,5 +6,6 @@ export interface IExtendedTerminalApi extends ITerminal {
} & StatusEvents
methods: ITerminal['methods'] & {
logHtml(html: string): void
log(message: any): void
}
}

@ -13,6 +13,9 @@ import { ISidePanelApi } from "./plugins/sidePanel-api"
import { IPinnedPanelApi } from "./plugins/pinned-panel-api"
import { ILayoutApi } from "./plugins/layout-api"
import { IMatomoApi } from "./plugins/matomo-api"
import { IRemixAI } from "./plugins/remixai-api"
import { IRemixAID } from "./plugins/remixAIDesktop-api"
export interface ICustomRemixApi extends IRemixApi {
dgitApi: IGitApi
@ -29,6 +32,8 @@ export interface ICustomRemixApi extends IRemixApi {
pinnedPanel: IPinnedPanelApi
layout: ILayoutApi
matomo: IMatomoApi
remixAI: IRemixAI,
remixAID: IRemixAID
}
export declare type CustomRemixApi = Readonly<ICustomRemixApi>

@ -3,5 +3,5 @@ export { dispatchModalContext, dispatchModalInterface, AppContext, appProviderCo
export { ModalProvider, useDialogDispatchers } from './lib/remix-app/context/provider'
export { AppModal } from './lib/remix-app/interface/index'
export { AlertModal } from './lib/remix-app/interface/index'
export { ModalTypes } from './lib/remix-app/types/index'
export { ModalTypes, AppModalCancelTypes } from './lib/remix-app/types/index'
export { AppAction, appActionTypes } from './lib/remix-app/actions/app'

@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl'
import { AppContext } from '../../context/context'
import { useDialogDispatchers } from '../../context/provider'
import { AppModalCancelTypes } from '../../types'
declare global {
interface Window {
_paq: any
@ -15,7 +16,7 @@ interface MatomoDialogProps {
}
const MatomoDialog = (props: MatomoDialogProps) => {
const { settings, showMatamo, appManager } = useContext(AppContext)
const { settings, showMatomo, appManager } = useContext(AppContext)
const { modal } = useDialogDispatchers()
const [visible, setVisible] = useState<boolean>(props.hide)
@ -63,7 +64,7 @@ const MatomoDialog = (props: MatomoDialogProps) => {
}
useEffect(() => {
if (visible && showMatamo) {
if (visible && showMatomo) {
modal({
id: 'matomoModal',
title: <FormattedMessage id="remixApp.matomoTitle" />,
@ -72,18 +73,22 @@ const MatomoDialog = (props: MatomoDialogProps) => {
okFn: handleModalOkClick,
cancelLabel: <FormattedMessage id="remixApp.decline" />,
cancelFn: declineModal,
preventBlur: true
})
}
}, [visible])
const declineModal = async () => {
const declineModal = async (reason: AppModalCancelTypes) => {
if (reason === AppModalCancelTypes.click || reason === AppModalCancelTypes.enter) {
settings.updateMatomoAnalyticsChoice(false)
// revoke tracking consent
_paq.push(['forgetConsentGiven'])
setVisible(false)
}
}
const handleModalOkClick = async () => {
// user has given consent to process their data
_paq.push(['setConsentGiven'])
settings.updateMatomoAnalyticsChoice(true)

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'
import { ModalDialog, ModalDialogProps, ValidationResult } from '@remix-ui/modal-dialog'
import { ModalTypes } from '../../types'
import { AppModalCancelTypes, ModalTypes } from '../../types'
interface ModalWrapperProps extends ModalDialogProps {
modalType?: ModalTypes
@ -42,8 +42,8 @@ const ModalWrapper = (props: ModalWrapperProps) => {
props.okFn ? props.okFn(data.current) : props.resolve(data.current || true)
}
const onCancelFn = async () => {
props.cancelFn ? props.cancelFn() : props.resolve(false)
const onCancelFn = async (reason?: AppModalCancelTypes) => {
props.cancelFn ? props.cancelFn(reason) : props.resolve(false)
}
const onInputChanged = (event) => {
@ -142,6 +142,8 @@ const ModalWrapper = (props: ModalWrapperProps) => {
props.handleHide()
}
if (!props.id || props.id === '') return null
return <ModalDialog id={props.id} {...state} handleHide={handleHide} />
}
export default ModalWrapper

@ -5,7 +5,7 @@ import { AppAction } from '../actions/app'
export type appProviderContextType = {
settings: any,
showMatamo: boolean,
showMatomo: boolean,
showEnter: boolean,
appManager: any
modal: any

@ -23,7 +23,7 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt
}
const modal = (modalData: AppModal) => {
const { id, title, message, validationFn, okLabel, okFn, cancelLabel, cancelFn, modalType, modalParentClass, defaultValue, hideFn, data } = modalData
const { id, title, message, validationFn, okLabel, okFn, cancelLabel, cancelFn, modalType, modalParentClass, defaultValue, hideFn, data, preventBlur } = modalData
return new Promise((resolve, reject) => {
dispatch({
type: modalActionTypes.setModal,
@ -42,7 +42,8 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt
hideFn,
resolve,
next: onNextFn,
data
data,
preventBlur
}
})
})

@ -1,5 +1,5 @@
import { GitHubUser } from '@remix-api'
import { ModalTypes } from '../types'
import { AppModalCancelTypes, ModalTypes } from '../types'
export type ValidationResult = {
valid: boolean,
@ -17,14 +17,15 @@ export interface AppModal {
okLabel: string | JSX.Element
okFn?: (value?:any) => void
cancelLabel?: string | JSX.Element
cancelFn?: () => void,
cancelFn?: (reason?: AppModalCancelTypes) => void,
modalType?: ModalTypes,
modalParentClass?: string
defaultValue?: string
hideFn?: () => void,
resolve?: (value?:any) => void,
next?: () => void,
data?: any
data?: any,
preventBlur?: boolean
}
export interface AlertModal {

@ -23,7 +23,8 @@ export const modalReducer = (state: ModalState = ModalInitialState, action: Moda
hideFn: action.payload.hideFn,
resolve: action.payload.resolve,
next: action.payload.next,
data: action.payload.data
data: action.payload.data,
preventBlur: action.payload.preventBlur
}
const modalList: AppModal[] = state.modals.slice()

@ -60,7 +60,7 @@ const RemixApp = (props: IRemixAppUi) => {
activateApp()
}
const hadUsageTypeAsked = localStorage.getItem('hadUsageTypeAsked')
if (props.app.showMatamo) {
if (props.app.showMatomo) {
// if matomo dialog is displayed, it will take care of calling "setShowEnterDialog",
// if the user approves matomo tracking.
// so "showEnterDialog" stays false
@ -149,7 +149,7 @@ const RemixApp = (props: IRemixAppUi) => {
const value: appProviderContextType = {
settings: props.app.settings,
showMatamo: props.app.showMatamo,
showMatomo: props.app.showMatomo,
appManager: props.app.appManager,
showEnter: props.app.showEnter,
modal: props.app.notification,

@ -8,6 +8,15 @@ export const enum ModalTypes {
forceChoice = 'forceChoice'
}
export const enum AppModalCancelTypes {
close = 'close',
cancel = 'cancel',
blur = 'blur',
escape = 'escape',
enter = 'enter',
click = 'click'
}
export const enum UsageTypes {
Beginner = 1,
Prototyper,

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

@ -1,33 +1,42 @@
/* 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';
import { activateService } from '@remixproject/plugin-utils';
const _paq = (window._paq = window._paq || [])
const controller = new AbortController();
const { signal } = controller;
const result: string = ''
export class RemixInLineCompletionProvider implements monacoTypes.languages.InlineCompletionsProvider {
props: EditorUIProps
monaco: any
completionEnabled: boolean
task: string
currentCompletion
currentCompletion: any
private lastRequestTime: number = 0;
private readonly minRequestInterval: number = 200;
constructor(props: any, monaco: any) {
this.props = props
this.monaco = monaco
this.completionEnabled = true
this.currentCompletion = {
text: '',
item: [],
task : this.task,
displayed: false,
accepted: false
}
}
async provideInlineCompletions(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.InlineCompletionContext, token: monacoTypes.CancellationToken): Promise<monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>> {
if (context.selectedSuggestionInfo) {
return;
return { items: []};
}
const currentTime = Date.now();
const timeSinceLastRequest = currentTime - this.lastRequestTime;
if (timeSinceLastRequest < this.minRequestInterval) {
return { items: []}; // dismiss the request
}
this.lastRequestTime = Date.now();
const getTextAtLine = (lineNumber) => {
const lineRange = model.getFullModelRange().setStartPosition(lineNumber, 1).setEndPosition(lineNumber + 1, 1);
return model.getValueInRange(lineRange);
@ -68,18 +77,20 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
const ask = split[split.length - 2].trimStart()
if (split[split.length - 1].trim() === '' && ask.startsWith('///')) {
// 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('///', '') })
this.props.plugin.call('terminal', 'log', { type: 'aitypewriterwarning', value: 'RemixAI - generating code for following comment: ' + ask.replace('///', '') })
const data = await this.props.plugin.call('remixAI', 'code_insertion', word, word_after)
this.task = 'code_generation'
const data = await this.props.plugin.call('solcoder', 'code_generation', word)
const parsedData = data[0].trimStart() //JSON.parse(data).trimStart()
const parsedData = data.trimStart() //JSON.parse(data).trimStart()
const item: monacoTypes.languages.InlineCompletion = {
insertText: parsedData
};
this.currentCompletion.text = parsedData
this.currentCompletion.item = item
return {
items: [item],
enableForwardStability: true
enableForwardStability: false
}
}
} catch (e) {
@ -93,7 +104,7 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
word.split('\n').at(-1).trimStart().startsWith('*/') ||
word.split('\n').at(-1).endsWith(';')
){
return; // do not do completion on single and multiline comment
return { items: []}; // do not do completion on single and multiline comment
}
// abort if there is a signal
@ -101,28 +112,22 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
return
}
// abort if the completion is not enabled
if (!this.completionEnabled) {
return
}
if (word.replace(/ +$/, '').endsWith('\n')){
// Code insertion
try {
const output = await this.props.plugin.call('remixAI', 'code_insertion', word, word_after)
const generatedText = output // no need to clean it. should already be
this.task = 'code_insertion'
const output = await this.props.plugin.call('solcoder', 'code_insertion', word, word_after)
const generatedText = output[0] // no need to clean it. should already be
const item: monacoTypes.languages.InlineCompletion = {
insertText: generatedText
};
this.completionEnabled = false
const handleCompletionTimer = new CompletionTimer(100, () => { this.completionEnabled = true });
handleCompletionTimer.start()
this.currentCompletion.text = generatedText
this.currentCompletion.item = item
return {
items: [item],
enableForwardStability: true
enableForwardStability: false,
}
}
catch (err){
@ -130,12 +135,11 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
}
}
let result
try {
// Code completion
this.task = 'code_completion'
const output = await this.props.plugin.call('solcoder', 'code_completion', word)
const generatedText = output[0]
const output = await this.props.plugin.call('remixAI', 'code_completion', word)
const generatedText = output
let clean = generatedText
if (generatedText.indexOf('@custom:dev-run-script./') !== -1) {
@ -145,13 +149,10 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
clean = this.process_completion(clean)
const item: monacoTypes.languages.InlineCompletion = {
insertText: clean
insertText: clean,
};
// handle the completion timer by locking suggestions request for 2 seconds
this.completionEnabled = false
const handleCompletionTimer = new CompletionTimer(100, () => { this.completionEnabled = true });
handleCompletionTimer.start()
this.currentCompletion.text = clean
this.currentCompletion.item = item
return {
items: [item],
@ -175,11 +176,14 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
}
handleItemDidShow?(completions: monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>, item: monacoTypes.languages.InlineCompletion, updatedInsertText: string): void {
this.currentCompletion = { 'item':item, 'task':this.task }
_paq.push(['trackEvent', 'ai', 'solcoder', this.task + '_did_show'])
this.currentCompletion.displayed = true
this.currentCompletion.task = this.task
_paq.push(['trackEvent', 'ai', 'remixAI', this.task + '_did_show'])
}
handlePartialAccept?(completions: monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>, item: monacoTypes.languages.InlineCompletion, acceptedCharacters: number): void {
_paq.push(['trackEvent', 'ai', 'solcoder', this.task + '_partial_accept'])
this.currentCompletion.accepted = true
this.currentCompletion.task = this.task
_paq.push(['trackEvent', 'ai', 'remixAI', this.task + '_partial_accept'])
}
freeInlineCompletions(completions: monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>): void {
}

@ -708,8 +708,7 @@ export const EditorUI = (props: EditorUIProps) => {
const changes = e.changes;
// Check if the change matches the current completion
if (changes.some(change => change.text === inlineCompletionProvider.currentCompletion.item.insertText)) {
_paq.push(['trackEvent', 'ai', 'solcoder', inlineCompletionProvider.currentCompletion.task + '_accepted'])
inlineCompletionProvider.currentCompletion = null;
_paq.push(['trackEvent', 'ai', 'remixAI', inlineCompletionProvider.currentCompletion.task + '_accepted'])
}
}
});
@ -777,12 +776,11 @@ export const EditorUI = (props: EditorUIProps) => {
const file = await props.plugin.call('fileManager', 'getCurrentFile')
const content = await props.plugin.call('fileManager', 'readFile', file)
const message = intl.formatMessage({ id: 'editor.generateDocumentationByAI' }, { content, currentFunction: currentFunction.current })
const cm = await props.plugin.call('solcoder', 'code_explaining', message)
const cm = await await props.plugin.call('remixAI', 'code_explaining', message)
const natSpecCom = "\n" + extractNatspecComments(cm)
const cln = await props.plugin.call('codeParser', "getLineColumnOfNode", currenFunctionNode)
const range = new monacoRef.current.Range(cln.start.line, cln.start.column, cln.start.line, cln.start.column)
const lines = natSpecCom.split('\n')
const newNatSpecCom = []
@ -813,7 +811,7 @@ export const EditorUI = (props: EditorUIProps) => {
},
]);
_paq.push(['trackEvent', 'ai', 'solcoder', 'generateDocumentation'])
_paq.push(['trackEvent', 'ai', 'remixAI', 'generateDocumentation'])
},
}
@ -831,8 +829,8 @@ export const EditorUI = (props: EditorUIProps) => {
const file = await props.plugin.call('fileManager', 'getCurrentFile')
const content = await props.plugin.call('fileManager', 'readFile', file)
const message = intl.formatMessage({ id: 'editor.explainFunctionByAI' }, { content, currentFunction: currentFunction.current })
await props.plugin.call('solcoder', 'code_explaining', message, content)
_paq.push(['trackEvent', 'ai', 'solcoder', 'explainFunction'])
await props.plugin.call('remixAI', 'code_explaining', message, content)
_paq.push(['trackEvent', 'ai', 'remixAI', 'explainFunction'])
},
}
@ -851,8 +849,8 @@ export const EditorUI = (props: EditorUIProps) => {
const content = await props.plugin.call('fileManager', 'readFile', file)
const selectedCode = editor.getModel().getValueInRange(editor.getSelection())
await props.plugin.call('solcoder', 'code_explaining', selectedCode, content)
_paq.push(['trackEvent', 'ai', 'solcoder', 'explainFunction'])
await props.plugin.call('remixAI', 'code_explaining', selectedCode, content)
_paq.push(['trackEvent', 'ai', 'remixAI', 'explainFunction'])
},
}

@ -113,30 +113,6 @@ function HomeTabFeatured(props:HomeTabFeaturedProps) {
</a>
</div>
</div>
<div className="mr-1 pr-1 d-flex align-items-center justify-content-center h-100">
<a href="https://docs.google.com/forms/d/e/1FAIpQLSd0WsJnKbeJo-BGrnf7WijxAdmE4PnC_Z4M0IApbBfHLHZdsQ/viewform" target="__blank">
<img src={'assets/img/remixRewardBetaTester_small.webp'} className="remixui_carouselImage_remixbeta" alt=""></img>
</a>
<div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: '1' }}>
<h5>
<FormattedMessage id="home.betaTesting" />
</h5>
<p style={{ fontStyle: 'italic', fontSize: '1rem' }}>
<FormattedMessage id="home.betaTestingText1" />
</p>
<div style={{ fontSize: '0.8rem' }} className="mb-3">
<FormattedMessage id="home.betaTestingText2" />
</div>
<a
className="remixui_home_text btn-sm btn-secondary mt-2 text-decoration-none mb-3"
onClick={() => _paq.push(['trackEvent', 'hometab', 'featuredSection', 'betatesting'])}
target="__blank"
href="https://docs.google.com/forms/d/e/1FAIpQLSd0WsJnKbeJo-BGrnf7WijxAdmE4PnC_Z4M0IApbBfHLHZdsQ/viewform"
>
<FormattedMessage id="home.betaTestingMore" />
</a>
</div>
</div>
</Carousel>
</ThemeContext.Provider>
</div>

@ -125,12 +125,6 @@
width: 20rem;
}
.remixui_carouselImage_remixbeta {
flex: 1;
height: 20rem;
width: 18rem;
}
.remixui_carouselbox {
min-height: 25.12rem;
}

@ -2,6 +2,7 @@ import React, {useRef, useState, useEffect} from 'react' // eslint-disable-line
import {ModalDialogProps} from './types' // eslint-disable-line
import './remix-ui-modal-dialog.css'
import { AppModalCancelTypes } from '@remix-ui/app'
declare global {
// eslint-disable-next-line no-unused-vars
@ -24,24 +25,26 @@ export const ModalDialog = (props: ModalDialogProps) => {
}
useEffect(() => {
if (!props.id) return
calledHideFunctionOnce.current = props.hide
if (!props.hide) {
modal.current.focus()
if (modal.current) {
modal.current.removeEventListener('blur', handleBlur)
if (modal.current && !props.preventBlur) {
modal.current.addEventListener('blur', handleBlur)
}
}
return () => {
modal.current && modal.current.removeEventListener('blur', handleBlur)
}
}, [props.hide])
function handleBlur(e) {
if (!e.currentTarget.contains(e.relatedTarget)) {
if (e.currentTarget && !e.currentTarget.contains(e.relatedTarget)) {
e.stopPropagation()
if (document.activeElement !== this) {
!window.testmode && handleHide()
!window.testmode && props.cancelFn && props.cancelFn()
!window.testmode && props.cancelFn && props.cancelFn(AppModalCancelTypes.blur)
}
}
}
@ -49,7 +52,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
const modalKeyEvent = (keyCode) => {
if (keyCode === 27) {
// Esc
if (props.cancelFn) props.cancelFn()
if (props.cancelFn) props.cancelFn(AppModalCancelTypes.escape)
handleHide()
} else if (keyCode === 13) {
// Enter
@ -71,7 +74,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
if (state.toggleBtn) {
if (props.okFn) props.okFn()
} else {
if (props.cancelFn) props.cancelFn()
if (props.cancelFn) props.cancelFn(AppModalCancelTypes.enter)
}
handleHide()
}
@ -99,7 +102,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
{props.title && props.title}
</h6>
{!props.showCancelIcon && (
<span className="modal-close" onClick={() => handleHide()}>
<span data-id={`${props.id}-modal-close`} className="modal-close" onClick={() => handleHide()}>
<i className="fas fa-times" aria-hidden="true"></i>
</span>
)}
@ -130,7 +133,7 @@ export const ModalDialog = (props: ModalDialogProps) => {
className={'modal-cancel btn btn-sm ' + (props.cancelBtnClass ? props.cancelBtnClass : state.toggleBtn ? 'border-secondary' : 'border-primary')}
data-dismiss="modal"
onClick={() => {
if (props.cancelFn) props.cancelFn()
if (props.cancelFn) props.cancelFn(AppModalCancelTypes.click)
handleHide()
}}
>

@ -1,3 +1,5 @@
import { AppModalCancelTypes } from "@remix-ui/app"
export type ValidationResult = {
valid: boolean,
message?: string
@ -15,7 +17,7 @@ export interface ModalDialogProps {
okFn?: (value?:any) => void,
donotHideOnOkClick?: boolean,
cancelLabel?: string | JSX.Element,
cancelFn?: () => void,
cancelFn?: (reason?: AppModalCancelTypes) => void,
modalClass?: string,
modalParentClass?: string
showCancelIcon?: boolean,
@ -26,5 +28,6 @@ export interface ModalDialogProps {
next?: () => void,
data?: any,
okBtnClass?: string,
cancelBtnClass?: string
cancelBtnClass?: string,
preventBlur?: boolean
}

@ -0,0 +1 @@
export { RemixAITab } from './lib/components/RemixAI'

@ -0,0 +1,84 @@
import React, { useContext, useEffect, useState } from 'react'
import '../remix-ai.css'
import { DefaultModels } from '@remix/remix-ai-core';
export const Default = (props) => {
const [searchText, setSearchText] = useState('');
const [resultText, setResultText] = useState('');
const pluginName = 'remixAI'
const appendText = (newText) => {
setResultText(resultText => resultText + newText);
}
useEffect(() => {
const handleResultReady = async (e) => {
appendText(e);
};
if (props.plugin.isOnDesktop ) {
props.plugin.on(props.plugin.remixDesktopPluginName, 'onStreamResult', (value) => {
handleResultReady(value);
})
}
}, [])
return (
<div>
<div className="remix_ai_plugin_search_container">
<input
type="text"
className="remix_ai_plugin_search-input"
placeholder="Search..."
value={searchText}
onChange={() => console.log('searchText not implememted')}
></input>
<button
className="remix_ai_plugin_search_button text-ai pl-2 pr-0 py-0 d-flex"
onClick={() => console.log('searchText not implememted')}
>
<i
className="fa-solid fa-arrow-right"
style={{ color: 'black' }}
></i>
<span className="position-relative text-ai text-sm pl-1"
style={{ fontSize: "x-small", alignSelf: "end" }}>Search</span>
</button>
<button className="remix_ai_plugin_download_button text-ai pl-2 pr-0 py-0 d-flex"
onClick={async () => {
if (props.plugin.isOnDesktop ) {
await props.plugin.call(pluginName, 'downloadModel', DefaultModels()[3]);
}
}}
> Download Model </button>
</div>
<div className="remix_ai_plugin_find_container_internal">
<textarea
className="remix_ai_plugin_search_result_textbox"
rows={10}
cols={50}
placeholder="Results..."
onChange={(e) => {
console.log('resultText changed', e.target.value)
setResultText(e.target.value)}
}
value={resultText}
readOnly
/>
<button className="remix_ai_plugin_download_button text-ai pl-2 pr-0 py-0 d-flex"
onClick={async () => {
props.plugin.call("remixAI", 'initialize', DefaultModels()[1], DefaultModels()[3]);
}}
> Init Model </button>
</div>
<div className="remix_ai_plugin_find-part">
<a href="#" className="remix_ai_plugin_search_result_item_title">/fix the problems in my code</a>
<a href="#" className="remix_ai_plugin_search_result_item_title">/tests add unit tests for my code</a>
<a href="#" className="remix_ai_plugin_search_result_item_title">/explain how the selected code works</a>
</div>
</div>
);
}

@ -0,0 +1,78 @@
// UI interface for selecting a model from a list of models
// This component is used in the ModelSelectionModal component
// It is a dropdown list of models that the user can select from
// The user can also search for a specific model by typing in the search bar
// The user can also filter the models by type
// The user can select a model from the dropdown list
// the panel controlling the model selection can be hidden or shown
// Once selected, the model is either loaded from the local storage or downloaded
// the remix ai desktop plugin provided the interface for storing the model in the local storage after downloading
import React, { useState, useEffect } from 'react';
import { Select, Input, Button, Icon } from 'antd';
import { IModel } from '@remix/remix-ai-core';
import { DefaultModels } from '@remix/remix-ai-core';
import { ModelType } from '@remix/remix-ai-core';
import { useTranslation } from 'react-i18next';
const { Option } = Select;
const { Search } = Input;
interface ModelSelectionProps {
onSelect: (model: IModel) => void;
}
export const ModelSelection: React.FC<ModelSelectionProps> = ({ onSelect }) => {
const { t } = useTranslation();
const [models, setModels] = useState<IModel[]>([]);
const [filteredModels, setFilteredModels] = useState<IModel[]>([]);
const [search, setSearch] = useState<string>('');
const [type, setType] = useState<ModelType | 'all'>('all');
useEffect(() => {
setModels(DefaultModels());
}, []);
useEffect(() => {
setFilteredModels(models.filter((model) => {
return model.name.toLowerCase().includes(search.toLowerCase()) &&
(type === 'all' || model.modelType === type);
}));
}, [models, search, type]);
return (
<div>
<Search
placeholder={t('search_models')}
onChange={(e) => setSearch(e.target.value)}
style={{ width: 200, marginBottom: 10 }}
/>
<Select
defaultValue="all"
style={{ width: 200, marginBottom: 10 }}
onChange={(value) => setType(value)}
>
<Option value="all">{t('all_models')}</Option>
<Option value={ModelType.IMAGE}>{t('image_models')}</Option>
<Option value={ModelType.TEXT}>{t('text_models')}</Option>
<Option value={ModelType.AUDIO}>{t('audio_models')}</Option>
</Select>
<Select
showSearch
style={{ width: 200 }}
placeholder={t('select_model')}
optionFilterProp="children"
onChange={(value) => onSelect(models.find((model) => model.name === value))}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{filteredModels.map((model) => (
<Option key={model.name} value={model.name}>
{model.name}
</Option>
))}
</Select>
</div>
);
};

@ -0,0 +1,15 @@
import React, { useContext } from 'react'
import '../remix-ai.css'
import { Default } from './Default'
export const RemixAITab = (props) => {
const plugin = props.plugin
return (
<>
<div id="remixAITab pr-4 px-2 pb-4">
<Default plugin={plugin}></Default>
</div>
</>
)
}

@ -0,0 +1,167 @@
/* Existing CSS */
.remix_ai_plugin_search_result_item_title {
display: flex;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
cursor: pointer;
align-items: center;
color: #58a6ff;
text-decoration: none;
font-size: 1.2em;
margin: 10px 0;
}
.remix_ai_plugin_search_result_item_title:hover {
text-decoration: underline;
}
.remix_ai_plugin_wrap_summary {
overflow: hidden;
white-space: nowrap;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
cursor: pointer;
}
.remix_ai_plugin_find-part {
display: flex;
flex-direction: column;
padding-top: 5px;
}
.remix_ai_plugin_controls {
display: flex;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_line_container {
display: flex;
flex-direction: row;
position: relative;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_line {
width: 100%;
overflow: hidden;
display: flex;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_control {
flex-grow: 0;
position: absolute;
right: 0px;
top: 0px;
}
.remix_ai_plugin_summary_right {
min-width: 0;
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_replace_strike {
text-decoration: line-through;
}
.remix_ai_plugin_summary_left {
white-space: pre;
}
.remix_ai_plugin_search_tab mark {
padding: 0;
white-space: pre;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_line_container .remix_ai_plugin_search_control {
display: none;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_line_container:hover .remix_ai_plugin_search_control {
display: block;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_line_container:hover .remix_ai_plugin_search_line {
width: 93%;
}
.remix_ai_plugin_search-input {
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
margin: 10px 0;
width: 100%;
max-width: 500px;
border: 1px solid #ccc;
border-radius: 4px;
}
.remix_ai_plugin_search_tab .checked {
background-color: var(--secondary);
}
.remix_ai_plugin_search_tab .remix_ai_plugin_search_file_name {
text-overflow: ellipsis;
overflow: hidden;
text-transform: uppercase;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_result_count {
flex-grow: 1;
text-align: right;
display: flex;
justify-content: flex-end;
}
.remix_ai_plugin_search_tab .remix_ai_plugin_result_count_number {
font-size: x-small;
}
.remix_ai_plugin_search_container {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 20px;
}
.remix_ai_plugin_search_container_internal {
display: flex;
flex-direction: column;
flex-grow: 1;
margin-top: 20px;
align-items: center;
}
.remix_ai_plugin_search_container_arrow {
display: flex !important;
align-items: center;
cursor: pointer !important;
}
.remix_ai_plugin_wrap_summary_replace {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.remix_ai_plugin_search_indicator {
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
}
.remix_ai_plugin_search_result_textbox {
width: 100%;
max-width: 500px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
margin: 10px 0;
color: #333;
}

@ -75,8 +75,8 @@ export const Renderer = ({ message, opt = {}, plugin }: RendererProps) => {
try {
const content = await plugin.call('fileManager', 'readFile', editorOptions.errFile)
const message = intl.formatMessage({ id: 'solidity.openaigptMessage' }, { content, messageText })
await plugin.call('solcoder', 'error_explaining', message)
_paq.push(['trackEvent', 'ai', 'solcoder', 'error_explaining_SolidityError'])
await plugin.call('remixAI', 'error_explaining', message)
_paq.push(['trackEvent', 'ai', 'remixAI', 'error_explaining_SolidityError'])
} catch (err) {
console.error('unable to askGtp')
console.error(err)

@ -5,6 +5,7 @@ import { CopyToClipboard } from '@remix-ui/clipboard'
import { AccountProps } from '../types'
import { PassphrasePrompt } from './passphrase'
import { CustomTooltip } from '@remix-ui/helper'
const _paq = window._paq = window._paq || []
export function AccountUI(props: AccountProps) {
const { selectedAccount, loadedAccounts } = props.accounts
@ -96,6 +97,7 @@ export function AccountUI(props: AccountProps) {
}
const signMessage = () => {
_paq.push(['trackEvent', 'udapp', 'signUsingAccount', `selectExEnv: ${selectExEnv}`])
if (!accounts[0]) {
return props.tooltip(intl.formatMessage({ id: 'udapp.tooltipText1' }))
}

@ -6,7 +6,6 @@ import { ContractData, FuncABI, OverSizeLimit } from '@remix-project/core-plugin
import * as ethJSUtil from '@ethereumjs/util'
import { ContractGUI } from './contractGUI'
import { CustomTooltip, deployWithProxyMsg, upgradeWithProxyMsg } from '@remix-ui/helper'
import { title } from 'process'
const _paq = (window._paq = window._paq || [])
export function ContractDropdownUI(props: ContractDropdownProps) {
@ -80,6 +79,7 @@ export function ContractDropdownUI(props: ContractDropdownProps) {
content: '',
})
if (!currentContract) enableAtAddress(false)
if (currentContract && loadedAddress) enableAtAddress(true)
} else {
setAbiLabel({
display: 'none',

@ -315,7 +315,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
</div>
<div className="custom-control custom-checkbox mb-1">
<input onChange={onchangeMatomoAnalytics} id="settingsMatomoAnalytics" type="checkbox" className="custom-control-input" checked={isMatomoChecked} />
<label className={`form-check-label custom-control-label align-middle ${getTextClass('settings/matomo-analytics')}`} htmlFor="settingsMatomoAnalytics">
<label data-id="label-matomo-settings" className={`form-check-label custom-control-label align-middle ${getTextClass('settings/matomo-analytics')}`} htmlFor="settingsMatomoAnalytics">
<span>
<FormattedMessage id="settings.matomoAnalytics" />
</span>
@ -477,7 +477,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
role='link'
onClick={()=>{
window.open("https://remix-ide.readthedocs.io/en/latest/ai.html")
_paq.push(['trackEvent', 'ai', 'solcoder', 'documentation'])
_paq.push(['trackEvent', 'ai', 'remixAI', 'documentation'])
}}
>
<i aria-hidden="true" className="fas fa-book"></i>

@ -41,6 +41,7 @@ export const copilotTemperature = (config, checked, dispatch) => {
export const useMatomoAnalytics = (config, checked, dispatch) => {
config.set('settings/matomo-analytics', checked)
localStorage.setItem('matomo-analytics-consent', Date.now().toString())
dispatch({ type: 'useMatomoAnalytics', payload: { isChecked: checked, textClass: checked ? textDark : textSecondary } })
if (checked) {
// user has given consent to process their data

@ -21,7 +21,8 @@
vertical-align: middle;
}
.tab:hover .close-tabs{
visibility: visible
visibility: visible;
cursor: pointer;
}
.active .close-tabs {
visibility: visible
@ -29,6 +30,7 @@
.close-tabs {
visibility: hidden;
font-size: medium;
cursor: pointer;
}
.iconImage {
width: 1rem;

@ -251,9 +251,9 @@ export const TabsUI = (props: TabsUIProps) => {
const content = await props.plugin.call('fileManager', 'readFile', path)
if (tabsState.currentExt === 'sol') {
setExplaining(true)
await props.plugin.call('solcoder', 'code_explaining', content)
await props.plugin.call('remixAI', 'code_explaining', content)
setExplaining(false)
_paq.push(['trackEvent', 'ai', 'solcoder', 'explain_file'])
_paq.push(['trackEvent', 'ai', 'remixAI', 'explain_file'])
}
}}
>
@ -283,7 +283,7 @@ export const TabsUI = (props: TabsUIProps) => {
onClick={async () => {
await props.plugin.call('settings', 'updateCopilotChoice', !ai_switch)
setAI_switch(!ai_switch)
ai_switch ? _paq.push(['trackEvent', 'ai', 'solcoder', 'copilot_enabled']) : _paq.push(['trackEvent', 'ai', 'solcoder', 'copilot_disabled'])
ai_switch ? _paq.push(['trackEvent', 'ai', 'remixAI', 'copilot_enabled']) : _paq.push(['trackEvent', 'ai', 'remixAI', 'copilot_disabled'])
}}
>
<i className={ai_switch ? "fas fa-toggle-on fa-lg" : "fas fa-toggle-off fa-lg"}></i>

@ -238,12 +238,12 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
// TODO: rm gpt or redirect gpt to sol-pgt
} else if (script.trim().startsWith('gpt')) {
call('terminal', 'log',{ type: 'warn', value: `> ${script}` })
await call('solcoder', 'solidity_answer', script)
_paq.push(['trackEvent', 'ai', 'solcoder', 'askFromTerminal'])
await call('remixAI', 'solidity_answer', script)
_paq.push(['trackEvent', 'ai', 'remixAI', 'askFromTerminal'])
} 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'])
await call('remixAI', 'solidity_answer', script)
_paq.push(['trackEvent', 'ai', 'remixAI', 'askFromTerminal'])
} else {
await call('scriptRunner', 'execute', script)
}

@ -193,6 +193,7 @@ export const createWorkspace = async (
}
}
}
await populateWorkspace(workspaceTemplateName, opts, isEmpty, (err: Error) => { cb && cb(err, workspaceName) }, isGitRepo, createCommit)
// this call needs to be here after the callback because it calls dGitProvider which also calls this function and that would cause an infinite loop
await plugin.setWorkspaces(await getWorkspaces())
@ -545,7 +546,6 @@ export const switchToWorkspace = async (name: string) => {
await plugin.fileProviders.workspace.setWorkspace(name)
await plugin.setWorkspace({ name, isLocalhost: false })
const isGitRepo = await plugin.fileManager.isGitRepo()
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace({ name, isGitRepo }))
dispatch(setReadOnlyMode(false))
@ -703,9 +703,10 @@ export const cloneRepository = async (url: string) => {
dispatch(cloneRepositoryRequest())
promise
.then(async () => {
if (!plugin.registry.get('platform').api.isDesktop()) {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
}
await fetchWorkspaceDirectory(ROOT_PATH)
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
const branches = await getGitRepoBranches(workspacesPath + '/' + repoName)
@ -790,7 +791,7 @@ export const getGitRepoCurrentBranch = async (workspaceName: string) => {
}
export const showAllBranches = async () => {
if (plugin.registry.get('platform').api.isDesktop()) return
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
plugin.call('menuicons', 'select', 'dgit')

@ -53,6 +53,20 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
icon: 'fa-solid fa-link',
placement: 'top',
platforms: [appPlatformTypes.web, appPlatformTypes.desktop]
},
{
action: 'connectToLocalFileSystem',
title: 'Connect to local filesystem using remixd',
icon: 'fa-solid fa-desktop',
placement: 'top',
platforms: [appPlatformTypes.web]
},
{
action: 'initializeWorkspaceAsGitRepo',
title: 'Initialize workspace as a git repository',
icon: 'fa-brands fa-git-alt',
placement: 'top',
platforms: [appPlatformTypes.web, appPlatformTypes.desktop]
}
].filter(
(item) =>
@ -136,6 +150,29 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
</label>
</CustomTooltip>
)
} else if (action === 'initializeWorkspaceAsGitRepo') {
return (
<CustomTooltip
placement={placement as Placement}
tooltipId="uploadFolderTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id={`filePanel.${action}`} defaultMessage={title} />}
key={`index-${action}-${placement}-${icon}`}
>
<label
id={action}
style={{ fontSize: '1.1rem', cursor: 'pointer' }}
data-id={'fileExplorerUploadFolder' + action}
className={icon + ' mx-1 remixui_menuItem'}
key={`index-${action}-${placement}-${icon}`}
onClick={() => {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.handleGitInit()
}}
>
</label>
</CustomTooltip>
)
} else {
return (
<CustomTooltip
@ -158,6 +195,9 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
props.createNewFolder()
} else if (action === 'publishToGist' || action == 'updateGist') {
props.publishToGist()
} else if (action === 'connectToLocalFileSystem') {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.connectToLocalFileSystem()
} else if (action === 'importFromIpfs') {
_paq.push(['trackEvent', 'fileExplorer', 'fileAction', action])
props.importFromIpfs('Ipfs', 'ipfs hash', ['ipfs://QmQQfBMkpDgmxKzYaoAtqfaybzfgGm9b2LWYyT56Chv6xH'], 'ipfs://')

@ -581,6 +581,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
const handleGitInit = async () => {
const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
await plugin.call('dgitApi', 'init')
}
return (
<div className="h-100 remixui_treeview" data-id="filePanelFileExplorerTree">
<div ref={treeRef} tabIndex={0} style={{
@ -609,6 +615,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
uploadFolder={uploadFolder}
importFromIpfs={props.importFromIpfs}
importFromHttps={props.importFromHttps}
connectToLocalFileSystem={() => props.connectToLocalFileSystem()}
handleGitInit={handleGitInit}
/>
</div>
</span>

@ -17,6 +17,7 @@ export interface HamburgerMenuProps {
downloadWorkspaces: () => void
restoreBackup: () => void
hideIconsMenu: (showMenu: boolean) => void
handleRemixdWorkspace: () => void
showIconsMenu: boolean
hideWorkspaceOptions: boolean
hideLocalhostOptions: boolean
@ -139,6 +140,17 @@ export function HamburgerMenu(props: HamburgerMenuProps) {
}}
platforms={[appPlatformTypes.web]}
></HamburgerMenuItem>
<Dropdown.Divider className="border mb-0 mt-0 remixui_menuhr" style={{ pointerEvents: 'none' }} />
<HamburgerMenuItem
kind="localFileSystem"
fa="far fa-desktop"
hideOption={hideWorkspaceOptions}
actionOnClick={() => {
props.handleRemixdWorkspace()
props.hideIconsMenu(!showIconsMenu)
}}
platforms={[appPlatformTypes.web]}
></HamburgerMenuItem>
</>
)
}

@ -2,6 +2,8 @@ import { branch } from '@remix-ui/git'
import { customAction } from '@remixproject/plugin-api'
import { createContext, SyntheticEvent } from 'react'
import { BrowserState } from '../reducers/workspace'
import { Plugin } from '@remixproject/engine'
import { CustomRemixApi } from '@remix-api'
export const FileSystemContext = createContext<{
fs: any,

@ -917,6 +917,86 @@ export function Workspace() {
_paq.push(['trackEvent', 'Workspace', 'GIT', 'login'])
}
const IsGitRepoDropDownMenuItem = (props: { isGitRepo: boolean, mName: string}) => {
return (
<>
{props.isGitRepo ? (
<div className="d-flex justify-content-between">
<span>{currentWorkspace === props.mName ? <span>&#10003; {props.mName} </span> : <span className="pl-3">{props.mName}</span>}</span>
<i className="fas fa-code-branch pt-1"></i>
</div>
) : (
<span>{currentWorkspace === props.mName ? <span>&#10003; {props.mName} </span> : <span className="pl-3">{props.mName}</span>}</span>
)}
</>
)
}
const ShowNonLocalHostMenuItems = () => {
const cachedFilter = global.fs.browser.workspaces.filter(x => !x.name.includes('localhost'))
return (
<>
{
currentWorkspace === LOCALHOST && cachedFilter.length > 0 ? cachedFilter.map(({ name, isGitRepo }, index) => (
<Dropdown.Item
key={index}
onClick={() => {
switchWorkspace(name)
}}
data-id={`dropdown-item-${name}`}
>
<IsGitRepoDropDownMenuItem isGitRepo={isGitRepo} mName={name} />
</Dropdown.Item>
)) : <ShowAllMenuItems />
}
</>
)
}
const ShowAllMenuItems = () => {
return (
<>
{ global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
<Dropdown.Item
key={index}
onClick={() => { switchWorkspace(name) }}
data-id={`dropdown-item-${name}`}
>
<IsGitRepoDropDownMenuItem isGitRepo={isGitRepo} mName={name} />
</Dropdown.Item>
))}
</>
)
}
const [togglerText, setTogglerText] = useState<'Connecting' | 'Connected to Local FileSystem'>('Connecting')
useEffect(() => {
setTimeout(() => {
setTogglerText('Connected to Local FileSystem')
}, 1000)
}, [selectedWorkspace])
const WorkspaceDropdownToggle = () => {
const [togglerText, setTogglerText] = useState<'Connecting' | 'Connected to Local FileSystem'>('Connecting')
useEffect(() => {
setTimeout(() => {
setTogglerText('Connected to Local FileSystem')
}, 1000)
}, [selectedWorkspace])
return (
<Dropdown.Toggle
as={CustomToggle}
id="dropdown-custom-components"
className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control mt-1"
icon={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null}
>
{selectedWorkspace ? selectedWorkspace.name === LOCALHOST ? togglerText : selectedWorkspace.name : currentWorkspace === LOCALHOST ? formatNameForReadonly('localhost') : NO_WORKSPACE}
</Dropdown.Toggle>
)
}
return (
<div className="d-flex flex-column justify-content-between h-100">
<div
@ -948,6 +1028,7 @@ export function Workspace() {
<HamburgerMenu
selectedWorkspace={selectedWorkspace}
createWorkspace={createWorkspace}
handleRemixdWorkspace={()=>switchWorkspace(LOCALHOST)}
createBlankWorkspace={createBlankWorkspace}
renameCurrentWorkspace={renameCurrentWorkspace}
downloadCurrentWorkspace={downloadCurrentWorkspace}
@ -1024,54 +1105,25 @@ export function Workspace() {
className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control mt-1"
icon={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null}
>
{selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? formatNameForReadonly('localhost') : NO_WORKSPACE}
{selectedWorkspace ? selectedWorkspace.name === LOCALHOST ? togglerText : selectedWorkspace.name : currentWorkspace === LOCALHOST ? formatNameForReadonly('localhost') : NO_WORKSPACE}
</Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className="w-100 custom-dropdown-items" data-id="custom-dropdown-items">
<Dropdown.Item
onClick={() => {
createWorkspace()
}}
>
{
<span className="pl-3">
{' '}
- <FormattedMessage id="filePanel.createNewWorkspace" /> -{' '}
</span>
}
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
switchWorkspace(LOCALHOST)
}}
>
{currentWorkspace === LOCALHOST ? (
<span>&#10003; localhost </span>
) : (
<span className="pl-3">
{' '}
<FormattedMessage id="filePanel.connectToLocalhost" />{' '}
</span>
)}
</Dropdown.Item>
{global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
<Dropdown.Item
key={index}
onClick={() => {
switchWorkspace(name)
}}
data-id={`dropdown-item-${name}`}
>
{isGitRepo ? (
<div className="d-flex justify-content-between">
<span>{currentWorkspace === name ? <span>&#10003; {name} </span> : <span className="pl-3">{name}</span>}</span>
<i className="fas fa-code-branch pt-1"></i>
</div>
<span>&#10003; Connected to Local Filesystem </span>
) : (
<span>{currentWorkspace === name ? <span>&#10003; {name} </span> : <span className="pl-3">{name}</span>}</span>
// <span className="pl-3">
// {' '}
// <FormattedMessage id="filePanel.connectToLocalhost" />{' '}
// </span>
null
)}
</Dropdown.Item>
))}
<ShowNonLocalHostMenuItems />
{(global.fs.browser.workspaces.length <= 0 || currentWorkspace === NO_WORKSPACE) && (
<Dropdown.Item
onClick={() => {
@ -1105,8 +1157,7 @@ export function Workspace() {
<FileExplorer
fileState={global.fs.browser.fileState}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', selectedWorkspace && selectedWorkspace.isGist ? 'updateGist' : 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '', 'importFromIpfs',
'importFromHttps']}
menuItems={['createNewFile', 'createNewFolder', selectedWorkspace && selectedWorkspace.isGist ? 'updateGist' : 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '', 'importFromIpfs','importFromHttps', 'connectToLocalFileSystem', 'initializeWorkspaceAsGitRepo']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
@ -1166,6 +1217,7 @@ export function Workspace() {
renamePath={editModeOn}
importFromIpfs={importFromUrl}
importFromHttps={importFromUrl}
connectToLocalFileSystem={()=>switchWorkspace(LOCALHOST)}
canPaste={canPaste}
hasCopied={hasCopied}
setHasCopied={setHasCopied}

@ -159,6 +159,8 @@ export interface FileExplorerProps {
dragStatus: (status: boolean) => void
importFromIpfs: any
importFromHttps: any
connectToLocalFileSystem?: any
handleGitInit?: () => Promise<void>
handleMultiCopies: any
feTarget: { key: string, type: 'file' | 'folder' }[]
setFeTarget: Dispatch<React.SetStateAction<{
@ -180,6 +182,8 @@ export interface FileExplorerMenuProps {
uploadFolder: (target: EventTarget & HTMLInputElement) => void
importFromIpfs: any
importFromHttps: any
connectToLocalFileSystem?: any
handleGitInit?: () => Promise<void>
tooltipPlacement?: Placement
}
export interface FileExplorerContextMenuProps {

68735
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -122,7 +122,6 @@
"@ricarso/react-image-magnifiers": "^1.9.0",
"@types/nightwatch": "^2.3.1",
"@web3modal/ethers5": "^4.0.1",
"@xenova/transformers": "^2.7.0",
"ansi-gray": "^0.1.1",
"assert": "^2.1.0",
"async": "^2.6.2",
@ -170,7 +169,6 @@
"merge": "^2.1.1",
"npm-install-version": "^6.0.2",
"octokit": "^3.1.2",
"openai": "^3.3.0",
"path-browserify": "^1.0.1",
"prettier": "^2.8.4",
"prettier-plugin-solidity": "^1.0.0-beta.24",
@ -278,6 +276,7 @@
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v3-core": "^1.0.1",
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
"@xenova/transformers": "^2.17.2",
"ace-mode-lexon": "^1.*.*",
"ace-mode-move": "0.0.1",
"ace-mode-solidity": "^0.1.0",
@ -351,6 +350,7 @@
"nx": "15.7.1",
"nyc": "^13.3.0",
"onchange": "^3.2.1",
"onnxruntime-web": "^1.18.0",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
"react-refresh": "^0.14.0",

@ -6,7 +6,7 @@ Release managers are responsible for the release management lifecycle, focusing
## Pre release planning:
In this stage, release manager and the team lead will elaborate a plan for the coming release.
This should take in account:
This should take into account:
- the current issues list (a fair amount of time should be taken to go over the github issues).
- the current critical bugs.
- the current roadmap.
@ -27,8 +27,8 @@ We check all the issues and associated effort and identify critical issues.
e.g:
- issues that'll need to be split.
- issues that miss important information.
- issues that are dependent each others.
- issued that require different skills or that the team member is less available during this release.
- issues that are dependent on each others.
- issues that require different skills or that the team member is less available during this release.
## Configuring releases:
Release managers will oversee the various aspects of a project before it is due to be deployed, ensuring everyone is on track and meeting the agreed timeline.
@ -56,7 +56,7 @@ The release manager is still responsible for ensuring a project is rolled out sm
## Detailed Responsibilities:
- Lead the daily standup meeting.
- 10 minutes or more are reserved at the end of the daily standup meeting where the release manager update the team on the opened PRs (PRs which aim to be delivered in the planned release).
- 10 minutes or more are reserved at the end of the daily standup meeting where the release manager update the team on the open PRs (PRs which aim to be delivered in the planned release).
- Regular check for new filed issues, identify those that require to be published (included in the release)
- In some really specific situations, it could be required to deploy intermediate releases (e.g critical bug fixes).
- Planning, refinement, retrospective meetings have to be organized by the release manager and any other required meetings.
@ -80,7 +80,7 @@ The release manager is still responsible for ensuring a project is rolled out sm
### coding period
- [ ] 10 min after each daily standup where the release manager give an update of the current situation and ETA.
- [ ] 10 min after each daily standup where the release manager gives an update of the current situation and ETA.
- [ ] release manager should make sure to be aware of the current state of each issues and PRs during the coding period in order to have a better overview of who is working on what and best provide support to all the team members that are involved in the release.
### QA preparation

@ -9,7 +9,7 @@ This document includes the release instructions for:
- Updating Remix's beta version on remix-beta.ethereum.org
## Feature Freeze
Once feature freeze is done, `remix_beta` should be updated latest to the master which will automatically update `remix-beta.ethereum.org` through a CI job.
Once feature freeze is done, `remix_beta` should be updated to the latest master which will automatically update `remix-beta.ethereum.org` through a CI job.
Use this unified command:
@ -45,7 +45,7 @@ This command will ask for a new version.
### Other libraries NPM release
`yarn run publish:libs `
`yarn run publish:libs`
This command uses `lerna` and is solely responsible for publishing all the remix libraries. It will ask for a new version of each library. Make sure you are logged in to NPM.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save