@qontinui/ui-bridge-native 0.6.2 → 0.6.4
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/index.js +268 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +268 -0
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +263 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +263 -0
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +268 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +268 -0
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/page-health.d.ts +72 -0
- package/dist/server/page-health.d.ts.map +1 -0
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1826,6 +1826,245 @@ function createDesignHandlers(registry) {
|
|
|
1826
1826
|
};
|
|
1827
1827
|
}
|
|
1828
1828
|
|
|
1829
|
+
// src/server/page-health.ts
|
|
1830
|
+
var GRID_SIZE = 20;
|
|
1831
|
+
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1832
|
+
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1833
|
+
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
1834
|
+
"button",
|
|
1835
|
+
"input",
|
|
1836
|
+
"switch",
|
|
1837
|
+
"checkbox",
|
|
1838
|
+
"radio",
|
|
1839
|
+
"touchable",
|
|
1840
|
+
"listItem"
|
|
1841
|
+
]);
|
|
1842
|
+
var ERROR_PHRASES = [
|
|
1843
|
+
"error occurred",
|
|
1844
|
+
"failed to",
|
|
1845
|
+
"exception",
|
|
1846
|
+
"crash",
|
|
1847
|
+
"unavailable",
|
|
1848
|
+
"something went wrong",
|
|
1849
|
+
"could not"
|
|
1850
|
+
];
|
|
1851
|
+
var LOADING_PHRASES = [
|
|
1852
|
+
"loading",
|
|
1853
|
+
"starting",
|
|
1854
|
+
"connecting",
|
|
1855
|
+
"please wait",
|
|
1856
|
+
"initializing",
|
|
1857
|
+
"fetching"
|
|
1858
|
+
];
|
|
1859
|
+
var EMPTY_PHRASES = [
|
|
1860
|
+
"no data",
|
|
1861
|
+
"no results",
|
|
1862
|
+
"nothing here",
|
|
1863
|
+
"empty",
|
|
1864
|
+
"no items",
|
|
1865
|
+
"get started"
|
|
1866
|
+
];
|
|
1867
|
+
function severityRank(s) {
|
|
1868
|
+
if (s === "CRITICAL") return 3;
|
|
1869
|
+
if (s === "WARNING") return 2;
|
|
1870
|
+
return 1;
|
|
1871
|
+
}
|
|
1872
|
+
function worstSeverity(findings) {
|
|
1873
|
+
let worst = "OK";
|
|
1874
|
+
for (const f of findings) {
|
|
1875
|
+
if (severityRank(f.severity) > severityRank(worst)) {
|
|
1876
|
+
worst = f.severity;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return worst;
|
|
1880
|
+
}
|
|
1881
|
+
function truncate(s, n) {
|
|
1882
|
+
if (s.length <= n) return s;
|
|
1883
|
+
return [...s].slice(0, n).join("");
|
|
1884
|
+
}
|
|
1885
|
+
function normalizeRect(state, viewport) {
|
|
1886
|
+
const layout = state.layout;
|
|
1887
|
+
if (!layout) return null;
|
|
1888
|
+
if (viewport.width <= 0 || viewport.height <= 0) return null;
|
|
1889
|
+
const px = layout.pageX ?? layout.x;
|
|
1890
|
+
const py = layout.pageY ?? layout.y;
|
|
1891
|
+
return {
|
|
1892
|
+
x: px / viewport.width,
|
|
1893
|
+
y: py / viewport.height,
|
|
1894
|
+
width: layout.width / viewport.width,
|
|
1895
|
+
height: layout.height / viewport.height
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
function diagnosePageHealth(elements, viewport) {
|
|
1899
|
+
const findings = [];
|
|
1900
|
+
const projected = elements.map((el) => ({
|
|
1901
|
+
el,
|
|
1902
|
+
rect: el.state.visible ? normalizeRect(el.state, viewport) : null
|
|
1903
|
+
}));
|
|
1904
|
+
const visible = projected.filter((p) => p.rect !== null);
|
|
1905
|
+
const grid = Array.from(
|
|
1906
|
+
{ length: GRID_SIZE },
|
|
1907
|
+
() => Array.from({ length: GRID_SIZE }, () => false)
|
|
1908
|
+
);
|
|
1909
|
+
for (const { rect } of visible) {
|
|
1910
|
+
const colStart = Math.max(0, Math.floor(rect.x * GRID_SIZE));
|
|
1911
|
+
const colEnd = Math.min(GRID_SIZE, Math.ceil((rect.x + rect.width) * GRID_SIZE));
|
|
1912
|
+
const rowStart = Math.max(0, Math.floor(rect.y * GRID_SIZE));
|
|
1913
|
+
const rowEnd = Math.min(GRID_SIZE, Math.ceil((rect.y + rect.height) * GRID_SIZE));
|
|
1914
|
+
for (let r = rowStart; r < rowEnd; r++) {
|
|
1915
|
+
for (let c = colStart; c < colEnd; c++) {
|
|
1916
|
+
grid[r][c] = true;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
const totalCells = GRID_SIZE * GRID_SIZE;
|
|
1921
|
+
const halfCells = totalCells / 2;
|
|
1922
|
+
let filled = 0;
|
|
1923
|
+
let leftFilled = 0;
|
|
1924
|
+
let rightFilled = 0;
|
|
1925
|
+
const half = GRID_SIZE / 2;
|
|
1926
|
+
for (let r = 0; r < GRID_SIZE; r++) {
|
|
1927
|
+
for (let c = 0; c < GRID_SIZE; c++) {
|
|
1928
|
+
if (grid[r][c]) {
|
|
1929
|
+
filled++;
|
|
1930
|
+
if (c < half) leftFilled++;
|
|
1931
|
+
else rightFilled++;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const coveragePct = Math.round(filled / totalCells * 100);
|
|
1936
|
+
const leftHalfPct = Math.round(leftFilled / halfCells * 100);
|
|
1937
|
+
const rightHalfPct = Math.round(rightFilled / halfCells * 100);
|
|
1938
|
+
let spatialSeverity = "OK";
|
|
1939
|
+
if (coveragePct < 15) spatialSeverity = "CRITICAL";
|
|
1940
|
+
else if (coveragePct < 30) spatialSeverity = "WARNING";
|
|
1941
|
+
if (rightHalfPct < 5 && leftHalfPct > 20) spatialSeverity = "CRITICAL";
|
|
1942
|
+
findings.push({
|
|
1943
|
+
check: "spatial_coverage",
|
|
1944
|
+
severity: spatialSeverity,
|
|
1945
|
+
detail: `Elements cover ${coveragePct}% of viewport. Left=${leftHalfPct}%, Right=${rightHalfPct}%`,
|
|
1946
|
+
data: {
|
|
1947
|
+
coverage_pct: coveragePct,
|
|
1948
|
+
left_half_pct: leftHalfPct,
|
|
1949
|
+
right_half_pct: rightHalfPct
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
let sidebar = 0;
|
|
1953
|
+
let header = 0;
|
|
1954
|
+
let content = 0;
|
|
1955
|
+
for (const { rect } of visible) {
|
|
1956
|
+
const cx = rect.x + rect.width / 2;
|
|
1957
|
+
const cy = rect.y + rect.height / 2;
|
|
1958
|
+
if (cx < 0.2) sidebar++;
|
|
1959
|
+
else if (cy < 0.08) header++;
|
|
1960
|
+
else content++;
|
|
1961
|
+
}
|
|
1962
|
+
let layoutSeverity = "OK";
|
|
1963
|
+
if (content === 0) layoutSeverity = "CRITICAL";
|
|
1964
|
+
else if (content < 3) layoutSeverity = "WARNING";
|
|
1965
|
+
findings.push({
|
|
1966
|
+
check: "layout_regions",
|
|
1967
|
+
severity: layoutSeverity,
|
|
1968
|
+
detail: `sidebar=${sidebar}, header=${header}, content=${content}`,
|
|
1969
|
+
data: { sidebar, header, content }
|
|
1970
|
+
});
|
|
1971
|
+
const typeCounts = {};
|
|
1972
|
+
for (const el of elements) {
|
|
1973
|
+
const t = el.type || "unknown";
|
|
1974
|
+
typeCounts[t] = (typeCounts[t] ?? 0) + 1;
|
|
1975
|
+
}
|
|
1976
|
+
const allNav = elements.length > 5 && Object.keys(typeCounts).every((k) => NAV_TYPES.has(k));
|
|
1977
|
+
const diversitySeverity = allNav ? "WARNING" : "OK";
|
|
1978
|
+
findings.push({
|
|
1979
|
+
check: "element_diversity",
|
|
1980
|
+
severity: diversitySeverity,
|
|
1981
|
+
detail: `${Object.keys(typeCounts).length} type(s) across ${elements.length} elements${allNav ? " (navigation-only)" : ""}`,
|
|
1982
|
+
data: { types: typeCounts }
|
|
1983
|
+
});
|
|
1984
|
+
const detectedErrors = [];
|
|
1985
|
+
const detectedLoading = [];
|
|
1986
|
+
const detectedEmpty = [];
|
|
1987
|
+
const detectedCss = [];
|
|
1988
|
+
for (const el of elements) {
|
|
1989
|
+
const elType = el.type ?? "";
|
|
1990
|
+
if (SKIP_TEXT_TYPES.has(elType)) continue;
|
|
1991
|
+
const text = (el.state.textContent ?? "").toString();
|
|
1992
|
+
if (!text) continue;
|
|
1993
|
+
const textLower = text.toLowerCase();
|
|
1994
|
+
for (const p of ERROR_PHRASES) {
|
|
1995
|
+
if (textLower.includes(p)) {
|
|
1996
|
+
detectedErrors.push(truncate(text, 120));
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
for (const p of LOADING_PHRASES) {
|
|
2001
|
+
if (textLower.includes(p)) {
|
|
2002
|
+
detectedLoading.push(truncate(text, 120));
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
for (const p of EMPTY_PHRASES) {
|
|
2007
|
+
if (textLower.includes(p)) {
|
|
2008
|
+
detectedEmpty.push(truncate(text, 120));
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
let textSeverity = "OK";
|
|
2014
|
+
if (detectedErrors.length > 0) textSeverity = "CRITICAL";
|
|
2015
|
+
else if (detectedLoading.length > 0 || detectedEmpty.length > 0) textSeverity = "WARNING";
|
|
2016
|
+
findings.push({
|
|
2017
|
+
check: "text_signals",
|
|
2018
|
+
severity: textSeverity,
|
|
2019
|
+
detail: `errors=${detectedErrors.length}, loading=${detectedLoading.length}, empty=${detectedEmpty.length}, css_signals=${detectedCss.length}`,
|
|
2020
|
+
data: {
|
|
2021
|
+
errors: detectedErrors,
|
|
2022
|
+
loading: detectedLoading,
|
|
2023
|
+
empty: detectedEmpty,
|
|
2024
|
+
css_signals: detectedCss
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
let interactiveTotal = 0;
|
|
2028
|
+
let interactiveDisabled = 0;
|
|
2029
|
+
for (const el of elements) {
|
|
2030
|
+
if (!INTERACTIVE_TYPES.has(el.type)) continue;
|
|
2031
|
+
interactiveTotal++;
|
|
2032
|
+
if (el.state.enabled === false) interactiveDisabled++;
|
|
2033
|
+
}
|
|
2034
|
+
const interactiveSeverity = interactiveTotal > 0 && interactiveDisabled / interactiveTotal > 0.5 ? "WARNING" : "OK";
|
|
2035
|
+
findings.push({
|
|
2036
|
+
check: "interactive_readiness",
|
|
2037
|
+
severity: interactiveSeverity,
|
|
2038
|
+
detail: `${interactiveTotal} interactive elements, ${interactiveDisabled} disabled`,
|
|
2039
|
+
data: { total: interactiveTotal, disabled: interactiveDisabled }
|
|
2040
|
+
});
|
|
2041
|
+
let zeroSize = 0;
|
|
2042
|
+
let outsideViewport = 0;
|
|
2043
|
+
for (const { rect } of visible) {
|
|
2044
|
+
if (rect.width === 0 || rect.height === 0) zeroSize++;
|
|
2045
|
+
if (rect.x + rect.width < 0 || rect.y + rect.height < 0 || rect.x > 1 || rect.y > 1) {
|
|
2046
|
+
outsideViewport++;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
const anomalySeverity = zeroSize > 0 || outsideViewport > 0 ? "WARNING" : "OK";
|
|
2050
|
+
findings.push({
|
|
2051
|
+
check: "visual_anomalies",
|
|
2052
|
+
severity: anomalySeverity,
|
|
2053
|
+
detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport}`,
|
|
2054
|
+
data: { zero_size: zeroSize, outside_viewport: outsideViewport }
|
|
2055
|
+
});
|
|
2056
|
+
const heatmap = grid.map(
|
|
2057
|
+
(row) => row.map((cell) => cell ? "#" : ".").join("")
|
|
2058
|
+
);
|
|
2059
|
+
return {
|
|
2060
|
+
summary: worstSeverity(findings),
|
|
2061
|
+
findings,
|
|
2062
|
+
heatmap,
|
|
2063
|
+
element_count: elements.length,
|
|
2064
|
+
visible_count: visible.length
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1829
2068
|
// src/server/handlers.ts
|
|
1830
2069
|
async function executeWorkflowStep(step, registry, executor) {
|
|
1831
2070
|
const startTime = Date.now();
|
|
@@ -2005,6 +2244,15 @@ async function executeWorkflowStep(step, registry, executor) {
|
|
|
2005
2244
|
};
|
|
2006
2245
|
}
|
|
2007
2246
|
}
|
|
2247
|
+
function getWindowDimensions() {
|
|
2248
|
+
try {
|
|
2249
|
+
const { Dimensions: Dimensions2 } = __require("react-native");
|
|
2250
|
+
const win = Dimensions2.get("window");
|
|
2251
|
+
return { width: win.width, height: win.height };
|
|
2252
|
+
} catch {
|
|
2253
|
+
return { width: 0, height: 0 };
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2008
2256
|
function success2(data) {
|
|
2009
2257
|
return {
|
|
2010
2258
|
success: true,
|
|
@@ -2210,6 +2458,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2210
2458
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
2211
2459
|
return success2(snapshot);
|
|
2212
2460
|
},
|
|
2461
|
+
getPageHealth: async (ctx) => {
|
|
2462
|
+
const bodyViewport = ctx.body?.viewport;
|
|
2463
|
+
const viewport = bodyViewport ?? getWindowDimensions();
|
|
2464
|
+
const elements = registry.getAllElements().map((e) => ({
|
|
2465
|
+
type: e.type,
|
|
2466
|
+
state: e.getState()
|
|
2467
|
+
}));
|
|
2468
|
+
const report = diagnosePageHealth(elements, viewport);
|
|
2469
|
+
return success2(report);
|
|
2470
|
+
},
|
|
2213
2471
|
// Workflows
|
|
2214
2472
|
getWorkflows: async () => {
|
|
2215
2473
|
const workflows = registry.getAllWorkflows().map((w) => ({
|
|
@@ -2958,6 +3216,10 @@ var WS_ROUTES = [
|
|
|
2958
3216
|
{ pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
|
|
2959
3217
|
{ pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
|
|
2960
3218
|
{ pattern: "control/discover", handler: "getSnapshot" },
|
|
3219
|
+
// Page health — accepts both GET and POST so the canonical skill invocation
|
|
3220
|
+
// (`POST .../control/page-health` with an empty body) and ad-hoc curls both
|
|
3221
|
+
// work without a body-shape gotcha.
|
|
3222
|
+
{ pattern: "control/page-health", handler: "getPageHealth", httpMethods: ["GET", "POST"] },
|
|
2961
3223
|
// Workflows
|
|
2962
3224
|
{ pattern: "control/workflows", handler: "getWorkflows", httpMethods: ["GET"] },
|
|
2963
3225
|
{ pattern: "control/workflow/:id/run", handler: "runWorkflow", httpMethods: ["POST"] },
|
|
@@ -3062,6 +3324,7 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
3062
3324
|
executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
|
|
3063
3325
|
find: "AI-powered element search by natural-language query",
|
|
3064
3326
|
getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
|
|
3327
|
+
getPageHealth: 'Holistic page health diagnostic over current snapshot elements (spatial coverage, layout regions, text/anomaly signals, heatmap). Optional body: { viewport?: { width, height } } to override Dimensions.get("window").',
|
|
3065
3328
|
getWorkflows: "List registered workflows",
|
|
3066
3329
|
runWorkflow: "Trigger a workflow by id",
|
|
3067
3330
|
pageRefresh: "Reload the current route",
|
|
@@ -5795,6 +6058,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
5795
6058
|
path: "/ui-bridge/control/snapshot",
|
|
5796
6059
|
description: "Get full bridge snapshot"
|
|
5797
6060
|
},
|
|
6061
|
+
PAGE_HEALTH: {
|
|
6062
|
+
method: "POST",
|
|
6063
|
+
path: "/ui-bridge/control/page-health",
|
|
6064
|
+
description: "Holistic page health diagnostic: spatial coverage, layout regions, text signals, interactive readiness, anomalies + ASCII heatmap"
|
|
6065
|
+
},
|
|
5798
6066
|
// Workflows
|
|
5799
6067
|
GET_WORKFLOWS: {
|
|
5800
6068
|
method: "GET",
|