AI sections fall back to fetch with no Qt bridge, for non-React web frontends

ai_sections.html fetches {API_BASE}+path when window.qt is absent; a page-check case routes those requests to server_stub.dispatch(). AI SKILL.md step 6 and frontend.md send plain-HTML/Vue/Angular/Svelte projects to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 15:55:30 +05:30
1 parent 344718007e
commit 461798127a
6 files changed
+100 -13

No files matched your search

+15 -6
View File
@@ -15,7 +15,7 @@ means `../rpa-dashboard-no-ai/templates/`.
| `templates/web/pricing.py` | The pricing table (with its source and as-of date) and `cost_for_call`. The backend imports it. |
| `templates/web/server_stub.py` | Sample-data backend for runs, review rows and the approve gate. Standard library only. |
| `templates/web/bands.py` | The band cut points and rules. The one source of truth: the backend imports it. |
| `templates/desktop/ai_sections.html` | Desktop: the Review tab (band legend, review split pane, drafts, Verify, approve gate) and the Cost tab in plain HTML, pasted into `<no-ai>dashboard.html`. |
| `templates/desktop/ai_sections.html` | Desktop, and web projects with a non-React frontend: the Review tab (band legend, review split pane, drafts, Verify, approve gate) and the Cost tab in plain HTML, pasted into `<no-ai>dashboard.html`. Talks over the Qt bridge, or with no Qt, fetches the `templates/contract.md` endpoints. |
| `templates/desktop/ai_bridge.py` | Desktop: the no-AI `Bridge` plus the `ai_request` slot that hands every AI request to the Python review backend. |
| `templates/contract.md` | The AI contract: runs, review rows, endpoints and the gate, over HTTP or the Qt bridge. Extends `<no-ai>contract.md`. |
| `templates/reference/` | Superbill Review extracts, marked EXAMPLE ONLY. Use them to port extra polish, never copy their project names. |
@@ -59,7 +59,8 @@ Skip this if the user chose keep. Offer:
Ask only if there is no frontend. If there is one, use its stack. If the project
uses PyQt, choose desktop and say so.
- **Web:** the React starter in `templates/web/`, served by the backend.
- **Web:** the React starter in `templates/web/`, served by the backend. An existing
non-React frontend gets the plain-HTML page instead (step 6, web).
- **Desktop:** the no-AI PyQt page with `templates/desktop/ai_sections.html` pasted
in, hosted by `templates/desktop/ai_bridge.py`. No web server, no npm.
@@ -83,9 +84,17 @@ Both need the endpoints in `templates/contract.md`; add them to the backend (and
## 6. Build (web)
1. Copy `templates/web/` into the project as `dashboard/` (an existing frontend:
see `<no-ai>frontend.md` section 3; in an existing React app, port the
`src/` components into it). Don't copy `node_modules/` or `dist/`.
**Existing plain-HTML, Vue, Angular or Svelte frontend:** don't use the React starter.
Build the no-AI page as `../rpa-dashboard-no-ai/SKILL.md` step 6 says (web, with
`API_BASE` filled, for example `/api`), paste the whole of `templates/desktop/ai_sections.html`
into it just before `</body>`, and put it where `<no-ai>frontend.md` section 3 says. With no
Qt bridge, its AI sections fetch `{API_BASE}` + the paths in `templates/contract.md`, so
the backend serves those (step 7). Port it into the app's components only if the user asks,
with `ai_sections.html` as the reference. Then skip to step 7.
1. Copy `templates/web/` into the project as `dashboard/` (an existing React app:
port the `src/` components into it, see `<no-ai>frontend.md` section 3).
Don't copy `node_modules/` or `dist/`.
2. Fill every `{{...}}` in `dashboard/index.html`'s CONFIG block:
| Placeholder | Value |
@@ -122,7 +131,7 @@ Both need the endpoints in `templates/contract.md`; add them to the backend (and
The contracts: `<no-ai>contract.md` for the shell, `templates/contract.md` for runs,
review rows and the gate.
- **Sample data (no backend for these endpoints yet):** keep `SAMPLE_DATA = true` in
`src/store.ts` and use `server_stub.py`. Replace its `SAMPLE_FIELDS` with the
`src/store.ts` (a plain-HTML page: in `dashboard.html`) and use `server_stub.py`. Replace its `SAMPLE_FIELDS` with the
fields the project's extraction really returns (name, a sample value, word scores,
mandatory). Tell the user which endpoints their backend must serve later.
- **Backend exists:** add the endpoints from `templates/contract.md` to it. Copy
@@ -2,9 +2,11 @@
Paste this whole block into the no-AI dashboard.html, just before </body>. It adds a Review
tab: band legend, band filters, the source document beside its extracted fields, drafts,
Verify, and Approve. Plain HTML, the page's own theme tokens, no build step.
Every request goes over the Qt bridge as one slot call, ai_request(id, method, path, body),
and comes back as an `ai_response` envelope (see ../contract.md, "Desktop"). The approve
gate is the Python backend's: this page only asks, and shows the refusal.
In PyQt, every request goes over the Qt bridge as one slot call, ai_request(id, method, path,
body), and comes back as an `ai_response` envelope (see ../contract.md, "Desktop"). With no
Qt (a plain-HTML web project), it fetches {API_BASE}{path} from the backend instead, the
web endpoints in ../contract.md. The approve gate is the backend's: this page only asks,
and shows the refusal.
-->
<style>
/* BAND COLOURS: semantic only (swatches, the rule down a field). The same in every preset and both modes. */
@@ -117,10 +119,19 @@ showTab=t=>{
};
window.addEventListener('beforeunload',e=>{ if(dirty().length) e.preventDefault(); });
// ----- transport: one bridge slot out, one `ai_response` envelope back -----
// ----- transport: in PyQt, one bridge slot out, one `ai_response` envelope back;
// with no Qt (a plain web page), fetch {API_BASE}{path} from the backend -----
const pending={}; let seq=0;
async function aiFetch(method,path,body){
const r=await fetch(API_BASE+path,{method,headers:{'Content-Type':'application/json'},
body:method==='GET'?undefined:JSON.stringify(body||{})});
const b=await r.json().catch(()=>({}));
if(!r.ok) throw new Error(b.detail||`Error ${r.status}`);
return b;
}
function ai(method,path,body){
if(!bridge) return Promise.reject(new Error('Review needs the desktop app (no Qt bridge here).'));
if(!window.qt) return aiFetch(method,path,body);
if(!bridge) return Promise.reject(new Error('The Qt bridge is not ready yet. Try again.'));
return new Promise((ok,fail)=>{ const id=++seq; pending[id]=[ok,fail];
bridge.ai_request(id,method,path,JSON.stringify(body||{})); });
}
@@ -149,7 +160,8 @@ function openRun(id){
clearTimeout(timer);
jobId=id; run=null; rows=[]; drafts={}; refusal=null;
$('rv-doc-pick').value=id;
ai('GET',`/runs/${id}/document`).then(doc=>{ if(id===jobId) $('rv-doc').src=
if(!window.qt) $('rv-doc').src=`${API_BASE}/runs/${encodeURIComponent(id)}/document`; // web: the URL itself
else ai('GET',`/runs/${id}/document`).then(doc=>{ if(id===jobId) $('rv-doc').src=
String(doc).startsWith('<') ? 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(doc) : doc; }).catch(()=>{});
render(); tick(id);
}
@@ -54,6 +54,12 @@ before you report.
| React, Vue, Angular, Svelte | In the app's `public/` folder (`public/dashboard.html`), so it is served as-is. Port it into the app's components only if the user asks: best effort, with the page as reference. |
| PyQt web view | Next to the host's page. Use the desktop path (`bridge.py`). |
**AI skill (`rpa-dashboard-ai`):** a React app gets the React starter's components
ported in. Plain HTML, Vue, Angular and Svelte get this same page with the AI skill's
`templates/desktop/ai_sections.html` pasted in before `</body>`, placed as above. With no
Qt bridge it fetches `{API_BASE}` + the AI contract's endpoints. A port into components
uses `ai_sections.html` as the reference.
Don't edit existing pages to link to it. Tell the user the path to link from
their nav.
+4
View File
@@ -40,6 +40,10 @@ uv run --with pytest --with playwright pytest tests
does). Runs the AI web page's review and cost cases and the no-AI cases unchanged, plus
a tampered page calling the slot directly (still refused). To check a generated folder,
set `PAGE_CHECK_AI_DESKTOP_DIR=path/to/dashboard`; `PAGE_CHECK_WIRED=1` as above.
`test_ai_fetch.py` runs the review cases on the same page with no Qt bridge (a plain-HTML
web project): the AI sections fetch `/api/...`, routed to `server_stub.dispatch()`.
- `page_check_common.py`: what the three page checks share (modes, test fill, fake Qt
bridge, stub loader, page opener, the list of no-AI cases the AI pages rerun).
- `test_pricing.py`: the AI cost function (normal, long-context, thinking tokens,
unknown model).
- `test_reference_scan.py`: no patient data, internal IPs, credentials or keys in any
@@ -0,0 +1,54 @@
"""The same AI sections with no Qt bridge: a plain-HTML (or Vue/Angular) web project.
The page is served over HTTP with API_BASE "/api", and every AI request goes out as
fetch({API_BASE}{path}). Playwright routes the API to the review backend's dispatch()
in this process (what a real backend serves; server_stub.py over HTTP does the same).
This module's `page` replaces the folder's bridge one; the review cases run unchanged.
"""
import json
import sys
from pathlib import Path
import pytest
from page_check_common import MODES, open_page
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "page_check_ai"))
from test_ai_review import ( # noqa: E402,F401
test_approve_stays_blocked_while_rows_are_unverified,
test_band_counts_and_legend_match,
test_band_filters_show_the_right_rows,
test_draft_edit_is_not_saved_until_save,
test_page_x_of_y_updates_while_running,
)
from test_ai_cost import cost_page, test_coverage_pill_counts_priced_of_all_documents # noqa: E402,F401
ORIGIN = "http://dashboard.test"
NO_BRIDGE = "window.__calls = [];"
@pytest.fixture(params=list(MODES))
def page(request, browser, site, stub):
html = (site / "dashboard.html").read_text(encoding="utf-8").replace(
'const API_BASE = "";', 'const API_BASE = "/api";')
def serve(route):
path = route.request.url[len(ORIGIN):].split("?")[0]
if not path.startswith("/api/"):
return route.fulfill(body=html, content_type="text/html")
body = json.loads(route.request.post_data or "{}")
status, payload = stub.dispatch(route.request.method, path[len("/api"):], body)
if isinstance(payload, str):
return route.fulfill(status=status, body=payload, content_type="image/svg+xml")
route.fulfill(status=status, body=json.dumps(payload), content_type="application/json")
yield from open_page(browser, ORIGIN + "/dashboard.html", request.param, NO_BRIDGE,
route=(ORIGIN + "/**", serve))
def test_ai_requests_go_over_fetch(page, stub):
"""With no Qt bridge, nothing reaches a bridge slot: the review loads over HTTP."""
from test_ai_review import FIELDS, open_review
stub.put_run("job-f", "fetch.pdf", FIELDS)
open_review(page, "job-f")
assert page.evaluate("window.__calls") == []
assert page.locator("#rv-doc").get_attribute("src") == "/api/runs/job-f/document"
+3 -1
View File
@@ -63,13 +63,15 @@ def push_to(pg, event, data):
pg.evaluate("([event, data]) => window.__pyMsg(JSON.stringify({event, data}))", [event, data])
def open_page(browser, url, mode, init_script, expose=None):
def open_page(browser, url, mode, init_script, expose=None, route=None):
"""Generator for a `page` fixture: opens url in one mode, signs in through a login
section if the page has one, switches to dark, and fails on any page script error."""
width, height, dark = MODES[mode]
ctx = browser.new_context(viewport={"width": width, "height": height})
if expose:
ctx.expose_function(*expose)
if route:
ctx.route(*route)
ctx.add_init_script(init_script)
pg = ctx.new_page()
pg.set_default_timeout(5000)