Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/embed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Embed widget

# Builds and tests the WebBrain for websites widget (embed/) whenever it or
# the extension code it bundles changes, including loading embed.js in Chrome.
# Pushing a tag like embed-v1.0.0 publishes a GitHub release with embed.js,
# tools.json, the icons, VERSION.json and SHA256SUMS, which sites.webbrain.one
# pins by version.

on:
pull_request:
paths:
- ".github/workflows/embed.yml"
- "embed/**"
- "package.json"
- "package-lock.json"
- "src/chrome/**"
push:
branches:
- main
paths:
- ".github/workflows/embed.yml"
- "embed/**"
- "package.json"
- "package-lock.json"
- "src/chrome/**"
tags:
- "embed-v*"

permissions:
contents: read

concurrency:
group: embed-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 10
defaults:
run:
working-directory: embed
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Build and test
run: |
npm run build
npm test

# Playwright comes from the repository root; the widget itself has no dependencies.
- name: Install Playwright
working-directory: .
run: npm ci

- name: Run embed.js in Chrome
env:
EMBED_CHROME_PATH: /usr/bin/google-chrome
run: npm run test:browser

release:
if: startsWith(github.ref, 'refs/tags/embed-v')
needs: test
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: write
defaults:
run:
working-directory: embed
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Check the tag matches embed/package.json
run: |
version="$(node -p "require('./package.json').version")"
if [ "${GITHUB_REF_NAME}" != "embed-v${version}" ]; then
echo "Tag ${GITHUB_REF_NAME} does not match embed/package.json version ${version}" >&2
exit 1
fi

- name: Build
run: npm run build

