# Add documentation Q&A (/en/docs/cookbook/scenarios/docs-qa-assistant)



Add an on-demand Q\&A window to a public documentation site. Clicking the button or pressing ⌘K / Ctrl+K downloads PraxisKit; closing the window restores focus. Visitors use a publishable key to obtain separate anonymous identities without signing in. This recipe provides a complete page and local server.

## Prepare public content and identity [#prepare]

You need Bun, a Develop service agent, and a publishable key created through the [anonymous visitor guide](/en/docs/developers/agent/embed/tokens#anonymous). Use an available `deepseek-flash` model. Prepare a test policy using the [web integration guide](/en/docs/developers/agent/embed/quickstart#prepare). This example runs at `http://localhost:4242` with localhost enabled. For a live site, configure its HTTPS origin and anonymous limits.

See [Embed in a web page](/en/docs/developers/agent/embed/quickstart#mount) for Kit installation, fixed versions, and integrity verification. The [component reference](/en/docs/developers/agent/embed/component-reference) defines shared options. This recipe uses them for on-demand loading and window interactions; it needs no server API Key.

Use this recipe only for content anyone may read. If documents require sign-in, use trusted token issuance from the [signed-in user recipe](/en/docs/cookbook/scenarios/saas-embedded-assistant). Do not put private documentation in an anonymous agent.

## Define the answer scope [#content]

In the dashboard, open **Service agents → your agent → Integration and configuration → Configuration → Instructions**. Save the sample content below, replacing it with verified product information, then select the new version. This example puts a small public FAQ into instructions. It has no crawling, indexing, or retrieval step and is not a complete retrieval-based knowledge system.

```text
You are a product help-center assistant. Answer only from the public material below. If it does not cover the question, say you do not know and direct the visitor to the help center. Do not invent policies or read account or order data.
Material: Export a report: open Reports, click Export, and select CSV. The download uses the current report filters.
Material: Change notifications: open Notifications in Settings, select the notification types, and save.
```

Keep only capabilities needed by this task under **Configuration → Tools and authorization**; this example needs no business tools. In **Try and debug**, ask how to export a report, then ask an unrelated question. &#x2A;*Expected:** The first answer follows the supplied steps; the second acknowledges missing information. Instructions and content may be repeated to visitors, so never include secrets.

## Implement the on-demand window [#implementation]

Save the following as `index.html`. Replace `PRAXIS_PUBLISHABLE_KEY` with your actual publishable key. It may appear in page source; never substitute an organization API Key. The shared `pending` promise combines simultaneous open actions. A failed load clears it so the visitor can retry.

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Product documentation</title>
<h1>Product documentation</h1>
<button id="ask" type="button">Ask the docs (⌘K / Ctrl+K)</button>
<p id="status" role="status" aria-live="polite"></p>
<script type="module">
  const button = document.querySelector("#ask");
  const status = document.querySelector("#status");
  const kitUrl = "https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js";
  const integrity = "sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU";
  let pending;
  let returnFocus = button;
  function loadKit() {
    if (customElements.get("praxis-agent")) return Promise.resolve();
    return new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.type = "module";
      script.src = kitUrl;
      script.integrity = integrity;
      script.crossOrigin = "anonymous";
      const timer = setTimeout(() => {
        script.remove(); reject(new Error("Kit load timeout"));
      }, 15000);
      script.onload = () => {
        clearTimeout(timer);
        if (customElements.get("praxis-agent")) resolve();
        else reject(new Error("Kit element unavailable"));
      };
      script.onerror = () => {
        clearTimeout(timer); script.remove(); reject(new Error("Kit load failed"));
      };
      document.head.appendChild(script);
    });
  }
  function assistant() {
    pending ??= (async () => {
      await loadKit();
      const agent = document.createElement("praxis-agent");
      agent.setOptions({
        api: {
          url: "https://api-develop.deepraxis.com",
          publishableKey: "PRAXIS_PUBLISHABLE_KEY",
        },
        agent: { name: "Documentation assistant" },
        layout: "modal",
        launcher: { type: "none" },
        header: { title: "Ask the docs" },
        startScreen: {
          greeting: "Ask about the public product guide.",
          prompts: [{ label: "Export a report", prompt: "How do I export a report?" }],
        },
        composer: { placeholder: "Ask a product question" },
      });
      agent.addEventListener("praxis.close", () => {
        button.setAttribute("aria-expanded", "false");
        returnFocus?.focus();
      });
      agent.addEventListener("praxis.error", () => {
        status.textContent = "Chat unavailable. Try again later or read the guide.";
      });
      document.body.appendChild(agent);
      return agent;
    })().catch(error => { pending = undefined; throw error; });
    return pending;
  }
  async function openAssistant() {
    returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : button;
    button.disabled = true;
    status.textContent = "Loading assistant…";
    try {
      const agent = await assistant();
      agent.open();
      button.setAttribute("aria-expanded", "true");
      status.textContent = "";
    } catch { status.textContent = "Could not load the assistant. Press the button to retry."; }
    finally { button.disabled = false; }
  }
  button.setAttribute("aria-haspopup", "dialog");
  button.setAttribute("aria-expanded", "false");
  button.addEventListener("click", openAssistant);
  document.addEventListener("keydown", event => {
    if (!event.repeat && !event.isComposing && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
      event.preventDefault();
      if (!button.disabled) void openAssistant();
    }
  });
