import { confidenceFromChecks, defineAppIntelligence, localizedConcepts, } from "../lib/app-intelligence.mjs"; import { buildFuserSourceAtlas } from "./fuser-source-atlas.mjs"; export const FUSER_SELECTORS = Object.freeze({ canvas: ".react-flow", pane: ".react-flow__pane", viewport: ".react-flow__viewport", settingsDialog: '[role="dialog"][aria-label="Settings"]', settingsNav: 'nav[aria-label="Settings"]', editorSettingsAnchor: '[data-neo-anchor="editor-theme-toggle"]', imageNode: ".react-flow__node-ImageNode", // The handle id is a per-node runtime UUID in current Fuser source. Scope by // node type + side + React Flow direction, then prove the node contract. imageInput: ".react-flow__node-ImageNode .react-flow__handle-left.target", // ImageNode renders an outgoing passthrough for its input socket. The model // also owns `outputs.imageOut`, but there is no `imageOut` canvas handle. imagePassthrough: ".react-flow__node-ImageNode .react-flow__handle-right.source", }); export const FUSER_SOURCE_CHECKS = Object.freeze([ { id:"image-node-contract", path:"packages/flow/src/nodes/ImageNode.tsx", contains:["type: 'ImageNode'", "socketKey=\"image\"", "outputs?.imageOut?.data"], }, { id:"image-data-contract", path:"packages/flow/src/nodes/ImageNode.tool.ts", contains:["inputs:", "image:", "outputs:", "imageOut:"], }, { id:"visible-passthrough-contract", path:"packages/flow/src/components/NodeSection.tsx", contains:["const isPassthrough = type === 'source' && socket.type === 'target'", " [node.type, node])); const nodes = (dom.nodes || []).map((node) => { const sourceNode = catalog.get(node.type); return { ...node, ...(sourceNode ? { catalog:{ name:sourceNode.name, category:sourceNode.category, description:sourceNode.description, billable:sourceNode.billable, source:sourceNode.source, inputs:sourceNode.inputs, outputs:sourceNode.outputs, } } : {}), ports:(node.handles || []).map((handle) => semanticPort(handle, node.type, labels)), handles:undefined, }; }); const imageNodes = nodes.filter((node) => node.type === "ImageNode"); const imagePorts = imageNodes.flatMap((node) => node.ports); const checks = { fuserHost:Boolean(dom.url?.includes(FUSER_INTELLIGENCE.hostMatch)), editorSurfacePresent:Boolean(dom.canvas?.present || dom.settings?.present), knownLiveNodeTypes:atlas ? nodes.every((node) => catalog.has(node.type)) : true, ...(imageNodes.length ? { onePrimitiveImageNode:imageNodes.length === 1, imageInputPresent:imagePorts.some((port) => port.role === "input" && port.key === "image"), imagePassthroughPresent:imagePorts.some((port) => port.role === "input-passthrough" && port.key === "image"), visibleSurfaceDistinctFromModelOutput:imagePorts .some((port) => port.role === "input-passthrough" && port.key === "image" && port.modelOutput?.key === "imageOut"), } : {}), }; const issues = Object.entries(checks).filter(([, pass]) => !pass).map(([name]) => name); return { schema:"captutor-app-frame/v1", capturedAt:dom.capturedAt, app:{ id:FUSER_INTELLIGENCE.id, locale, url:dom.url, title:dom.title }, source:FUSER_INTELLIGENCE.source, confidence:{ score:confidenceFromChecks(checks), checks, issues }, viewport:dom.viewport, editor:{ canvas:dom.canvas, selection:{ nodeIds:nodes.filter((node) => node.selected).map((node) => node.id) }, nodes, edgeCount:dom.edgeCount, }, settings:dom.settings?.present ? { ...dom.settings, catalog:atlas?.settings || null, } : { present:false }, atlas:atlas ? { schema:atlas.schema, coverage:atlas.coverage, editorBehaviors:atlas.editorBehaviors } : null, intent:{ surface:"A node-based workflow editor where users place, inspect, connect, and move typed units on an infinite canvas.", applicableBehaviors:Object.keys(FUSER_INTELLIGENCE.behaviors), }, genericFrameSchema:genericFrame?.schema || null, }; } export async function readFuserFrame(cdp, { locale, t }) { let atlas = null; try { atlas = buildFuserSourceAtlas(process.env.FUSER_SOURCE_ROOT); } catch {} const [genericFrame, dom] = await Promise.all([ cdp.frame(), cdp.eval(`(() => { const clean = (value, limit = 160) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, limit); const rect = (element) => { if (!element) return null; const r = element.getBoundingClientRect(); return { x:Math.round(r.left), y:Math.round(r.top), width:Math.round(r.width), height:Math.round(r.height), cx:Math.round(r.left+r.width/2), cy:Math.round(r.top+r.height/2) }; }; const handle = (element) => { const style = getComputedStyle(element); const zone = element.parentElement; return { id:element.getAttribute('data-handleid') || '', kind:element.classList.contains('target') ? 'target' : element.classList.contains('source') ? 'source' : '', side:['left','right','top','bottom'].find(side => element.classList.contains(side) || element.classList.contains('react-flow__handle-' + side)) || '', ariaLabel:clean(element.getAttribute('aria-label')), connected:element.classList.contains('connecting') || element.classList.contains('valid'), rect:rect(element), interactionRect:rect(zone), state:{ opacity:style.opacity, transform:style.transform, cursor:style.cursor, pointerEvents:style.pointerEvents }, }; }; const nodes = [...document.querySelectorAll('.react-flow__node')].map(node => ({ id:node.getAttribute('data-id') || node.id || '', type:[...node.classList].find(name => name.startsWith('react-flow__node-'))?.replace('react-flow__node-', '') || '', selected:node.classList.contains('selected'), rect:rect(node), text:clean(node.innerText || node.textContent, 220), handles:[...node.querySelectorAll('.react-flow__handle')].map(handle), })); const canvas=document.querySelector('.react-flow'); const viewport=document.querySelector('.react-flow__viewport'); const settingsDialog=document.querySelector('[role="dialog"][aria-label="Settings"]'); const settingsNav=settingsDialog?.querySelector('nav[aria-label="Settings"]'); const activeSetting=settingsNav?.querySelector('[aria-current="true"]'); const settingControls=settingsDialog ? [...settingsDialog.querySelectorAll('button,[role="switch"],[role="combobox"]')] .map(element => ({ role:element.getAttribute('role') || element.tagName.toLowerCase(), label:clean(element.getAttribute('aria-label') || element.innerText || element.textContent, 100), selected:element.getAttribute('aria-selected') || element.getAttribute('aria-checked') || null, rect:rect(element), })).filter(control => control.label).slice(0, 80) : []; return { capturedAt:new Date().toISOString(), url:location.href, title:document.title, viewport:{ width:innerWidth, height:innerHeight, dpr:devicePixelRatio }, canvas:{ present:Boolean(canvas), rect:rect(canvas), transform:getComputedStyle(viewport || document.documentElement).transform, zoomLabel:[...document.querySelectorAll('button')].map(button => (button.innerText || '').trim()) .find(text => /^\\d+\\s*%$/.test(text)) || '' }, nodes, edgeCount:document.querySelectorAll('.react-flow__edge').length, settings:{ present:Boolean(settingsDialog), rect:rect(settingsDialog), active:clean(activeSetting?.innerText || activeSetting?.textContent), controls:settingControls }, }; })()`), ]); return interpretFuserFrame(dom, genericFrame, { locale, t, atlas }); }