Skip to content

Element picker on the website widget #1

Description

@ezzcodeezzlife

Summary

Let visitors point at the actual UI they are talking about, instead of describing it in words. The widget today is a text box plus pagePath / pageUrl. An element picker is the single biggest jump in agent accuracy for visual/layout feedback (“this button”, “this card on mobile”).

This is table stakes for Marker.io, BugHerd, Usersnap, and similar visual feedback tools. feedback2code’s differentiator is that we turn that pin into a PR, so the selector + snippet should go all the way into the agent prompt and the PR body.

Current behavior

The embed is a cross-origin iframe, not an in-page DOM overlay:

  1. Parent site loads /widget/{wid} (app/widget/[wid]/route.ts).
  2. buildParentEmbedScript in lib/widget-embed.ts injects a full-viewport iframe from /embed/frame?w=….
  3. The iframe posts { f2c: "f2c-frame-ready" }; the parent replies with { f2c: "parent", href, pathname } only.
  4. Submit payload to POST /f is { w, text, pageUrl, pagePath, parentOrigin, turnstileToken }.
  5. WidgetFeedback stores body, pageUrl, pagePath (prisma/schema.prisma). Those two location fields are already passed into buildOpencodeFeedbackPrompt and buildFeedbackPrBody.

The iframe cannot read the parent DOM. Any picker must run in the parent script (or a same-origin overlay injected there) and talk to the iframe over postMessage.

Problem

Clients rarely know CSS class names. They say “the orange button on the hero”. The agent already gets pagePath (good) but still has to guess which node. Wrong file / wrong component is the expensive failure mode (wasted sandbox + quota).

Proposed design

UX

  • In the widget panel, a control: Pick element (or “Point at the page”).
  • Clicking it:
    1. Collapses or pointer-events: nones the iframe so clicks hit the parent page.
    2. Parent overlay highlights hovered nodes (outline + label: tag, id, a short class, or accessible name).
    3. Click selects; Esc / second click on empty cancels.
  • Selected target appears in the panel as a chip: button.cta / Nav › Pricing plus a tiny screenshot thumbnail if we capture one.
  • Submit still requires text (keep the 2000-char body). Picker is optional context, not a replacement for the request.

Data to capture (parent → iframe → POST /f)

Keep the payload bounded and reviewable:

Field Why
selector Stable-ish CSS path (prefer id, data-testid, aria-label; fall back to nth-of-type path). Cap length (~500).
elementTag / elementText Short visible text (truncated) so the PR is human-readable.
boundingRect { x, y, w, h, vw, vh } so the agent knows layout context / likely breakpoint.
outerHtml Truncated (~2–4 KB), sanitized (no <script>, no inline event handlers).
screenshot (v2) Optional cropped PNG of the element; do not block v1 on this.

Store on WidgetFeedback (new nullable JSON column, e.g. elementTarget Json?) rather than many scalar columns.

Agent + PR

  • Prompt (lib/feedback-agent/prompt.ts): add a block “Visitor selected this element…” with selector, text, and truncated HTML. Instruct the agent to start from that node / corresponding component, not a repo-wide hunt.
  • PR body (lib/feedback-agent/feedback-pr-copy.ts): a table row Selected element + fenced selector, same as we already do for page path / submitted URL.

Parent ↔ iframe protocol

Extend the existing f2c message namespace (do not use * origin for secrets; keep origin checks as today):

  • iframe → parent: { f2c: "pick-start" } / { f2c: "pick-cancel" }
  • parent → iframe: { f2c: "pick-result", target: { … } } / { f2c: "pick-cancelled" }

While picking, shrink the iframe hit target (e.g. height to the FAB) so the page is clickable. Restore on cancel/select.

Assumptions

  • v1 is selector + text + truncated HTML, not full-page screenshots. Screenshots are a follow-up (storage, PII, bandwidth).
  • Picker is on by default for every widget. No plan gate for v1 — accuracy is the product.
  • We generate selectors ourselves; we do not depend on the host app exposing data-f2c attributes (nice-to-have later in docs).
  • Shadow DOM / cross-origin iframes inside the client site: skip or pick the host element; document the limitation.
  • Same-origin vs cross-origin parent: parent script already runs in the client page, so picker works even when the iframe origin is feedback2code.dev.

Security / privacy

  • Treat outerHtml as untrusted. Strip scripts, on*, srcdoc, javascript: URLs before persist and before prompt injection.
  • Do not capture password/inputs (input[type=password], payment fields). If the hit target is in a sensitive field, select the nearest non-sensitive ancestor and label it as such.
  • Selector + HTML go into GitHub PRs (often public). Warn in configure UI: “Element HTML may appear on the PR.”
  • Turnstile and domain allowlist (lib/widget-resolve.ts) unchanged.

Acceptance criteria

  • “Pick element” in the widget; hover outline on the parent page; click selects; Esc cancels.
  • Iframe does not swallow clicks during pick mode.
  • Submit includes element payload; dashboard feedback row shows selector/text.
  • Agent prompt and PR body include the selected element.
  • Works on a real third-party origin (authorized domain), not only localhost.
  • No picker: submit still works (backward compatible).
  • Password / payment fields are not captured as inner HTML.

Out of scope

  • Annotated screenshots, arrows, consoles logs, network HAR (Marker.io parity).
  • Click-maps / heatmaps.
  • Multi-element select.

Implementation notes

Primary files:

  • lib/widget-embed.ts — parent script (picker + postMessage) and iframe UI
  • app/f/route.ts — validate/store payload
  • prisma/schema.prismaWidgetFeedback.elementTarget
  • lib/feedback-agent/prompt.ts, lib/feedback-agent/feedback-pr-copy.ts
  • components/repo/repo-feedbacks-panel.tsx — show chip on the dashboard

Widget JS is cached max-age=300 (app/widget/[wid]/route.ts); iframe is no-store. Plan for a protocol version on messages so mixed old/new parents fail closed.

See also

  • Widget theme (accent/position will share the parent-script config channel)
  • Page-path allowlist (picker should not run on disabled paths)
  • Iterate on feedback PRs (a follow-up could re-use the same pinned element)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:widgetEmbeddable widget, iframe, parent scriptenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions