This code is for now just released as is without a functional backend to make it work practically.
The main intention of releasing it in its current form is to allow developers who are looking to build similar browser extensions to look at how we dealt with the challenges of:
- highlighting text within editable fields
- allowing interaction with the highlights to receive context information
- replacing content through proposed alternatives.
Our ideal hope would be the creation of an open source package that will make this type of use case a "solved problem" across all browsers by pooling resources to maintain reliable algorithms for dealing with all websites out there, or at least a sizeable subset.
This code was created at https://witty.works that formed our solution for "operationalizing" inclusive language and consistent language within organizations. This is essentially a client UI for the https://github.com/witty-works/nlp_api and integrates with https://github.com/witty-works/dashboard for authentication. The Microsoft Word Add-in https://github.com/witty-works/word-plugin is an alternative client UI
Clone repo git clone https://github.com/witty-works/browser-extension.git
Go to project directory cd browser-extension
Ensure you are using node 18+
Run npm install to install dependencies.
Before running or building the extension, copy the example config and customize it for your environment:
cp source/witty.config.json.example source/witty.config.json
# then edit source/witty.config.json to set `api.endpoint`, feature flags etc.From inside the directory, start the development server depending on the browser you are using
- Chrome:
npm run dev:chrome - Firefox:
npm run dev:firefox - Opera:
npm run dev:opera
This will run in the background, watching for changes and rebuilding the project automatically.
You can several options:
npm run build:chrometo build chrome extensionnpm run build:firefoxto build firefox addonnpm run build:operato build opera extensionnpm run buildbuilds and packs extensions all at once toextension/BROWSERdirectory respectively.
Note: By default the manifest.json is set with version 0.0.0. The webpack loader will update the version in the build with that of the package.json version. In order to release a new version, update version in package.json and run script.
If you don't want to use package.json version, you can disable the option here.
The extension reads runtime defaults from source/witty.config.json. Key settings you may want to tune during local development or testing:
-
ORTHOGRAPHY: written when the app fetches organization config from the server — see source/shared/utils.ts. -
API_DELAY: user-changeable via the popup Delay selector (writes to storage). See source/Popup/PopupComponents/DelaySelector.tsx. -
ACCESS_TOKEN/REFRESH_TOKEN: written/cleared by auth flows and logout. See source/shared/utils.ts and source/Background/index.tsx. -
Telemetry counters:
DAILY_POSTHOG_EVENTS_USEDandLAST_CHECK_EVENT_TIMEare updated at runtime by analytics code. See source/shared/ApiServices/analyticsUtils.ts. -
Other server-populated keys (written at runtime):
DOMAINS,ORGANIZATION_DOMAINS,PLAN,USER_ID,CONFIG_HASH,LLM_ALTERNATIVES, etc. See source/shared/utils.ts. -
X_KEY: optional static API key — when set the extension will attachx-key: <value>to outbound API requests and skip token/refresh flows; see theX_KEYsection below for example and security notes. -
BASE_URLS: object — choose which set of API/dashboard/PostHog endpoints the extension uses (Prod/Dev/Local). See the "Base URLs configuration (BASE_URLS)" section below for structure and examples. -
EXPOSE_WITTY_ID_ALLOW_LIST: object — domains where the extension exposes itsextension-team/extension-idattributes for development. See the "Expose Witty ID allow list (EXPOSE_WITTY_ID_ALLOW_LIST)" section below for structure and examples.
Authentication
The extension supports two modes:
-
API key (
X_KEY): addX_KEYtosource/witty.config.jsonto sendx-key: <value>on requests. This is convenient for simple tests or CI; it bypasses token refresh and may purge storedACCESS_TOKEN/REFRESH_TOKEN. -
OAuth tokens (
ACCESS_TOKEN/REFRESH_TOKEN): normal sign-in obtains per-user bearer tokens. When present the extension sendsAuthorization: Bearer <access_token>and will attempt refresh usingREFRESH_TOKENif needed.
Security note: never commit production X_KEY, ACCESS_TOKEN, or REFRESH_TOKEN; prefer environment-backed secrets.
Example snippets (local testing only):
{ "X_KEY": "your-api-key-here" }or
{
"ACCESS_TOKEN": "your-static-access-token",
"REFRESH_TOKEN": "your-static-refresh-token"
}Base URLs configuration (BASE_URLS)
The extension looks up API and dashboard base URLs from the BASE_URLS config object when it boots. This allows switching the target API (Prod/Dev/Local) at build time without changing source code.
Structure (in source/witty.config.json):
"BASE_URLS": {
"Prod": { "api": "https://api.example.com/", "dashboard": "https://dashboard.example.com/", "posthog_url": "https://app.posthog.com", "posthog_key": "..." },
"Dev": { "api": "https://dev.api.example.com/", "dashboard": "https://dev.dashboard.example.com/", "posthog_url": "https://app.posthog.com", "posthog_key": "..." },
"Local": { "api": "http://127.0.0.1:8000/", "dashboard": "https://dashboard.lndo.site/", "posthog_url": "https://app.posthog.com", "posthog_key": "..." }
}source/shared/constants.ts reads defaultConfig.BASE_URLS (the uppercase BASE_URLS key) and falls back to compiled defaults if the key is missing. See source/shared/constants.ts for implementation.
Expose Witty ID allow list (EXPOSE_WITTY_ID_ALLOW_LIST)
Use this config to control which domains the extension exposes its extension-team/extension-id attributes for development purposes. The key is expected to be present at build time (in source/witty.config.json) and is read by source/shared/constants.ts. If the key is missing, the code will not expose the attributes.
Structure example:
"EXPOSE_WITTY_ID_ALLOW_LIST": {
"dev": ["lndo.site", "platformsh.site", "witty.works"],
"prod": ["witty.works"]
}Notes:
- Use
X_KEYfor simple test setups or CI where a single static key is acceptable; prefer running the local NLP API and selecting its endpoint via the PopupApiSelectorfor iteration. - Never commit production
X_KEY,ACCESS_TOKEN, or any secret to source control. Use environment-backed secrets or local.envfiles for CI/test fixtures. - To quickly toggle features like
REPHRASE_ENABLED, editsource/witty.config.jsonand restart the dev build (npm run dev:chromeor rebuild).
Either you run the Development or Production scripts, it is build inside extension/BROWSER folder. Just follow the different instructions depending on the browser
- Go to the browser address bar and type
chrome://extensions - Check the
Developer Modebutton to enable it. - Click on the
Load Unpacked Extension…button. - Select your extension’s extracted directory.
This section explains how to run the extension locally during development and connect it to a locally-running NLP API backend.
- Prerequisites: Node.js 18+, npm, and a local copy of the NLP API (see below).
- Install dependencies:
npm install- Start the dev build for Chrome (webpack will watch and rebuild on changes):
npm run dev:chrome-
Open Chrome and go to
chrome://extensions→ enableDeveloper mode→Load unpackedand point to the build output directory. The development build is emitted into the extension folder for the chosen browser, e.g.extension/chrome(orextension/BROWSERif you use a different target). -
Configure the NLP API backend:
- Option A (recommended for fast iteration): run the NLP API locally and select the
LocalAPI endpoint in the extension popup's Development Settings (ApiSelector). The extension stores your choice in browser local storage under theapiEndpointkey and will use the selected base URL for requests. - Option B (static API key): if you prefer using a static API key, set
X_KEYinsource/witty.config.jsonand rebuild (npm run build:chromeornpm run dev:chrome). This will attach thex-key: <value>header to requests.
- Option A (recommended for fast iteration): run the NLP API locally and select the
-
Running a local NLP API server: clone the NLP API repository referenced in this project (https://github.com/witty-works/nlp_api) and follow its README to start the server locally. Note the server base URL (for example
http://localhost:8000) and select the matching API endpoint in the extension popup. -
Useful tips:
- When using
npm run dev:chrome, webpack writes updated bundles on each save; reload the extension inchrome://extensions(or enable extension auto-reload tooling) to pick up the latest build. - To test API-key mode without rebuilding, you can temporarily update the
apiEndpointvia the popupApiSelectorand use an endpoint that accepts a test key; howeverX_KEYinwitty.config.jsonrequires rebuilding to take effect. - Check the browser console & background logs (via the extension's service worker or background page) for
/v2.0/authand/v2.4/checkrequests when troubleshooting connectivity.
- When using
If you need a sample X_KEY for local testing, add it to source/witty.config.json as shown in the X_KEY section above — but never commit production keys to source control.
- Load the Add-on via
about:debuggingas temporary Add-on. - Choose the
manifest.jsonfile in the extracted directory
- Load the extension via
opera:extensions - Check the
Developer Modeand load as unpacked from extension’s extracted directory.
Update source/manifest.json file with browser vendor prefixed manifest keys
{
"__chrome__name": "SuperChrome",
"__firefox__name": "SuperFox",
"__edge__name": "SuperEdge",
"__opera__name": "SuperOpera"
}if the vendor is chrome this compiles to:
{
"name": "SuperChrome",
}Add keys to multiple vendors by separating them with | in the prefix
{
__chrome|opera__name: "SuperBlink"
}
if the vendor is chrome or opera, this compiles to:
{
"name": "SuperBlink"
}
See the original README of wext-manifest-loader package for more details
run npx playwright install-deps
run npx playwright install
Copy .env.example to .env and adjust the values accordingly.
PREMIUM_TEST_USER_EMAIL = 'witty.works.premium.user@gmail.com'
PREMIUM_TEST_USER_PASSWORD = '<can be found in 1Password>'
HTACCESS_USERNAME = 'witty'
HTACCESS_PASSWORD = '<can be found in 1Password>'
To run locally: From inside the directory just run npm run test (runs in linux environment)
You can watch the tests executing on http://localhost:6080/ (PW: vscode)
test-results folder will be created with screenshots for manual debuging
First, download the test-results folder, which is produced during runtime. You can find this under the Artifacts section in the Summary of the failing test.
Next, compare the actual screenshot with the expected one to identify any discrepancies.
If the actual screenshot is now correct, follow these steps to update the repository:
- Upload the correct screenshot to the
__tests__/highlightPlacement.spec.js-snapshotsdirectory in this repository.
- Delete the original screenshots:
Highlights-witty-form-not-logged-in-1-linux.pngHighlights-witty-form-1-linux.png
- Rename the new screenshots to match the names of the originals.
- Shared Eslint & Prettier Configuration -
@abhijithvijayan/eslint-config - Shared TypeScript Configuration -
@abhijithvijayan/tsconfig
This is a fork of Web Extension Browser by Abhijith Vijayan under MIT license.
After making changes to highlights:
- Ensure highlights scroll correctly within the scrolling text area.
- Verify highlights stay fixed when scrolling the surrounding window.
- Check that highlights maintain their position.
- Ensure highlights above text remain fixed while the highlights below are removed and then re-added in the correct position.
- For long text, ensure only the first set number of characters are sent to the API. Users should be able to highlight additional text by clicking on a paragraph.
- Note that much of the Google Docs compatibility code is separate and also needs updates.
- Confirm that no highlights are displayed when the user is not logged in or if the trial has expired.
- Verify that highlights adapt when resizing the input window as well as when resizing the entire window.
After making changes to popover:
- Ensure the popover opens and closes correctly when clicking a word.
- Verify the correct popover is displayed based on user status (not logged in, needs upgrade, logged in).
- Check that the position of the popover is accurate relative to the selected word.
- Confirm that learning bites are loaded correctly.
- Verify that alternatives are inserted correctly, with specific attention to 'remove' alternatives.
- Test the functionality of ignoring a word once and ignoring a word permanently through dashboards.
After making changes to popup:
- Verify enabling and disabling the extension functions correctly:
- Ensure that when the extension is disabled, neither the 'witty active' nor 'witty passive' icons appear anywhere.
- Confirm no 'ww-container' elements are in the DOM.
- Pay extra attention to the behavior with iframes and when multiple input fields are on a page.
- Ensure the correct popup is shown in appropriate situations (not logged in, domain disabled, no subscription, valid subscription).
- Highlights Do Not Appear: Check
getZIndexinHighlights.tsxfor issues related to z-index or stacking contexts. - Witty Does Not Load: Investigate the target element detected in
handleFocusinElementinContentScriptApp.tsx. The issue may involve incorrect targeting of parent, child, or unrelated elements. - Highlights Displaced: Commonly related to
canvasSize,correctedPosition, orrangeRectspositions inHighlights.tsx. - Google Docs and Text Area Displaced Highlights: Ensure correct styling is applied in
GoogleDocsClone.tsxandTextAreaClone.tsx.
- Scrolling Displaces Highlights: Typically involves issues with
correctedPosition.topin thecanvasSizeorelementScrollinInput.tsx. - Alternatives Cannot Be Inserted: Explore different methods provided in
updateTextWithAlternativeinInput.tsx.
- Enabling/Disabling Witty Not Functioning: Often a problem with how the domain is stored or accessed, especially within iframe domains.
- CSS Conflicts: Ensure that CSS classes are properly namespaced to avoid conflicts.