diff --git a/apps/quick-dapp-v2/.eslintrc b/apps/quick-dapp-v2/.eslintrc new file mode 100644 index 00000000000..2d85f9fa667 --- /dev/null +++ b/apps/quick-dapp-v2/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json", +} \ No newline at end of file diff --git a/apps/quick-dapp-v2/README.md b/apps/quick-dapp-v2/README.md new file mode 100644 index 00000000000..c17eee80fd8 --- /dev/null +++ b/apps/quick-dapp-v2/README.md @@ -0,0 +1 @@ +# Remix QuickDapp V2 Plugin diff --git a/apps/quick-dapp-v2/package.json b/apps/quick-dapp-v2/package.json new file mode 100644 index 00000000000..4824b3eb349 --- /dev/null +++ b/apps/quick-dapp-v2/package.json @@ -0,0 +1,15 @@ +{ + "name": "quick-dapp-v2", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@drafish/surge-client": "^1.1.5", + "cid": "multiformats/cid", + "esbuild-wasm": "^0.25.12", + "ethers": "^6.15.0", + "ipfs-http-client": "^47.0.1" + } +} diff --git a/apps/quick-dapp-v2/project.json b/apps/quick-dapp-v2/project.json new file mode 100644 index 00000000000..4e593d40a34 --- /dev/null +++ b/apps/quick-dapp-v2/project.json @@ -0,0 +1,70 @@ +{ + "name": "quick-dapp-v2", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/quick-dapp-v2/src", + "projectType": "application", + "implicitDependencies": [], + "targets": { + "build": { + "executor": "@nrwl/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "development", + "options": { + "compiler": "babel", + "outputPath": "dist/apps/quick-dapp-v2", + "index": "apps/quick-dapp-v2/src/index.html", + "baseHref": "./", + "main": "apps/quick-dapp-v2/src/main.tsx", + "polyfills": "apps/quick-dapp-v2/src/polyfills.ts", + "tsConfig": "apps/quick-dapp-v2/tsconfig.app.json", + "assets": ["apps/quick-dapp-v2/src/profile.json", "apps/quick-dapp-v2/src/assets/sparkling.png"], + "styles": ["apps/quick-dapp-v2/src/index.css"], + "scripts": [], + "webpackConfig": "apps/quick-dapp-v2/webpack.config.js" + }, + "configurations": { + "development": { + }, + "production": { + } + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/quick-dapp-v2/**/*.ts"], + "eslintConfig": "apps/quick-dapp-v2/.eslintrc" + } + }, + "install": { + "executor": "nx:run-commands", + "cache": false, + "options": { + "commands": [ + "cd apps/quick-dapp-v2 && yarn" + ], + "parallel": false + } + }, + "serve": { + "executor": "@nrwl/webpack:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "quick-dapp-v2:build", + "hmr": true, + "baseHref": "/" + }, + "configurations": { + "development": { + "buildTarget": "quick-dapp-v2:build:development", + "port": 2025 + }, + "production": { + "buildTarget": "quick-dapp-v2:build:production" + } + } + } + }, + "tags": [] +} diff --git a/apps/quick-dapp-v2/src/App.css b/apps/quick-dapp-v2/src/App.css new file mode 100644 index 00000000000..8298749eca3 --- /dev/null +++ b/apps/quick-dapp-v2/src/App.css @@ -0,0 +1,127 @@ +/* You can add global styles to this file, and also import other style files */ + +.item-wrapper { + transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0) + scaleX(var(--scale-x, 1)) scaleY(var(--scale-y, 1)); + transform-origin: 0 0; + touch-action: manipulation; + + &:hover { + .item-remove { + visibility: visible; + } + } +} + +.item-remove { + visibility: hidden; + top: 5px; + right: 5px; + width: 20px; + height: 20px; + background-color: var(--gray-dark); + + .fas { + color: var(--text-bg-mark); + } +} + +.item-action { + touch-action: none; + outline: none !important; + appearance: none; + background-color: transparent; + -webkit-tap-highlight-color: transparent; + + @media (hover: hover) { + &:hover { + background-color: var(--bs-light); + } + } + + .fas { + color: var(--text-bg-mark); + } +} + +.bg-light { + .item-action { + @media (hover: hover) { + &:hover { + background-color: var(--dark); + } + } + } +} + +.container { + flex-direction: column; + + &.placeholder { + justify-content: center; + align-items: center; + cursor: pointer; + } +} + + +.container-header { + &:hover { + .container-actions > * { + opacity: 1 !important; + } + } +} + +.container-actions { + > *:first-child:not(:last-child) { + opacity: 0; + + &:focus-visible { + opacity: 1; + } + } +} + +.instance-input { + background-color: var(--custom-select) !important; + font-size: 10px; +} +.has-args { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.udapp_intro { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + white-space: pre-wrap; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.udapp_intro:hover { + -webkit-line-clamp: inherit; +} +.cursor_pointer { + cursor: pointer; +} +.cursor_pointer:hover { + color: var(--bs-secondary); +} +.custom-dropdown-items { + padding: 0.25rem 0.25rem; + border-radius: .25rem; + background: var(--custom-select); +} + +.custom-dropdown-items a { + border-radius: .25rem; + text-transform: none; + text-decoration: none; + font-weight: normal; + font-size: 0.875rem; + padding: 0.25rem 0.25rem; + width: auto; + color: var(--text); +} diff --git a/apps/quick-dapp-v2/src/App.tsx b/apps/quick-dapp-v2/src/App.tsx new file mode 100644 index 00000000000..d8567bd6294 --- /dev/null +++ b/apps/quick-dapp-v2/src/App.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useReducer, useState } from 'react'; +import { IntlProvider } from 'react-intl' +import CreateInstance from './components/CreateInstance'; +import EditInstance from './components/EditInstance'; +import EditHtmlTemplate from './components/EditHtmlTemplate'; +import DeployPanel from './components/DeployPanel'; +import LoadingScreen from './components/LoadingScreen'; +import { appInitialState, appReducer } from './reducers/state'; +import { + connectRemix, + initDispatch, + updateState, + selectTheme, +} from './actions'; +import { AppContext } from './contexts'; +import remixClient from './remix-client'; +import './App.css'; + +function App(): JSX.Element { + const [locale, setLocale] = useState<{code: string; messages: any}>({ + code: 'en', + messages: null, + }) + const [appState, dispatch] = useReducer(appReducer, appInitialState); + const { isAiLoading } = appState; + + useEffect(() => { + updateState(appState); + }, [appState]); + useEffect(() => { + initDispatch(dispatch); + updateState(appState); + connectRemix().then(() => { + remixClient.call('theme', 'currentTheme').then((theme: any) => { + selectTheme(theme.name); + }); + remixClient.on('theme', 'themeChanged', (theme: any) => { + selectTheme(theme.name); + }); + // @ts-ignore + remixClient.call('locale', 'currentLocale').then((locale: any) => { + setLocale(locale) + }) + // @ts-ignore + remixClient.on('locale', 'localeChanged', (locale: any) => { + setLocale(locale) + }) + // @ts-ignore + remixClient.on('ai-dapp-generator', 'generationProgress', (progress: any) => { + if (progress.status === 'started') { + dispatch({ type: 'SET_AI_LOADING', payload: true }); + } + }); + // @ts-ignore + remixClient.on('ai-dapp-generator', 'dappGenerated', () => { + dispatch({ type: 'SET_AI_LOADING', payload: false }); + }); + // @ts-ignore + remixClient.on('ai-dapp-generator', 'dappUpdated', () => { + dispatch({ type: 'SET_AI_LOADING', payload: false }); + }); + }); + }, []); + return ( + + + {appState.instance.htmlTemplate ? ( +
+ +
+ ) : Object.keys(appState.instance.abi).length > 0 ? ( +
+ + +
+ ) : ( +
+ +
+ )} + +
+
+ ); +} + +export default App; diff --git a/apps/quick-dapp-v2/src/InBrowserVite.tsx b/apps/quick-dapp-v2/src/InBrowserVite.tsx new file mode 100644 index 00000000000..5423cd119f0 --- /dev/null +++ b/apps/quick-dapp-v2/src/InBrowserVite.tsx @@ -0,0 +1,322 @@ +// InBrowserVite - Class-based esbuild builder for in-browser bundling +// Extracted from BrowserVite.tsx to provide a reusable, non-React API + +export interface BuildResult { + js: string; + success: boolean; + error?: string; +} + +let globalInitPromise: Promise | null = null; +let globalEsbuild: any = null; + +export class InBrowserVite { + private esbuild: any = null; + private initialized = false; + private initPromise: Promise | null = null; + + /** + * Initialize esbuild-wasm. This is async and should be called before build. + * Subsequent calls return the same initialization promise. + */ + async initialize(): Promise { + if (globalInitPromise) { + await globalInitPromise; + this.esbuild = globalEsbuild; + this.initialized = true; + return; + } + + globalInitPromise = (async () => { + try { + // @ts-ignore + if (!window.esbuild) { + throw new Error('esbuild not found on window. Make sure to include esbuild-wasm script.'); + } + const esbuild = (window as any).esbuild; + + await esbuild.initialize({ + wasmURL: "https://unpkg.com/esbuild-wasm@0.25.12/esbuild.wasm", + worker: true, + }); + + console.log('[InBrowserVite-LOG] ✅ esbuild initialized '); + globalEsbuild = esbuild; + + } catch (err) { + globalInitPromise = null; + throw new Error(`esbuild initialization failed: ${err.message}`); + } + })(); + + await globalInitPromise; + this.esbuild = globalEsbuild; + this.initialized = true; + } + + /** + * Check if esbuild is initialized and ready + */ + isReady(): boolean { + return this.initialized && this.esbuild !== null; + } + + /** + * Build the entry point with the given virtual filesystem + * @param files Map of file paths to their contents + * @param entry Entry point path (default: auto-detect) + * @returns BuildResult with js output or error + */ + async build(files: Map, entry?: string): Promise { + if (!this.isReady()) { + return { + js: '', + success: false, + error: 'esbuild not initialized. Call initialize() first.', + }; + } + + try { + // Log available files for debugging + console.log('[InBrowserVite] Available files:', Array.from(files.keys())); + + // Auto-detect entry point if not provided or if it's an HTML file + let actualEntry = entry; + if (!actualEntry || !this.isBuildableEntry(actualEntry)) { + actualEntry = this.findEntryPoint(files); + if (!actualEntry) { + return { + js: '', + success: false, + error: 'No valid JavaScript/TypeScript entry point found. Please provide a .js, .jsx, .ts, or .tsx file.', + }; + } + } + + console.log('[InBrowserVite] Using entry point:', actualEntry); + + const plugin = this.makePlugin(files); + const result = await this.esbuild.build({ + entryPoints: [actualEntry], + bundle: true, + write: false, + format: 'esm', + plugins: [plugin], + define: { 'process.env.NODE_ENV': '"production"' }, + loader: { + '.js': 'jsx', + '.jsx': 'jsx', + '.ts': 'tsx', + '.tsx': 'tsx', + '.json': 'json', + }, + }); + + const js = result.outputFiles[0].text; + return { + js, + success: true, + }; + } catch (err) { + return { + js: '', + success: false, + error: err.message || err.toString(), + }; + } + } + + /** + * Find a valid entry point from the files map + */ + private findEntryPoint(files: Map): string | null { + // Common entry point patterns in order of preference + const patterns = [ + '/src/main.jsx', + '/src/main.js', + '/src/index.jsx', + '/src/index.js', + '/main.jsx', + '/main.js', + '/index.jsx', + '/index.js', + '/src/App.jsx', + '/src/App.js', + '/App.jsx', + '/App.js', + ]; + + // Check common patterns first + for (const pattern of patterns) { + if (files.has(pattern)) { + return pattern; + } + } + + // Find any buildable file + for (const [path] of files) { + if (this.isBuildableEntry(path)) { + return path; + } + } + + return null; + } + + /** + * Create esbuild plugin that resolves bare imports to esm.sh and loads files from in-memory map + */ + private makePlugin(map: Map) { + return { + name: 'virtual-fs-and-cdn', + setup: (build: any) => { + // resolve absolute paths (starting with /) + build.onResolve({ filter: /^\/.*/ }, (args: any) => { + return { path: args.path, namespace: 'local' }; + }); + + // resolve relative paths (starting with ./ or ../) + build.onResolve({ filter: /^\.\.?\/.*/ }, (args: any) => { + // Resolve relative to the importer + const importerDir = args.importer ? args.importer.substring(0, args.importer.lastIndexOf('/')) : ''; + let resolvedPath = this.resolvePath(importerDir, args.path); + return { path: resolvedPath, namespace: 'local' }; + }); + + // resolve bare specifiers (like react, app.jsx) + build.onResolve({ filter: /^[^./].*/ }, (args: any) => { + // if it's an absolute URL, set namespace to external + if (args.path.startsWith('http')) { + return { path: args.path, namespace: 'external' }; + } + + // Check if this bare specifier exists as a local file + // Try common locations (with and without leading slash) + const possiblePaths = [ + args.path, // bare: app.jsx + `/${args.path}`, // absolute: /app.jsx + `/src/${args.path}`, // src directory: /src/app.jsx + `src/${args.path}`, // src directory (no leading slash) + args.importer ? `${args.importer.substring(0, args.importer.lastIndexOf('/'))}/${args.path}` : null, + ].filter(Boolean); + + for (const testPath of possiblePaths) { + if (map.has(testPath)) { + // Normalize to absolute path with leading slash + const normalizedPath = testPath.startsWith('/') ? testPath : `/${testPath}`; + console.log(`[InBrowserVite] Resolved '${args.path}' to local file '${normalizedPath}'`); + return { path: normalizedPath, namespace: 'local' }; + } + } + + const cdnPath = `https://esm.sh/${args.path}`; + + return { path: cdnPath, external: true }; + }); + + build.onLoad({ filter: /\.css$/, namespace: 'local' }, async (args: any) => { + + const pathsToTry = [ + args.path, + args.path.startsWith('/') ? args.path.substring(1) : `/${args.path}`, + ]; + + for (const testPath of pathsToTry) { + if (map.has(testPath)) { + const cssContent = map.get(testPath); + const escapedCss = JSON.stringify(cssContent); + + const jsContent = ` + try { + const css = ${escapedCss}; + if (typeof css === 'string' && css.trim().length > 0) { + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(css)); + document.head.appendChild(style); + } + } catch (e) { + console.error('Failed to inject CSS for ${args.path}', e); + } + `; + + return { contents: jsContent, loader: 'js' }; + } + } + return { contents: `throw new Error('File not found: ${args.path}')`, loader: 'js' }; + }); + + // load local files + build.onLoad({ filter: /.*/, namespace: 'local' }, async (args: any) => { + if (args.path.endsWith('.css')) return; + + const pathsToTry = [ + args.path, + args.path.startsWith('/') ? args.path.substring(1) : `/${args.path}`, + ]; + + for (const testPath of pathsToTry) { + if (map.has(testPath)) { + const contents = map.get(testPath); + const loader = this.guessLoader(args.path); + return { contents, loader }; + } + } + + return { contents: `throw new Error('File not found in virtual filesystem: ${args.path}')`, loader: 'js' }; + }); + + } + }; + } + + /** + * Resolve a relative path against a base directory + */ + private resolvePath(base: string, relative: string): string { + // Normalize base to always be a directory path + if (!base) base = '/'; + if (!base.startsWith('/')) base = '/' + base; + if (!base.endsWith('/')) base = base + '/'; + + // Handle different relative patterns + const parts = base.split('/').filter(Boolean); + const relativeParts = relative.split('/'); + + for (const part of relativeParts) { + if (part === '..') { + parts.pop(); + } else if (part !== '.') { + parts.push(part); + } + } + + return '/' + parts.join('/'); + } + + /** + * Guess the appropriate esbuild loader based on file extension + */ + private guessLoader(path: string): string { + if (path.endsWith('.ts')) return 'ts'; + if (path.endsWith('.tsx')) return 'tsx'; + if (path.endsWith('.jsx')) return 'jsx'; + if (path.endsWith('.css')) return 'js'; + if (path.endsWith('.json')) return 'json'; + if (path.endsWith('.html')) return 'text'; // HTML files as text, not code + // Default to 'jsx' for .js, .mjs and other files to support JSX syntax + return 'jsx'; + } + + /** + * Check if a file path is a buildable entry point + */ + private isBuildableEntry(path: string): boolean { + const ext = path.toLowerCase(); + return ext.endsWith('.js') || + ext.endsWith('.jsx') || + ext.endsWith('.ts') || + ext.endsWith('.tsx') || + ext.endsWith('.mjs'); + } +} diff --git a/apps/quick-dapp-v2/src/actions/index.ts b/apps/quick-dapp-v2/src/actions/index.ts new file mode 100644 index 00000000000..8176b89c706 --- /dev/null +++ b/apps/quick-dapp-v2/src/actions/index.ts @@ -0,0 +1,286 @@ +import axios from 'axios'; +import { omitBy } from 'lodash'; +import semver from 'semver'; +import { execution } from '@remix-project/remix-lib'; +import remixClient from '../remix-client'; +import { themeMap } from '../components/DeployPanel/theme'; + +const { encodeFunctionId } = execution.txHelper; + +const getVersion = (solcVersion) => { + let version = '0.8.25' + try { + const arr = solcVersion.split('+') + if (arr && arr[0]) version = arr[0] + if (semver.lt(version, '0.6.0')) { + return { version: version, canReceive: false }; + } else { + return { version: version, canReceive: true }; + } + } catch (e) { + return { version, canReceive: true }; + } +}; + +let dispatch: any, state: any; + +export const initDispatch = (_dispatch: any) => { + dispatch = _dispatch; +}; + +export const updateState = (_state: any) => { + state = _state; +}; + +export const connectRemix = async () => { + await dispatch({ + type: 'SET_LOADING', + payload: { + screen: true, + }, + }); + + await remixClient.onload(); + + // @ts-expect-error + await remixClient.call('layout', 'minimize', 'terminal', true); + + await dispatch({ + type: 'SET_LOADING', + payload: { + screen: false, + }, + }); +}; + +export const saveDetails = async (payload: any) => { + const { abi, userInput, natSpec } = state.instance; + + await dispatch({ + type: 'SET_INSTANCE', + payload: { + abi: { + ...abi, + [payload.id]: { + ...abi[payload.id], + details: + natSpec.checked && !payload.details + ? natSpec.methods[payload.id] + : payload.details, + }, + }, + userInput: { + ...omitBy(userInput, (item) => item === ''), + methods: omitBy( + { + ...userInput.methods, + [payload.id]: payload.details, + }, + (item) => item === '' + ), + }, + }, + }); +}; + +export const saveTitle = async (payload: any) => { + const { abi } = state.instance; + + await dispatch({ + type: 'SET_INSTANCE', + payload: { + abi: { + ...abi, + [payload.id]: { ...abi[payload.id], title: payload.title }, + }, + }, + }); +}; + +export const getInfoFromNatSpec = async (value: boolean) => { + const { abi, userInput, natSpec } = state.instance; + const input = value + ? { + ...natSpec, + ...userInput, + methods: { ...natSpec.methods, ...userInput.methods }, + } + : userInput; + Object.keys(abi).forEach((id) => { + abi[id].details = input.methods[id] || ''; + }); + await dispatch({ + type: 'SET_INSTANCE', + payload: { + abi, + title: input.title || '', + details: input.details || '', + natSpec: { ...natSpec, checked: value }, + }, + }); +}; + +export const initInstance = async ({ + methodIdentifiers, + devdoc, + solcVersion, + htmlTemplate, + ...payload +}: any) => { + // If HTML template is provided, use simplified initialization + if (htmlTemplate) { + await dispatch({ + type: 'SET_INSTANCE', + payload: { + ...payload, + htmlTemplate, + abi: {}, + items: {}, + containers: [], + natSpec: { checked: false, methods: {} }, + solcVersion: solcVersion ? getVersion(solcVersion) : { version: '0.8.25', canReceive: true }, + }, + }); + return; + } + + // Original ABI-based initialization (kept for backward compatibility) + const functionHashes: any = {}; + const natSpec: any = { checked: false, methods: {} }; + if (methodIdentifiers && devdoc) { + for (const fun in methodIdentifiers) { + functionHashes[`0x${methodIdentifiers[fun]}`] = fun; + } + natSpec.title = devdoc.title; + natSpec.details = devdoc.details; + Object.keys(functionHashes).forEach((hash) => { + const method = functionHashes[hash]; + if (devdoc.methods[method]) { + const { details, params, returns } = devdoc.methods[method]; + const detailsStr = details ? `@dev ${details}` : ''; + const paramsStr = params + ? Object.keys(params) + .map((key) => `@param ${key} ${params[key]}`) + .join('\n') + : ''; + const returnsStr = returns + ? Object.keys(returns) + .map( + (key) => + `@return${/^_\d$/.test(key) ? '' : ' ' + key} ${returns[key]}` + ) + .join('\n') + : ''; + natSpec.methods[hash] = [detailsStr, paramsStr, returnsStr] + .filter((str) => str !== '') + .join('\n'); + } + }); + } + + const abi: any = {}; + const lowLevel: any = {} + if (payload.abi) { + payload.abi.forEach((item: any) => { + if (item.type === 'function') { + item.id = encodeFunctionId(item); + abi[item.id] = item; + } + if (item.type === 'fallback') { + lowLevel.fallback = item; + } + if (item.type === 'receive') { + lowLevel.receive = item; + } + }); + } + const ids = Object.keys(abi); + const items = + ids.length > 2 + ? { + A: ids.slice(0, ids.length / 2 + 1), + B: ids.slice(ids.length / 2 + 1), + } + : { A: ids }; + + await dispatch({ + type: 'SET_INSTANCE', + payload: { + ...payload, + abi, + items, + containers: Object.keys(items), + natSpec, + solcVersion: solcVersion ? getVersion(solcVersion) : { version: '0.8.25', canReceive: true }, + ...lowLevel, + }, + }); +}; + +export const resetInstance = async () => { + const abi = state.instance.abi; + const ids = Object.keys(abi); + ids.forEach((id) => { + abi[id] = { ...abi[id], title: '', details: '' }; + }); + const items = + ids.length > 1 + ? { + A: ids.slice(0, ids.length / 2 + 1), + B: ids.slice(ids.length / 2 + 1), + } + : { A: ids }; + await dispatch({ + type: 'SET_INSTANCE', + payload: { + items, + containers: Object.keys(items), + title: '', + details: '', + abi, + }, + }); +}; + +export const emptyInstance = async () => { + await dispatch({ + type: 'SET_INSTANCE', + payload: { + name: '', + address: '', + network: '', + htmlTemplate: '', + pages: {}, + abi: {}, + items: {}, + containers: [], + title: '', + details: '', + logo: null, + theme: 'Dark', + userInput: { methods: {} }, + natSpec: { checked: false, methods: {} }, + }, + }); +}; + +export const selectTheme = async (selectedTheme: string) => { + await dispatch({ type: 'SET_INSTANCE', payload: { theme: selectedTheme } }); + + const linkEles = document.querySelectorAll('link'); + const nextTheme = themeMap[selectedTheme]; // Theme + for (const link of linkEles) { + if (link.href.indexOf('/assets/css/themes/') > 0) { + link.href = 'https://remix.ethereum.org/' + nextTheme.url; + document.documentElement.style.setProperty('--theme', nextTheme.quality); + break; + } + } +}; + +export const setAiLoading = async (isLoading: boolean) => { + await dispatch({ + type: 'SET_AI_LOADING', + payload: isLoading, + }); +}; \ No newline at end of file diff --git a/apps/quick-dapp-v2/src/assets/sparkling.png b/apps/quick-dapp-v2/src/assets/sparkling.png new file mode 100644 index 00000000000..eb454ccf746 Binary files /dev/null and b/apps/quick-dapp-v2/src/assets/sparkling.png differ diff --git a/apps/quick-dapp-v2/src/components/ChatBox/ChatBox.css b/apps/quick-dapp-v2/src/components/ChatBox/ChatBox.css new file mode 100644 index 00000000000..de1d9f4ecba --- /dev/null +++ b/apps/quick-dapp-v2/src/components/ChatBox/ChatBox.css @@ -0,0 +1,67 @@ +.chat-box-container { + border: 1px solid var(--border); + background: var(--body-bg); + border-radius: 0px; + box-shadow: none; + display: flex; + flex-direction: column; +} + +.chat-box-footer { + background-color: transparent; + border-top: none; + padding: 0.5rem; +} + +.chat-input-group { + display: flex; + gap: 8px; + background-color: var(--light); + border: 1px solid var(--border); + border-radius: 4px; + padding: 6px; + transition: border-color 0.15s ease-in-out; +} + +.chat-input-group:focus-within { + border-color: var(--primary); + box-shadow: 0 0 0 0.2rem rgba(var(--primary-rgb, 81, 88, 253), 0.15); +} + +.chat-input { + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + color: var(--text) !important; + resize: none; + font-size: 12px; + font-family: var(--font-family-monospace, monospace); + padding: 4px; +} + +.chat-input:disabled { + background-color: transparent !important; + color: var(--text-muted) !important; + cursor: not-allowed; +} + +.chat-input::placeholder { + color: var(--text-muted); + opacity: 0.7; + font-style: italic; +} + +.send-button { + border-radius: 4px; + font-size: 12px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + min-width: 60px; +} + +.send-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} \ No newline at end of file diff --git a/apps/quick-dapp-v2/src/components/ChatBox/index.tsx b/apps/quick-dapp-v2/src/components/ChatBox/index.tsx new file mode 100644 index 00000000000..12684b977da --- /dev/null +++ b/apps/quick-dapp-v2/src/components/ChatBox/index.tsx @@ -0,0 +1,102 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Form, Button, Card } from 'react-bootstrap'; +import { FormattedMessage, useIntl } from 'react-intl'; +import './ChatBox.css'; + +interface Message { + id: string; + role: 'user' | 'assistant'; + content: string; + timestamp: Date; +} + +interface ChatBoxProps { + onSendMessage?: (message: string) => void; + onUpdateCode?: (code: string) => void; +} + +const ChatBox: React.FC = ({ onSendMessage, onUpdateCode }) => { + const intl = useIntl(); + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleSendMessage = async () => { + if (!inputMessage.trim()) return; + + const newMessage: Message = { + id: Date.now().toString(), + role: 'user', + content: inputMessage, + timestamp: new Date() + }; + + setMessages(prev => [...prev, newMessage]); + setInputMessage(''); + setIsLoading(true); + + if (onSendMessage) { + onSendMessage(inputMessage); + } + + // Simulate assistant response (this will be replaced with actual LLM integration) + setTimeout(() => { + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: 'I understand you want to update the frontend. I can help you modify the HTML template. What specific changes would you like to make?', + timestamp: new Date() + }; + setMessages(prev => [...prev, assistantMessage]); + setIsLoading(false); + }, 1000); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + return ( + + +
+ setInputMessage(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={intl.formatMessage({ + id: 'quickDapp.chatPlaceholder', + defaultMessage: 'Ask the assistant to help modify your dApp...' + })} + disabled={isLoading} + className="chat-input" + /> + +
+
+
+ ); +}; + +export default ChatBox; \ No newline at end of file diff --git a/apps/quick-dapp-v2/src/components/ContractGUI/index.tsx b/apps/quick-dapp-v2/src/components/ContractGUI/index.tsx new file mode 100644 index 00000000000..b3227aadc96 --- /dev/null +++ b/apps/quick-dapp-v2/src/components/ContractGUI/index.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { execution } from '@remix-project/remix-lib'; +import { saveDetails, saveTitle } from '../../actions'; + +const txHelper = execution.txHelper; + +const getFuncABIInputs = (funABI: any) => { + if (!funABI.inputs) { + return ''; + } + return txHelper.inputParametersDeclarationToString(funABI.inputs); +}; + +export function ContractGUI(props: { funcABI: any, funcId: any }) { + const intl = useIntl() + const isConstant = + props.funcABI.constant !== undefined ? props.funcABI.constant : false; + const lookupOnly = + props.funcABI.stateMutability === 'view' || + props.funcABI.stateMutability === 'pure' || + isConstant; + const inputs = getFuncABIInputs(props.funcABI); + const [title, setTitle] = useState(''); + const [buttonOptions, setButtonOptions] = useState<{ + title: string; + content: string; + classList: string; + dataId: string; + }>({ title: '', content: '', classList: '', dataId: '' }); + + useEffect(() => { + if (props.funcABI.name) { + setTitle(props.funcABI.name); + } else { + setTitle(props.funcABI.type === 'receive' ? '(receive)' : '(fallback)'); + } + }, [props.funcABI]); + + useEffect(() => { + if (lookupOnly) { + setButtonOptions({ + title: title + ' - call', + content: 'call', + classList: 'btn-info', + dataId: title + ' - call', + }); + } else if ( + props.funcABI.stateMutability === 'payable' || + props.funcABI.payable + ) { + setButtonOptions({ + title: title + ' - transact (payable)', + content: 'transact', + classList: 'btn-danger', + dataId: title + ' - transact (payable)', + }); + } else { + setButtonOptions({ + title: title + ' - transact (not payable)', + content: 'transact', + classList: 'btn-warning', + dataId: title + ' - transact (not payable)', + }); + } + }, [lookupOnly, props.funcABI, title]); + + return ( +
+
+ { + saveTitle({ id: props.funcABI.id, title: value }); + }} + /> +
+
+
+ +
+ 0 + ) + ? 'hidden' + : 'visible', + }} + /> +
+
+