From f9e4e6ea3fac38f7707149aed8cddcc550277060 Mon Sep 17 00:00:00 2001 From: Nestor Qin Date: Sat, 31 Aug 2024 22:40:21 -0400 Subject: [PATCH] refactor: Simplify the codebase with functional Overleaf implementation (#2) Simplify and refactor the entire codebase to the new architecture. --------- Co-authored-by: YiyanZhai --- .github/workflows/lint.yml | 3 +- README.md | 60 ++++++++++--------------- eslint.config.mjs | 25 +++++++++++ package.json | 66 +++++++++++++++------------- rollup.config.mjs | 26 ----------- src/action.js | 15 ------- src/index.js | 35 +++++++++++++-- src/page/overleaf.js | 88 +++++++++++++++++++++++++++++++++++++ src/page/overleafPage.js | 90 -------------------------------------- src/page/page.js | 8 ---- webpack.config.cjs | 59 +++++++++++++++++++++++++ 11 files changed, 261 insertions(+), 214 deletions(-) create mode 100644 eslint.config.mjs delete mode 100644 rollup.config.mjs delete mode 100644 src/action.js create mode 100644 src/page/overleaf.js delete mode 100644 src/page/overleafPage.js delete mode 100644 src/page/page.js create mode 100644 webpack.config.cjs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f42b2f..c8ec932 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,11 +18,10 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version: "16" - name: Install dependencies run: npm install - name: Run linter check run: npm run lint - \ No newline at end of file diff --git a/README.md b/README.md index 200fcb7..456abf9 100644 --- a/README.md +++ b/README.md @@ -15,57 +15,41 @@ The `web-agent-interface` library provides an API to interact with text selectio To install and build the library, follow these steps: 1. Clone the repository: - ```bash - git clone https://github.com/mlc-ai/web-agent-interface.git - cd web-agent-interface - ``` + + ```bash + git clone https://github.com/mlc-ai/web-agent-interface.git + cd web-agent-interface + ``` 2. Install dependencies and build the project: - ```bash - npm install - npm run build - ``` -3. Import the necessary modules into your project. For example, if the dependency is named `@mlc-ai/web-agent-interface`: - ```javascript - import { OverleafPage } from '@mlc-ai/web-agent-interface'; - ``` + ```bash + npm install + npm run build + ``` ## Usage -### 1. Creating a Page Instance - -Depending on the platform you're working with, you can create an instance of the corresponding page class: +### 1. Initialize the page handler in content script -- For **Overleaf**: ```javascript - const overleafPage = new OverleafPage(); - ``` - -### 2. Handling Text +### 2. Get Available Tools -- Get the selected text: - ```javascript - const selectedText = overleafPage.executeAction('getTextSelection'); - console.log('Selected text:', selectedText); - ``` + ```javascript + import { getTools } from '@mlc-ai/web-agent-interface'; -- Replace selected text: - ```javascript - overleafPage.executeAction('replaceSelectedText', { newText: 'Your new text here' }); + const availableTools = getTools(); ``` -- Adding Text to the End of the Document: +### 3. Handling Tool Use + ```javascript - overleafPage.executeAction('addTextToEnd', { newText: 'Text to append' }); - ``` \ No newline at end of file + const observation = handler.handleToolCall(toolName, parameters); + console.log("Got observation:", observation); + ``` diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..6c0787d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,25 @@ +import eslint from "eslint"; + +const { ESLint } = eslint; + +export default new ESLint({ + baseConfig: { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: ["eslint:recommended"], + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + }, + rules: { + "no-unused-vars": "warn", + "no-console": "off", + indent: ["error", 2], + quotes: ["error", "single"], + semi: ["error", "always"], + }, + }, +}); diff --git a/package.json b/package.json index 5f0d816..bf24de9 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,37 @@ { - "name": "web-agent-interface", - "version": "0.1.0", - "description": "A generic library for web interaction", - "main": "lib/index.js", - "type": "module", - "scripts": { - "build": "rollup -c", - "lint": "npx eslint . && prettier --check \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"", - "test": "jest", - "format": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/mlc-ai/web-agent-interface.git" - }, - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/mlc-ai/web-agent-interface/issues" - }, - "homepage": "https://github.com/mlc-ai/web-agent-interface#readme", - "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-image": "^3.0.3", - "@rollup/plugin-node-resolve": "^15.2.3", - "@types/jest": "^29.5.12", - "@types/node": "^20.12.5", - "eslint": "^8.57.0", - "rollup": "^4.14.0", - "rollup-plugin-ignore": "^1.0.10", - "rollup-plugin-sass": "^1.12.21" - } + "name": "@mlc-ai/web-agent-interface", + "version": "0.1.0", + "description": "A generic library for web interaction", + "main": "lib/index.js", + "type": "module", + "scripts": { + "build": "webpack --config webpack.config.cjs", + "lint": "npx eslint . && prettier --check \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"", + "test": "jest", + "format": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mlc-ai/web-agent-interface.git" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/mlc-ai/web-agent-interface/issues" + }, + "homepage": "https://github.com/mlc-ai/web-agent-interface#readme", + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "^22.5.0", + "babel-loader": "^9.1.3", + "css-loader": "^7.1.2", + "eslint": "^9.9.1", + "jest": "^29.7.0", + "mini-css-extract-plugin": "^2.9.1", + "prettier": "^3.3.3", + "sass": "^1.77.8", + "sass-loader": "^16.0.1", + "style-loader": "^4.0.0", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4" + } } diff --git a/rollup.config.mjs b/rollup.config.mjs deleted file mode 100644 index 28b2977..0000000 --- a/rollup.config.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { nodeResolve } from "@rollup/plugin-node-resolve"; -import ignore from "rollup-plugin-ignore"; -import commonjs from "@rollup/plugin-commonjs"; -import image from "@rollup/plugin-image"; -import sass from "rollup-plugin-sass"; - -export default { - input: "src/index.js", - output: [ - { - file: "lib/index.js", - exports: "named", - format: "cjs", - sourcemap: true, - }, - ], - plugins: [ - ignore(["fs", "path", "crypto"]), - nodeResolve({ browser: true }), - commonjs({ - ignoreDynamicRequires: true, - }), - image(), - sass(), - ], -}; \ No newline at end of file diff --git a/src/action.js b/src/action.js deleted file mode 100644 index 9272581..0000000 --- a/src/action.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Abstract class for actions - * @class Action - */ -export class Action { - implementation; - - constructor(impl) { - this.implementation = impl; - } - - call = (params) => { - this.implementation(params); - } -} \ No newline at end of file diff --git a/src/index.js b/src/index.js index bc5f6b8..4c8d67f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,31 @@ -export { Page } from './page/page'; -export { Action } from './action'; -export { OverleafPage } from './page/overleafPage'; -export { GoogleDocPage } from './page/googleDocPage'; \ No newline at end of file +import * as Overleaf from "./page/overleaf.js"; + +const PAGE_HANDLER_MAP = { + "www.overleaf.com": Overleaf, +}; + +const initHandler = () => { + if (Object.keys(PAGE_HANDLER_MAP).includes(window.location.hostname)) { + return PAGE_HANDLER_MAP[window.location.hostname].initHandler(); + } + console.error("[Web Agent Interface] No handler found for the current page"); +}; + +const getTools = () => { + if (Object.keys(PAGE_HANDLER_MAP).includes(window.location.hostname)) { + return PAGE_HANDLER_MAP[window.location.hostname].actions; + } + console.error("[Web Agent Interface] No actions found for the current page"); +}; + +const getToolDisplayName = (action) => { + if ( + Object.keys(PAGE_HANDLER_MAP).includes(window.location.hostname) && + PAGE_HANDLER_MAP[window.location.hostname].actions.includes(action) + ) { + return PAGE_HANDLER_MAP[window.location.hostname].nameToDisplayName[action]; + } + console.error("[Web Agent Interface] No actions found for the current page"); +}; + +export { initHandler, getTools, getToolDisplayName }; diff --git a/src/page/overleaf.js b/src/page/overleaf.js new file mode 100644 index 0000000..0e6b9e2 --- /dev/null +++ b/src/page/overleaf.js @@ -0,0 +1,88 @@ +/** + * Implementation of getTextSelection, replaceSelectedText, etc for Overleaf + * @class OverleafPage + */ +class PageHandler { + currentSelection = null; + + constructor() { + // Listen for the selection change event + document.addEventListener("selectionchange", this.handleSelectionChange); + } + + handleSelectionChange = () => { + const selection = window.getSelection(); + + if ( + selection && + typeof selection.rangeCount !== "undefined" && + selection.rangeCount > 0 + ) { + this.currentSelection = selection; + } + }; + + getSelectionImpl = () => { + if (!this.currentSelection) { + return ""; + } + const selectedText = this.currentSelection.toString(); + return selectedText; + }; + + replaceSelectionImpl = (params) => { + const newText = params.newText; + const selection = this.currentSelection; + if (!newText || !selection) { + return; + } + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + if (Array.isArray(newText)) { + const fragment = document.createDocumentFragment(); + newText.forEach((text) => + fragment.appendChild(document.createTextNode(text)), + ); + range.insertNode(fragment); + } else { + range.insertNode(document.createTextNode(newText)); + } + selection.removeAllRanges(); + } else { + return; + } + }; + + appendTextImpl = (params) => { + const text = params.text; + const editorElement = document.querySelector(".cm-content"); + if (editorElement) { + const textNode = document.createTextNode(text); + editorElement.appendChild(textNode); + } + }; + + handleToolCall = (toolName, params) => { + const toolImplementation = this.toolNameToImplementation[actionName]; + if (action) { + const response = toolImplementation(params); + return response; + } else { + console.warn(`Tool '${toolName}' not found.`); + } + }; + + toolNameToImplementation = { + getSelection: this.getSelectionImpl, + replaceSelection: this.replaceSelectionImpl, + appendText: this.appendTextImpl, + }; +} +export const nameToDisplayName = { + getSelection: "Get Selected Text", + replaceSelection: "Replace Selected Text", + appendText: "Add Text to Document", +}; +export const tools = Object.keys(nameToDisplayName); +export const initHandler = () => new PageHandler(); diff --git a/src/page/overleafPage.js b/src/page/overleafPage.js deleted file mode 100644 index 3bdf622..0000000 --- a/src/page/overleafPage.js +++ /dev/null @@ -1,90 +0,0 @@ -import { Page } from './page'; -import { Action } from '../action'; - -/** - * Implementation of getTextSelection, replaceSelectedText, etc for Overleaf - * @class OverleafPage - */ -export class OverleafPage extends Page { - static currentSelection = null; - - constructor() { - super(); - // Listen for the selection change event - document.addEventListener('selectionchange', this.handleSelectionChange); - } - - handleSelectionChange = () => { - const selection = window.getSelection(); - console.log('[handleSelectionChange] Selection object:', selection); - - if (selection && typeof selection.rangeCount !== 'undefined' && selection.rangeCount > 0) { - this.currentSelection = selection; - console.log('[handleSelectionChange] New selection:', this.currentSelection.toString()); - } else { - console.log('[handleSelectionChange] No valid selection', this.currentSelection.toString()); - } - } - - getTextSelectionImpl = () => { - if (!this.currentSelection) { - console.log('[getTextSelection] currentSelection is null'); - return ''; - } - console.log('[getTextSelection] Selected text:', this.currentSelection.toString()); - return this.currentSelection.toString(); - } - - replaceSelectedTextImpl = (params) => { - console.log('params', params); - const newText = params.newText; - const selection = this.currentSelection; - if (!selection) { - console.log('[replaceSelectedTextImpl] Selection is null'); - return; - } - console.log('[replaceSelectedTextImpl] Selection:', selection); - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - range.deleteContents(); - if (Array.isArray(newText)) { - const fragment = document.createDocumentFragment(); - newText.forEach(text => fragment.appendChild(document.createTextNode(text))); - range.insertNode(fragment); - } else { - range.insertNode(document.createTextNode(newText)); - } - selection.removeAllRanges(); - } else { - console.log('[replaceSelectedTextImpl] No text selection'); - return; - } - } - - addTextToEndImpl = (params) => { - const newText = params.newText; - const editorElement = document.querySelector(".cm-content"); - if (editorElement) { - const textNode = document.createTextNode(newText); - editorElement.appendChild(textNode); - } - } - - executeAction = (actionName, params) => { - const action = this.nameToAction[actionName]; - if (action) { - // assuming params contains a key 'newText' for now - return action.call(params); - } else { - console.log(`Action '${actionName}' not found.`); - } - } - - nameToAction = { - 'getTextSelection': new Action(this.getTextSelectionImpl), - 'replaceSelectedText': new Action(this.replaceSelectedTextImpl), - 'addTextToEnd': new Action(this.addTextToEndImpl) - }; - - availableActions = Object.keys(this.nameToAction); -} \ No newline at end of file diff --git a/src/page/page.js b/src/page/page.js deleted file mode 100644 index 8fbde11..0000000 --- a/src/page/page.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Abstract class for pages - * @class Page - */ -export class Page { - availableActions; - nameToAction = []; -} \ No newline at end of file diff --git a/webpack.config.cjs b/webpack.config.cjs new file mode 100644 index 0000000..6109e84 --- /dev/null +++ b/webpack.config.cjs @@ -0,0 +1,59 @@ +const path = require("path"); +const webpack = require("webpack"); +const { IgnorePlugin } = require("webpack"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +module.exports = { + entry: "./src/index.js", + output: { + path: path.resolve(__dirname, "lib"), + filename: "index.js", + library: { + type: "commonjs2", + }, + clean: true, + sourceMapFilename: "[file].map", + }, + target: "web", + resolve: { + extensions: [".js", ".json"], + fallback: { + fs: false, + path: false, + crypto: false, + }, + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + }, + }, + { + test: /\.(png|jpe?g|gif|svg)$/i, + type: "asset/resource", + }, + { + test: /\.(scss|css)$/, + use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], + }, + ], + }, + plugins: [ + new IgnorePlugin({ + resourceRegExp: /^fs$|^path$|^crypto$/, + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + }), + ], + devtool: "source-map", + mode: "production", + externals: { + // Prevent bundling of certain imported packages + // (e.g., libraries already available as external scripts) + }, +};