@uibot/vlm-scorer 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html-self-check.d.ts +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -7
- package/package.json +11 -7
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface HeadlessHtmlSelfCheckOptions {
|
|
2
|
+
/** Chrome / Chromium executable (puppeteer-core does not download one). */
|
|
3
|
+
chromePath: string;
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
/** ms to let scripts run after load. Default 800. */
|
|
7
|
+
settleMs?: number;
|
|
8
|
+
/** Whole-check ceiling in ms. Default 15000. */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export type HtmlSelfCheckResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
detail: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Runtime self-check for a generated HTML document (moved up from InfoOS App Build): render it in
|
|
19
|
+
* headless Chrome the way the sandboxed preview runs it (storage shim first, network blocked) and
|
|
20
|
+
* fail on an uncaught script error or a page that renders nothing visible.
|
|
21
|
+
*
|
|
22
|
+
* Throws when puppeteer-core or Chrome is unavailable — a host that wires this check gets
|
|
23
|
+
* "unavailable", never a silent pass.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createHeadlessHtmlSelfCheck(options: HeadlessHtmlSelfCheckOptions): (input: {
|
|
26
|
+
html: string;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}) => Promise<HtmlSelfCheckResult>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { createVlmVisualScoreProvider, type CreateVlmVisualScoreProviderOptions,
|
|
|
2
2
|
export { createModelHtmlGenerator } from "./model-html-generator.js";
|
|
3
3
|
export { renderSpecToPng, type RenderScreenshotOptions } from "./render-screenshot.js";
|
|
4
4
|
export { createHandoffVisualCritiqueProvider, createRoutedHandoffCritiqueProvider, type CreateHandoffVisualCritiqueProviderOptions, type HandoffVisualCritiqueInput, type HandoffVisualCritiqueProvider, type HandoffVisualCritiqueResult, type HandoffVisualCritiqueSource, type HandoffRenderHtmlFn, } from "./handoff-visual-critique.js";
|
|
5
|
+
export { createHeadlessHtmlSelfCheck, type HeadlessHtmlSelfCheckOptions, type HtmlSelfCheckResult, } from "./html-self-check.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createHash as
|
|
2
|
+
import{createHash as O}from"crypto";function p(e,t){return O("sha256").update(JSON.stringify([e,t])).digest("hex")}function b(e){return typeof e==="number"&&Number.isFinite(e)&&e>=0&&e<=1}var R="title|text|label|content|description|heading|subtitle|placeholder|body|caption|name",Re=new RegExp(`["']?\\b(?:${R})["']?\\s*:\\s*["']([^"'\\n]*)["']`,"gi"),Ce=new RegExp(`^\\s*-?\\s*(?:${R})\\s*:\\s*([^\\s"'{\\[][^\\n]*)$`,"gim");function A(e){let t=e.trim();if(t.startsWith("{")&&t.endsWith("}"))return t;let r=0,a=-1,n=!1,i=!1;for(let o=0;o<e.length;o+=1){let s=e[o];if(n){if(i)i=!1;else if(s==="\\")i=!0;else if(s==='"')n=!1;continue}if(s==='"')n=!0;else if(s==="{"){if(r===0)a=o;r+=1}else if(s==="}"){if(r-=1,r===0&&a!==-1)return e.slice(a,o+1)}}return null}function h(e){let t=A(e);if(t===null)return null;let r;try{r=JSON.parse(t)}catch{return null}if(typeof r.score!=="number"||!Number.isFinite(r.score))return null;if(r.score<0||r.score>1)return null;return typeof r.reasoning==="string"?{score:r.score,reasoning:r.reasoning}:{score:r.score}}function j(e,t){return`<!DOCTYPE html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8" />
|
|
@@ -8,13 +8,82 @@ import{createHash as C}from"crypto";function m(e,t){return C("sha256").update(JS
|
|
|
8
8
|
<style>body { margin: 0; background: white; }</style>
|
|
9
9
|
</head>
|
|
10
10
|
<body>${e}</body>
|
|
11
|
-
</html>`}var
|
|
11
|
+
</html>`}var B="https://cdn.tailwindcss.com";async function f(e,t={}){let r;try{r=await import("puppeteer-core")}catch{return console.warn("[vlm-scorer] puppeteer-core not installed; screenshots unavailable"),null}let a=t.width??1200,n=t.height??800,i=t.deviceScaleFactor??1,o=t.settleMs??500,s=/<html[\s>]/i.test(e)?e:j(e,t.tailwindPlayUrl??B),c=null;try{c=await r.launch({headless:!0,...t.chromePath?{executablePath:t.chromePath}:{},args:["--no-sandbox","--disable-setuid-sandbox"]});let l=await c.newPage();if(await l.setViewport({width:a,height:n,deviceScaleFactor:i}),await l.setContent(s,{waitUntil:"networkidle0",timeout:15000}),o>0)await new Promise((u)=>setTimeout(u,o));let d=await l.screenshot({type:"png",fullPage:!1});return Buffer.isBuffer(d)?d:Buffer.from(d)}catch(l){return console.warn(`[vlm-scorer] screenshot failed: ${l instanceof Error?l.message:String(l)}`),null}finally{if(c)await c.close().catch(()=>{return})}}var F=30000,L=["You are a UI quality evaluator.","Score how well the provided UI satisfies the given intent on a 0\u20131 scale.","Consider: layout clarity, visual hierarchy, contrast, component choice, content coherence.",'Reply with a single JSON object: {"score": 0.xx, "reasoning": "\u2026"}.',"Score must be a number in [0, 1]. Do not output anything outside the JSON."].join(" ");function N(e){let t=e.timeoutMs??F,r=async(n,i)=>{let o=p(n,i),s=e.generateHtml?"unassessed":"model-text",c=(m)=>({status:"unavailable",method:s,subjectHash:o,reason:m});if(!n.trim())return c("empty-input");let l=new AbortController,d,u=new Promise((m)=>{d=setTimeout(()=>{l.abort(),m(c("timeout"))},t)}),g=async()=>{try{let m=e.generateHtml?await q(e,n,i,l.signal):G(n,i);if(l.signal.aborted)return c("timeout");if(m===null)return c("screenshot-unavailable");if(e.generateHtml)s="model-screenshot";let S=await e.generate({model:e.model,system:L,messages:[{role:"user",content:m}],responseFormat:{type:"json_object"}},{signal:l.signal}),T=typeof S.message.content==="string"?S.message.content:S.message.content.filter((x)=>x.type==="text").map((x)=>x.text).join(""),w=h(T);return w?{status:"scored",method:s,subjectHash:o,score:w.score}:c("invalid-score")}catch{return c(l.signal.aborted?"timeout":"provider-failed")}};try{return await Promise.race([g(),u])}finally{clearTimeout(d)}},a=async(n,i)=>{let o=await r(n,i);if(o.status==="scored")return o.score;if(e.fallback){let s=await e.fallback(n,i);if(b(s))return s}throw Error(`visual-evaluation-unavailable: ${o.reason}`)};return a.evaluate=r,a}async function q(e,t,r,a){if(!e.generateHtml)throw Error("generateHtml required for image content path");let n=await e.generateHtml(t,r);if(a.aborted)return null;let i=await f(n,e.screenshot);if(i===null)return null;let o=i.toString("base64");return[{type:"text",text:`Intent: ${r}
|
|
12
12
|
|
|
13
|
-
Evaluate the screenshot.`},{type:"image_base64",mediaType:"image/png",data:o}]}function
|
|
14
|
-
`)}var
|
|
13
|
+
Evaluate the screenshot.`},{type:"image_base64",mediaType:"image/png",data:o}]}function G(e,t){return[`Intent: ${t}`,"","Skill definition (SKILL.md body) that produces the UI:","---",e,"---","","Rate this skill definition's UI quality 0\u20131 for the given intent."].join(`
|
|
14
|
+
`)}var U=["You turn a UI skill description and a user intent into one self-contained HTML fragment.","Use Tailwind utility classes only, no external assets, no scripts.","Output only the HTML, with no explanation and no code fences."].join(" "),W=/^```(?:html)?\s*\n?([\s\S]*?)\n?```\s*$/i;function Q(e){return async(t,r)=>{let n=(await e.generate({model:e.model,system:U,messages:[{role:"user",content:`Intent:
|
|
15
15
|
${r}
|
|
16
16
|
|
|
17
17
|
Skill:
|
|
18
|
-
${t}`}]})).message.content,o=(typeof n==="string"?n:n.filter((
|
|
19
|
-
`)}async function
|
|
20
|
-
`)},{type:"image_base64",mediaType:"image/png",data:
|
|
18
|
+
${t}`}]})).message.content,o=(typeof n==="string"?n:n.filter((c)=>c.type==="text").map((c)=>c.text).join("")).trim(),s=(W.exec(o)?.[1]??o).trim();if(!s)throw Error("model-html-generator: empty reply");return s}}var K=30000,Y=["You are a UI visual critique evaluator for UIBot handoff drafts.","Score whether the generated UIBotSpec draft preserves the prototype/demo intent.","Consider visual hierarchy, density, contrast, layout clarity, component choice, and interaction affordance.",'Reply with one JSON object: {"score": 0.xx, "reasoning": "short reason"}.',"Score must be a number in [0, 1]. Do not output anything outside JSON."].join(" ");function J(e){return typeof e.message.content==="string"?e.message.content:e.message.content.filter((t)=>t.type==="text").map((t)=>t.text).join("")}function z(e,t,r,a){return new Promise((n,i)=>{let o=setTimeout(()=>{a.abort(),i(Error(`${r} timeout after ${t}ms`))},t);e.then((s)=>{clearTimeout(o),n(s)},(s)=>{clearTimeout(o),i(s)})})}function C(e){if(e.kind==="prototype")return`prototype:${e.prototypeId} title:${e.prototype.title}`;return`${e.kind}:${e.title??"(untitled)"}${e.designMdId?` designMd:${e.designMdId}`:""}`}function X(e){return[`Source: ${C(e.source)}`,"","Prototype/demo intent:",e.intent,"","Generated UIBotSpec YAML draft:","```yaml",e.draftYaml,"```","","Return a JSON score for how well the draft preserves the intended visual result."].join(`
|
|
19
|
+
`)}async function Z(e,t,r,a){let n=await t(e);r.throwIfAborted();let i=await f(n,a);if(r.throwIfAborted(),i===null)return null;return[{type:"text",text:[`Source: ${C(e.source)}`,"","Prototype/demo intent:",e.intent,"","Generated UIBotSpec YAML draft:",e.draftYaml,"","Evaluate the screenshot and YAML together."].join(`
|
|
20
|
+
`)},{type:"image_base64",mediaType:"image/png",data:i.toString("base64")}]}function ee(e){let t=e.timeoutMs??K;return async function(a){let n=new AbortController,i=p(a.draftYaml,a.intent),o=e.renderHtml?"unassessed":"model-text",s=(l)=>({status:"unavailable",reason:l,evidence:{status:"unavailable",method:o,subjectHash:i,reason:l}});if(!a.draftYaml.trim())return s("empty-input");let c=async()=>{let l=e.renderHtml?await Z(a,e.renderHtml,n.signal,e.screenshot):X(a);if(l===null)return s("screenshot-unavailable");if(e.renderHtml)o="model-screenshot";let d=await e.generate({model:e.model,system:Y,messages:[{role:"user",content:l}],responseFormat:{type:"json_object"}},{signal:n.signal}),u=h(J(d));if(!u)return s("invalid-score");return{status:"scored",score:u.score,evidence:{status:"scored",method:o,subjectHash:i,score:u.score},...u.reasoning?{reasoning:u.reasoning}:{}}};try{return await z(c(),t,"handoff visual critique",n)}catch{return s(n.signal.aborted?"timeout":"provider-failed")}}}function te(e){return(t)=>t.source.kind==="prototype"?e.text(t):e.screenshot(t)}var v=`<script data-uibot-storage-shim>(function(){
|
|
21
|
+
function makeStore(){
|
|
22
|
+
var data = {};
|
|
23
|
+
var api = {
|
|
24
|
+
getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
|
|
25
|
+
setItem: function(k, v){ data[k] = String(v); },
|
|
26
|
+
removeItem: function(k){ delete data[k]; },
|
|
27
|
+
clear: function(){ data = {}; },
|
|
28
|
+
key: function(i){ return Object.keys(data)[i] || null; }
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
|
|
31
|
+
return api;
|
|
32
|
+
}
|
|
33
|
+
function tryShim(name){
|
|
34
|
+
var works = false;
|
|
35
|
+
try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
|
|
36
|
+
catch (_) { works = false; }
|
|
37
|
+
if (works) return;
|
|
38
|
+
try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
|
|
39
|
+
catch (_) { try { window[name] = makeStore(); } catch (__) {} }
|
|
40
|
+
}
|
|
41
|
+
tryShim('localStorage');
|
|
42
|
+
tryShim('sessionStorage');
|
|
43
|
+
})();</script>`,k=`<script data-uibot-link-shim>(function(){
|
|
44
|
+
document.addEventListener('click', function(e){
|
|
45
|
+
if (!e.target || !(e.target instanceof Element)) return;
|
|
46
|
+
var link = e.target.closest('a[href]');
|
|
47
|
+
if (!link) return;
|
|
48
|
+
var href = link.getAttribute('href');
|
|
49
|
+
if (href === null) return;
|
|
50
|
+
if (href === '' || href.charAt(0) === '#') {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
if (href === '' || href === '#') { window.scrollTo({ top: 0 }); return; }
|
|
53
|
+
var t = document.getElementById(href.slice(1));
|
|
54
|
+
if (t) t.scrollIntoView();
|
|
55
|
+
} else if (link.getAttribute('target') === '_blank') {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
try {
|
|
58
|
+
var u = new URL(href, location.href);
|
|
59
|
+
if (u.protocol === 'http:' || u.protocol === 'https:' || u.protocol === 'mailto:') {
|
|
60
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
61
|
+
}
|
|
62
|
+
} catch (_) {}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
})();</script>`,I=v+k,_="uibot:preview-error";function y(){return`<script data-uibot-error-bridge>(function(){
|
|
66
|
+
function show(msg){
|
|
67
|
+
try {
|
|
68
|
+
var bar = document.getElementById('__uibot_err__');
|
|
69
|
+
if (!bar) {
|
|
70
|
+
bar = document.createElement('div');
|
|
71
|
+
bar.id = '__uibot_err__';
|
|
72
|
+
bar.style.cssText = 'position:fixed;left:0;right:0;top:0;z-index:2147483647;max-height:50vh;overflow:auto;'
|
|
73
|
+
+ 'background:#7f1d1d;color:#fee2e2;font:12px/1.5 ui-monospace,Menlo,monospace;padding:10px 14px;'
|
|
74
|
+
+ 'white-space:pre-wrap;box-shadow:0 2px 8px rgba(0,0,0,.3)';
|
|
75
|
+
(document.body || document.documentElement).appendChild(bar);
|
|
76
|
+
}
|
|
77
|
+
bar.textContent = '\u9884\u89C8\u8FD0\u884C\u65F6\u9519\u8BEF:\\n' + msg;
|
|
78
|
+
} catch (_) {}
|
|
79
|
+
try { if (window.parent && window.parent !== window) window.parent.postMessage({ type: ${JSON.stringify(_)}, message: String(msg) }, '*'); } catch (_) {}
|
|
80
|
+
}
|
|
81
|
+
window.addEventListener('error', function(ev){
|
|
82
|
+
var m = ev && ev.error && ev.error.stack ? ev.error.stack : (ev && ev.message) || 'unknown error';
|
|
83
|
+
show(m);
|
|
84
|
+
});
|
|
85
|
+
window.addEventListener('unhandledrejection', function(ev){
|
|
86
|
+
var r = ev && ev.reason;
|
|
87
|
+
show(r && r.stack ? r.stack : String(r));
|
|
88
|
+
});
|
|
89
|
+
})();</script>`}function se(e){let t=`${v}${y()}`,r=/<head\b[^>]*>/i.exec(e);return r?e.replace(r[0],(a)=>`${a}${t}`):`${t}${e}`}function ae(e){return async({html:t,signal:r})=>{let a;try{a=await import("puppeteer-core")}catch{throw Error("puppeteer-core is not installed")}let n=await a.launch({headless:!0,executablePath:e.chromePath,args:["--no-sandbox","--disable-setuid-sandbox"]}),i=()=>void n.close().catch(()=>{return});r?.addEventListener("abort",i,{once:!0});let o;try{let s=async()=>{let c=await n.newPage();await c.setViewport({width:e.width??1280,height:e.height??800,deviceScaleFactor:1}),await c.setRequestInterception(!0),c.on("request",(u)=>{let g=u.url();/^(data|blob|about):/i.test(g)?u.continue():u.abort()});let l=[];if(c.on("pageerror",(u)=>l.push(u.message)),c.on("console",(u)=>{if(u.type()==="error"&&/^Uncaught/.test(u.text()))l.push(u.text())}),await c.setContent(se(t),{waitUntil:"load",timeout:e.timeoutMs??15000}),await new Promise((u)=>setTimeout(u,e.settleMs??800)),l.length)return{ok:!1,detail:`script error: ${l[0]}`};return await c.evaluate(()=>{let u=globalThis.document.body;if(!u)return!1;if(u.innerText.trim().length>0)return!0;return Array.from(u.querySelectorAll("img,svg,canvas,video,input,button,select,textarea")).some((g)=>{let m=g.getBoundingClientRect();return m.width>0&&m.height>0})})?{ok:!0}:{ok:!1,detail:"the page renders nothing visible"}};return await Promise.race([s(),new Promise((c,l)=>{o=setTimeout(()=>l(Error("self-check timed out")),(e.timeoutMs??15000)+5000)})])}finally{if(o)clearTimeout(o);r?.removeEventListener("abort",i),i()}}}export{ee as createHandoffVisualCritiqueProvider,ae as createHeadlessHtmlSelfCheck,Q as createModelHtmlGenerator,te as createRoutedHandoffCritiqueProvider,N as createVlmVisualScoreProvider,f as renderSpecToPng};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uibot/vlm-scorer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@uibot/contracts": "^0.
|
|
31
|
-
"@uibot/evaluation": "^0.
|
|
32
|
-
"@uibot/compiler": "^0.
|
|
33
|
-
"@uibot/renderer-react": "^0.
|
|
34
|
-
"react-dom": "19.2.1"
|
|
30
|
+
"@uibot/contracts": "^0.14.0",
|
|
31
|
+
"@uibot/evaluation": "^0.14.0",
|
|
32
|
+
"@uibot/compiler": "^0.14.0",
|
|
33
|
+
"@uibot/renderer-react": "^0.14.0",
|
|
34
|
+
"react-dom": "19.2.1",
|
|
35
|
+
"@uibot/web-preview": "^0.14.0"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"puppeteer-core": "^23
|
|
38
|
+
"puppeteer-core": "^23"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"puppeteer-core": {
|
|
@@ -45,5 +46,8 @@
|
|
|
45
46
|
"build": "rm -rf dist && bun build --minify ./src/index.ts --outdir ./dist --format esm --target bun && bun x tsc -p tsconfig.json",
|
|
46
47
|
"typecheck": "bun x tsc -p tsconfig.json --noEmit",
|
|
47
48
|
"test": "bun test"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"puppeteer-core": "^23.11.1"
|
|
48
52
|
}
|
|
49
53
|
}
|