One script tag. One element. One callback. The EvalQA form renders inside an iframe with full postMessage round-trips - your DOM, cookies, and CSP stay yours.
<!-- 1. Load --> <script src="https://eval.qa/embed.js"></script> <!-- 2. Target --> <div id="eval-here"></div> <!-- 3. Render --> <script> EvalQA.embed({ container: "#eval-here", template: "saas", taskId: "draft-9012", prompt: user.lastMessage, raterId: currentUser.email, onSave: ({ eval_id }) => { fetch("/api/drafts/9012/eval", { method: "POST", body: JSON.stringify({ eval_id }) }); } }); </script>
The SDK is framework-agnostic - every snippet below produces the same DOM. Pick whichever feels native.
<script src="https://eval.qa/embed.js"></script> <div id="eval-here"></div> <script> EvalQA.embed({ container: "#eval-here", template: "foundation", taskId: "msg-9012", prompt: "What is photosynthesis?", onSave: ({ eval_id }) => console.log("saved", eval_id) }); </script>
import { useEffect, useRef } from "react"; // once: <script src="https://eval.qa/embed.js" /> in your <head> export default function EvalPane({ taskId, prompt, raterId, onSave }) { const ref = useRef(); useEffect(() => { const ev = window.EvalQA.embed({ container: ref.current, template: "saas", taskId, prompt, raterId, onSave }); return () => ev.destroy(); }, [taskId]); return <div ref={ref} />; }
<template> <div ref="box"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from "vue"; const props = defineProps(["taskId","prompt","raterId"]); const emit = defineEmits(["saved"]); const box = ref(); let handle; onMounted(() => { handle = window.EvalQA.embed({ container: box.value, template: "saas", taskId: props.taskId, prompt: props.prompt, raterId: props.raterId, onSave: (p) => emit("saved", p) }); }); onBeforeUnmount(() => handle?.destroy()); </script>
"use client"; import { useEffect, useRef } from "react"; import Script from "next/script"; export default function EvalPane({ taskId, prompt }: { taskId: string; prompt: string }) { const ref = useRef<HTMLDivElement>(null); useEffect(() => { if (!(window as any).EvalQA) return; const ev = (window as any).EvalQA.embed({ container: ref.current, template: "saas", taskId, prompt }); return () => ev.destroy(); }, [taskId]); return <><Script src="https://eval.qa/embed.js" strategy="afterInteractive" /><div ref={ref} /></>; }
// Node - LLM judge writes directly to the API const verdict = await myJudge.grade(prompt, response); const res = await fetch("https://eval.qa/api/save_eval.php", { method: "POST", headers: { "Content-Type": "application/json", "X-Rater-Type": "llm_judge", "X-Rater-Model": "claude-sonnet-4-6" }, body: JSON.stringify({ schema_version: "1.0.0", rater: { type: "llm_judge", model: "claude-sonnet-4-6", n_samples: 5 }, subject: { system_under_test: "Acme Copilot", modality_tags: ["chat"] }, task: { task_id: "ticket-9012", modality: "chat", prompt, reference: gold }, universal: { overall_quality: { score: verdict.score, rationale: verdict.reason }, instruction_following: { score: 4 }, faithfulness: { score: 5 }, helpfulness: { score: 5 }, safety_overall: { score: 5 } } }) }); const { eval_id } = await res.json();
Production integrations from design partners. Each is < 20 lines of code.
The reviewer submits → onSave fires → you persist the link between your draft and our eval record.
EvalQA.embed({ container: "#review", template: "saas", taskId: "draft-"+id, prompt: msg, onSave: ({ eval_id }) => fetch("/api/drafts/"+id+"/eval",{ method:"POST", body: JSON.stringify({ eval_id }) }) });
Server-side LLM judge writes the eval; CI bails if any AILuminate hazard was flagged.
const r = await EvalQA.postEval(payload); if (r.payload?.safety?.is_violating_any) { process.exit(1); }
LLM judge fills overnight. A human opens the review URL the next morning.
EvalQA.embed({ container: "#review", evalId: pendingEvalId, mode: "review", raterId: user.email });
Tiny iframe alongside each AI response. Stored with rater type end_user.
EvalQA.embed({ container: "#fb-"+messageId, template: "enduser", taskId: "msg-"+messageId, prompt: msg.userText, height: 320 });
Full reference is on the integrate page. The essentials:
| Option | Type | Notes |
|---|---|---|
container | string / Element | CSS selector or DOM node. Required. |
template | string | foundation · agent · rag · robotics · saas · enduser · universal |
taskId | string | Stable identifier. Multiple raters → same task_id. |
prompt | string | The AI's input. Pre-fills task.prompt. |
reference | string | Gold answer. Strongly recommended for LLM-judge runs. |
raterId | string | Eval Army rater email. Pre-loads the profile. |
evalId + mode:"review" | string | Hybrid mode - load existing eval for human confirm/override. |
onSave | function | ({eval_id, payload}) => void |
height | number | Initial min-height. Auto-resizes as content grows. |
baseUrl | string | Override for self-host deployments. |
| Message | Direction | Payload |
|---|---|---|
eval:saved | iframe → parent | { eval_id, payload } |
eval:resize | iframe → parent | { height } · auto-handled |
eval:close | iframe → parent | fired on destroy() |
~5 KB minified, ~2 KB gzipped. Zero npm dependencies. Lives at https://eval.qa/embed.js with aggressive edge caching.
Yes. The form runs inside an iframe - your DOM, cookies, and CSP stay isolated. Communication is via window.postMessage with an explicit source === "evalqa" check. The SDK does the check for you.
Add https://eval.qa to script-src for the SDK and frame-src for the iframe. The SDK does not use unsafe-inline or unsafe-eval. Only the direct-API path (postEval) needs connect-src https://eval.qa.
Yes - use EvalQA.postEval(payload) or POST directly to /api/save_eval.php. Schema is at /eval-form.schema.json. No iframe required for server-side flows.
Modern evergreen browsers (Chrome, Safari, Firefox, Edge). The SDK uses URLSearchParams, fetch, and ResizeObserver - all baseline 2020+.
Yes. The /api/*.php files, /embed.js, and /eval-form.schema.json are PHP/JS and open-source. Drop into your host, then call EvalQA.embed({ baseUrl: "https://your-host/" }).
Three lines of code, one PR. Then iterate. Full docs and live demo on the integrate page.