I dropped an AI agent into my own website

The site the agent drives — my recruiter-facing career dossier, all client-side React. Nothing about it was built for automation.
One script, and your page has an agent
I played with page-agent — Alibaba’s in-page GUI agent — into my own career site, and pointing it at DeepSeek. The pitch that hooked me: no browser extension, no Python, no headless browser. Just in-page JavaScript. You type plain English, and the page does it.
The entire integration is essentially this:
import { PageAgent } from 'page-agent'
new PageAgent({
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com/v1',
apiKey, // my DeepSeek key
language: 'en-US',
})DeepSeek is OpenAI-compatible, so page-agent’s baseURL + apiKey slot straight in, and deepseek-chat supports tool calling — the only capability this needs. No vision model, no screenshots. That last part is the whole trick.
The DOM as a tool surface
page-agent builds on the browser-use lineage, and the mechanism is elegant. Every step it: serialises the interactive DOM into a compact text tree, numbers each actionable element, and hands that to the model. The model reasons about your request, then emits a tool call — click(index), scroll, and so on. page-agent executes it against the real DOM, observes the new state (the URL, the refreshed tree), and loops until the model declares the task done.
The element index is the key idea. Instead of asking the model to invent brittle CSS selectors, page-agent assigns each element a number and the model just points: “click [3].” Because the page is represented as text, a cheap, non-vision model like deepseek-chat is perfectly capable of driving it.
Watching it think
I started blunt: “Click the 02 Experience navigation link.” Here is the trace it produced, live on the page:

The agent’s own step-by-step trace: find the element, click index [3], observe the URL change to /#experience, then verify and report success.
Two steps: act, then verify. It clicked element [3], saw the page navigate to /#experience, and only then declared “Verdict: Success.” That observe-and-confirm loop is what separates an agent from a macro.
Precise instructions are the easy case. What impressed me was vague intent. I typed “Take me to Zakaria’s CV” — no element, no hint:
Page already loaded on the profile page. The nav menu has
sections: Trajectory, Experience, Personal, Systems, CV, Blog,
Contact, Sign in. Click the "07 CV" navigation link.
Clicking element [6]... -> Clicked element ([6] <a> 07)
Page navigated to -> /#cvIt enumerated the whole navigation, mapped my loose phrasing to the right link, and clicked it. That mapping is the reasoning — there is no selector I wrote that says “CV means the 07 link.”
Then a comprehension test: “What’s his job title and which company?”
Zakaria Bouazza’s current job title: Lead Engineer. Company: ING Luxembourg. His most recent role, held from June 2024 to present, based in Luxembourg.
Correct — and pulled straight from the text view of the DOM it had already ingested. It didn’t even need to scroll; the content was in its context because the whole page is just text to it.
The part that made me nervous
Here is the catch: page-agent runs entirely in the browser and calls the LLM directly with that apiKey. Whatever key you hand it ships in your bundle, readable by anyone with dev tools. On a public, recruiter-facing site, that is a non-starter.
So I gated it hard — local dev only, and behind a lazy import:
if (started || !IS_LOCAL_FRONTEND) return
const apiKey = import.meta.env.VITE_DEEPSEEK_API_KEY
if (!apiKey) return
void import('page-agent').then(({ PageAgent }) => { /* mount */ })Two layers: a hostname check so it only ever activates on localhost, and a dynamic import() so the code is a separate chunk. Then a happy accident from the bundler: in a production build, Vite statically replaces import.meta.env.VITE_DEEPSEEK_API_KEY with undefined; the guard folds to “always return,” and Rollup dead-code-eliminates the entire import('page-agent'). I checked the dist/ — no page-agent chunk, no api.deepseek.com string anywhere. In production the feature simply does not exist.
It’s a sharp contrast with the other AI feature on this site, the “Ask Zakaria” concierge, which is deliberately the opposite. The concierge answers untrusted, jailbreak-prone recruiter input, so it is given no tools at all — a pure text generator that can’t reach the DOM, the filesystem, or the network. page-agent is all tools. Same underlying question, answered two different ways: when do you give an LLM hands?
Would I ship it?
On a public page as-is, no — the key belongs behind a server-side proxy that injects it, exactly how the concierge keeps its key on the backend. But the experience is genuinely striking: an in-app copilot, natural-language form-filling (“fill these 20 fields from that one sentence”), accessibility through plain English — none of which needs a frontend rewrite. For internal tools, where the key already lives behind your own gateway, I’d reach for it tomorrow.
Forty-odd lines, one evening, and my CV site now takes orders in English. Not bad for a script tag.