</script>
</html>
```

Save this launcher as `server.ts`, run `bun server.ts`, and open the printed address.

```ts
const server = Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  fetch(request) {
    const url = new URL(request.url);
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: { "Cache-Control": "no-store" } });
    }
    return new Response("Not found", { status: 404 });
  },
});
console.log(`Open http://localhost:${server.port}`);
```

**Expected:** The initial page makes no Kit download request. Clicking the button loads the module and opens a centered window. Later opens reuse the same element and anonymous identity. Escape closes it and restores focus to the previous control. The page owns the shortcut; Kit supplies the window layout, backdrop, and close behavior.

## Automated checks and answer acceptance [#verify]

Install the browser test dependency, save the following as `docs-qa.spec.ts`, and run it while the local site is available:

```bash
bun add --dev @playwright/test
bunx playwright install chromium
bunx playwright test docs-qa.spec.ts --workers=1
```

```ts
import { test, expect } from "@playwright/test";

test("loads once, opens a modal, and restores focus", async ({ page }) => {
  const kitRequests = [];
  page.on("request", request => {
    if (request.url().endsWith("/praxis-kit.js")) kitRequests.push(request.url());
  });
  await page.goto("http://localhost:4242");
  expect(kitRequests).toHaveLength(0);
  const button = page.getByRole("button", { name: /Ask the docs/ });
  await button.click();
  const dialog = page.locator("praxis-agent dialog");
  await expect(dialog).toBeVisible();
  await expect(button).toHaveAttribute("aria-expanded", "true");
  await page.keyboard.press("Escape");
  await expect(dialog).not.toBeVisible();
  await expect(button).toBeFocused();
  await page.keyboard.press("Control+k");
  await expect(dialog).toBeVisible();
  await expect(page.locator("praxis-agent")).toHaveCount(1);
  expect(kitRequests).toHaveLength(1);
});
```

This test checks deferred loading, a single instance, keyboard interaction, and focus. Seeing a component is not proof of a working conversation. Next, ask how to export a report and verify the stated steps. Ask an unsupported question and confirm that the answer does not invent information. Match the messages and anonymous user in **Operations and management → Sessions**.

Use a separate browser profile for another visitor and confirm that it cannot see the first visitor's conversations. Refresh the original browser to check identity continuity. Kit stores `visitor_secret` for this purpose and falls back to page memory when storage is unavailable. Never copy or share that value. A publishable key does not turn every visitor into one signed-in account.

## Failure branches and cleanup [#failures]

* A download failure displays a retry message. Check the network, CSP, and fixed version URL, then click again. A failed download should not leave a partially initialized component.
* If the window opens but cannot reply, check that the publishable key is enabled, the origin is allowed, and budget remains. Use the actual response with [Embed limits and errors](/en/docs/developers/agent/embed/limits-and-errors).
* Reopening cannot resolve an origin or quota refusal. Correct the dashboard configuration before testing another real reply.
* For wrong answers, update the material and instructions, verify the selected version, and start a fresh conversation. Increasing a timeout cannot fill a knowledge gap.

Stop the local server after testing. Disable the dedicated test publishable key and revoke unwanted test access through [End-user management](/en/docs/developers/agent/end-users). Closing the window alone does not revoke tokens. Before launch, replace the test origin with the real site and complete the [go-live checklist](/en/docs/developers/agent/embed/go-live).

## Source and extensions [#next]

The `index.html`, `server.ts`, and `docs-qa.spec.ts` blocks on this page form the complete example. To include the current article title, continue with [Pass context](/en/docs/developers/agent/embed/context). For private order data, use the [SaaS signed-in user recipe](/en/docs/cookbook/scenarios/saas-embedded-assistant).