- name: Publish the release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" dist/* \
--title "WebBrain for websites widget ${GITHUB_REF_NAME#embed-v}" \
--notes "The script site owners add with <script src=\".../embed.js\">, built from commit ${GITHUB_SHA}. Verify downloads against SHA256SUMS. Source: https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}/embed" \
--latest=false
1 change: 1 addition & 0 deletions embed/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
70 changes: 70 additions & 0 deletions embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# WebBrain for websites — the embed widget

The script a site owner adds to their pages to give every visitor a WebBrain
assistant:

```html
<script src="https://sites.webbrain.one/embed.js" data-site-id="site_…"></script>
```

It is built from the WebBrain extension's own code in this repository, so the
widget reads and acts on pages exactly the way the extension does:

| Extension file (`src/chrome/`) | What the widget uses it for |
| --- | --- |
| `src/content/accessibility-tree.js` | Reading the page as an accessibility tree |
| `src/content/content.js` | Clicking, typing, scrolling and extracting on the page |
| `src/content/agent-visual-indicator.js` | The on-page activity glow |
| `src/ui/activity-brain.js` | The thinking animation |
| `src/ui/markdown-render.js` | Rendering answers |
| `src/agent/calculator.js` | Exact arithmetic |
| `src/agent/read-page-window.js` | Paging `read_page` results |
| `src/agent/credential-fields.js` | Keeping credential field values out of tool results |
| `src/agent/tools.js`, `adapters.js` | The Ask and Act tools and prompts in `tools.json` |

`src/widget-runtime.js` adds the launcher, the chat panel and the agent loop
that talks to the server. The widget only runs the tools `tools.json` lists
for the turn's mode, and Act is off unless the site's config sets
`modePolicy: "ask_and_act"`.

Elements with `data-webbrain-ask="…"` start an Ask run when a visitor clicks
them. `data-webbrain-act="…"` only puts its prompt in the composer for the
visitor to send, since any markup on the page, including content the site's
users wrote, can carry the attribute.

The widget is licensed GPL-3.0-or-later, like the rest of WebBrain. Every copy
of `embed.js` states its license and links back to this folder.

## Build and test

No dependencies to install; Node 20 or newer.

```bash
cd embed
npm run build # writes dist/
npm test
npm run test:browser # loads embed.js in Chrome; needs `npm ci` at the repository root
```

`dist/` contains:

| File | Purpose |
| --- | --- |
| `embed.js` | The widget |
| `tools.json` | System prompts and tool definitions for the server that answers the widget |
| `icon48.png`, `icon128.png` | WebBrain icons the widget shows |
| `VERSION.json` | Widget version, the commit it was built from, and a link to that source |
| `SHA256SUMS` | Checksums of the files above |

## Releases

1. Bump `version` in `embed/package.json`.
2. Push a tag `embed-v<version>`, for example `embed-v1.0.0`.

The **Embed widget** workflow builds, tests and publishes a GitHub release
with the files above. It is never marked as the repository's latest release,
so extension releases stay on top.

The server behind sites.webbrain.one pins one of these releases by version and
checks it against `SHA256SUMS`. Pull requests that change the widget or the
extension files it bundles run the same build and tests.
18 changes: 18 additions & 0 deletions embed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@webbrain/embed",
"version": "1.0.0",
"private": true,
"description": "WebBrain for websites: the script site owners add to their pages. Built from the WebBrain extension's own content scripts.",
"type": "module",
"license": "GPL-3.0-or-later",
"scripts": {
"build": "node scripts/build.mjs",
"test": "node --no-warnings --test test/*.test.mjs",
"test:browser": "node --no-warnings --test test/browser.mjs"
},
"repository": {
"type": "git",
"url": "https://github.com/webbrain-one/webbrain.git",
"directory": "embed"
}
}
63 changes: 63 additions & 0 deletions embed/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env node
/**
* Builds the WebBrain for websites widget into embed/dist/:
*
* embed.js the script site owners add with <script src=".../embed.js">
* tools.json prompts and tool definitions for the server that answers it
* icon48.png, icon128.png
* VERSION.json widget version and the WebBrain commit it was built from
* SHA256SUMS checksums of the files above, for servers that pin a release
*
* The bundle is assembled from the extension's own code in src/chrome, so a
* change there reaches the widget on the next build.
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import { execFileSync } from 'node:child_process';
import { PROJECT_ROOT, REPO_ROOT, EXTENSION_PATHS } from '../src/webbrain.js';
import { buildEmbedBundle } from '../src/bundle.js';
import { buildAgentSpec } from '../src/agent-spec.js';

const distDir = path.resolve(PROJECT_ROOT, 'dist');
fs.rmSync(distDir, { recursive: true, force: true });
fs.mkdirSync(distDir, { recursive: true });

const write = (name, contents) => fs.writeFileSync(path.join(distDir, name), contents);

const bundle = buildEmbedBundle();
// Parse with the grammar of a classic <script>, the way sites load embed.js.
// (`node --check` would parse it as an ES module, since this package is "type": "module".)
new vm.Script(bundle, { filename: 'embed.js' });
write('embed.js', bundle);

write('tools.json', `${JSON.stringify(buildAgentSpec(), null, 2)}\n`);
fs.copyFileSync(EXTENSION_PATHS.icon48, path.join(distDir, 'icon48.png'));
fs.copyFileSync(EXTENSION_PATHS.icon128, path.join(distDir, 'icon128.png'));

const pkg = JSON.parse(fs.readFileSync(path.resolve(PROJECT_ROOT, 'package.json'), 'utf8'));
let commit = process.env.GITHUB_SHA || '';
if (!commit) {
try {
commit = execFileSync('git', ['-C', REPO_ROOT, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {
commit = 'unknown';
}
}
write('VERSION.json', `${JSON.stringify({
name: pkg.name,
version: pkg.version,
commit,
source: `https://github.com/webbrain-one/webbrain/tree/${commit === 'unknown' ? 'main' : commit}/embed`,
license: pkg.license,
}, null, 2)}\n`);

const files = ['embed.js', 'tools.json', 'icon48.png', 'icon128.png', 'VERSION.json'];
const sums = files.map((name) => {
const hash = crypto.createHash('sha256').update(fs.readFileSync(path.join(distDir, name))).digest('hex');
return `${hash} ${name}`;
});
write('SHA256SUMS', `${sums.join('\n')}\n`);

console.log(`✓ Built embed/dist/embed.js (${(Buffer.byteLength(bundle) / 1024).toFixed(1)} KB), tools.json, icons, VERSION.json and SHA256SUMS`);
27 changes: 27 additions & 0 deletions embed/src/agent-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
getEmbedToolsForMode,
SYSTEM_PROMPT_ASK,
SYSTEM_PROMPT_ACT_COMPACT,
UNIVERSAL_PREAMBLE,
} from './webbrain.js';

/**
* The agent specification a server needs to drive the widget: the system
* prompts and the tool definitions for Ask and Act mode, taken from the
* extension so both always agree. Published as `tools.json` with each release.
*/
export function buildAgentSpec() {
const ask = getEmbedToolsForMode('ask');
const act = getEmbedToolsForMode('act');
return {
version: 1,
prompts: {
ask: SYSTEM_PROMPT_ASK,
act: SYSTEM_PROMPT_ACT_COMPACT,
universalPreamble: UNIVERSAL_PREAMBLE,
},
tools: { ask, act },
askToolNames: ask.map((tool) => tool.function.name),
actToolNames: act.map((tool) => tool.function.name),
};
}
Loading
Loading