@ggui-ai/ui-gen 0.16.0 → 0.18.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/adapters/generation-dispatch.d.ts +6 -3
- package/dist/adapters/generation-dispatch.js +5161 -4312
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +8 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +12 -8
- package/dist/advanced/index.js +4685 -3801
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +8 -12
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +5 -2
- package/dist/boilerplate.js +120 -13
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +2 -2
- package/dist/check/index.js +72 -0
- package/dist/check/index.js.map +1 -1
- package/dist/compose.js +1 -1
- package/dist/compose.js.map +1 -1
- package/dist/{create-ui-generator-CCIeQaEe.d.ts → create-ui-generator-C8Ki9Wz1.d.ts} +18 -2
- package/dist/{design-mode-BHEE7flj.d.ts → design-mode-DLufhrNp.d.ts} +0 -6
- package/dist/{dispatch-BA5dJcg-.d.ts → dispatch-D2htzi9h.d.ts} +2 -2
- package/dist/evaluation/axis-checks/index.d.ts +4 -4
- package/dist/evaluation/axis-checks/index.js +347 -13
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -2
- package/dist/evaluation/axis-checks/registry.js +302 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +160 -8
- package/dist/evaluation/index.js +752 -81
- package/dist/evaluation/index.js.map +1 -1
- package/dist/fragments/index.js +1 -1
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-8L_bc2iP.d.ts → generate-9IeNT4ZY.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +3 -2
- package/dist/harness/check/runtime-render/index.js +8 -12
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.d.ts +3 -2
- package/dist/harness/index.js +410 -255
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +7 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +3 -3
- package/dist/harness/runtime.d.ts +13 -4
- package/dist/harness/runtime.js +131 -20
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +13 -2
- package/dist/harness/types-public.js +410 -255
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -2
- package/dist/index.d.ts +29 -7
- package/dist/index.js +4691 -3805
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -2
- package/dist/{prompts-k6m6reqe.d.ts → prompts-YU9piOgs.d.ts} +12 -46
- package/dist/styling-profile-CxfvV48g.d.ts +35 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-D24JLJgl.d.ts → types-public-DVSssdc-.d.ts} +39 -3
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +3 -2
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +6 -1
- package/src/boilerplate/free-design-prompt.ts +9 -0
- package/src/boilerplate/generate.ts +8 -1
- package/src/boilerplate/hard-sections.ts +45 -1
- package/src/boilerplate/styling-profile.ts +166 -0
- package/src/boilerplate/system-prompt.ts +22 -8
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
- package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
- package/src/check/run-tier0.ts +154 -0
- package/src/contract-context.ts +28 -14
- package/src/create-harness.ts +4 -0
- package/src/create-ui-generator.ts +41 -2
- package/src/design-mode.ts +11 -1
- package/src/design-system-docs.ts +7 -4
- package/src/effort.ts +48 -0
- package/src/evaluation/axis-checks/checks/grid.ts +148 -0
- package/src/evaluation/axis-checks/checks/universal.ts +308 -0
- package/src/evaluation/axis-checks/dispatch.ts +78 -13
- package/src/evaluation/axis-checks/helpers.ts +12 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/design-identity.ts +96 -0
- package/src/evaluation/index.ts +17 -0
- package/src/evaluation/llm-evaluator.ts +13 -1
- package/src/evaluation/theme-css.ts +14 -0
- package/src/evaluation/types-public.ts +35 -1
- package/src/evaluation/visual-evaluator.ts +478 -45
- package/src/fragments/render.ts +1 -1
- package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
- package/src/harness/coding/init-session.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +6 -14
- package/src/harness/coding/run-eval-round.ts +31 -3
- package/src/harness/runtime.ts +10 -0
- package/src/harness/types-public.ts +11 -0
- package/src/index.ts +2 -0
- package/src/pin-fixtures.ts +98 -0
- package/src/run-check.ts +10 -10
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools.ts +2 -1
- package/src/validation/primitives.ts +1 -1
package/src/check/run-tier0.ts
CHANGED
|
@@ -352,6 +352,137 @@ function isGadgetExportImported(
|
|
|
352
352
|
return false;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* AST-precise detector for the compiles-but-renders-nothing class: the
|
|
357
|
+
* default-exported component's body never returns a tree.
|
|
358
|
+
*
|
|
359
|
+
* The boilerplate HANDS the model the wrapper — `base.tsx.tmpl` and
|
|
360
|
+
* `base-free.tsx.tmpl` both emit `return (` … `);` around `{{LAYOUT}}`.
|
|
361
|
+
* A model rewriting the root block can delete those two lines with it,
|
|
362
|
+
* leaving the JSX as a bare expression statement. That is legal TSX: it
|
|
363
|
+
* parses, esbuild compiles it, `typecheck()` passes it, and the
|
|
364
|
+
* component renders a blank frame. Seen once in twenty takes of the
|
|
365
|
+
* ggui#1113 A/B (`xs-chat-card`, run 2 — the judge spent a screenshot
|
|
366
|
+
* and an LLM call to report "completely blank").
|
|
367
|
+
*
|
|
368
|
+
* `compile/default-export` already guards the scaffold's
|
|
369
|
+
* `export default function` line and `types/props-interface` its
|
|
370
|
+
* `interface Props` line. The `return (` was the third scaffold line
|
|
371
|
+
* with nothing watching it.
|
|
372
|
+
*
|
|
373
|
+
* Conservative by construction — it reports ONLY when it positively
|
|
374
|
+
* identified a block-bodied default-export component:
|
|
375
|
+
* - a concise arrow body (`export default () => <X/>`) returns by
|
|
376
|
+
* construction → silent
|
|
377
|
+
* - `export default memo(Component)` or any other expression it
|
|
378
|
+
* cannot resolve to a function → silent
|
|
379
|
+
* - a `return` inside a nested helper is not the component's own (the
|
|
380
|
+
* walk stops at every nested function boundary), but ONE own-level
|
|
381
|
+
* `return <value>` anywhere — an early `return null` included —
|
|
382
|
+
* satisfies it.
|
|
383
|
+
*
|
|
384
|
+
* Returns `null` when the component returns (or could not be
|
|
385
|
+
* identified), else the 1-based line of the first bare JSX expression
|
|
386
|
+
* statement in its body — the line the fix belongs on — or `undefined`
|
|
387
|
+
* when the body has no JSX statement to point at either.
|
|
388
|
+
*/
|
|
389
|
+
function detectComponentNeverReturns(
|
|
390
|
+
sourceCode: string,
|
|
391
|
+
): { jsxStatementLine: number | undefined } | null {
|
|
392
|
+
const sf = ts.createSourceFile(
|
|
393
|
+
'source.tsx',
|
|
394
|
+
sourceCode,
|
|
395
|
+
ts.ScriptTarget.Latest,
|
|
396
|
+
/* setParentNodes */ true,
|
|
397
|
+
ts.ScriptKind.TSX,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const isFunctionLike = (n: ts.Node): boolean =>
|
|
401
|
+
ts.isFunctionDeclaration(n) ||
|
|
402
|
+
ts.isFunctionExpression(n) ||
|
|
403
|
+
ts.isArrowFunction(n) ||
|
|
404
|
+
ts.isMethodDeclaration(n) ||
|
|
405
|
+
ts.isClassDeclaration(n) ||
|
|
406
|
+
ts.isClassExpression(n);
|
|
407
|
+
|
|
408
|
+
/** The top-level declaration `name` binds to, when it is a function. */
|
|
409
|
+
function resolveTopLevel(
|
|
410
|
+
name: string,
|
|
411
|
+
): ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | undefined {
|
|
412
|
+
for (const st of sf.statements) {
|
|
413
|
+
if (ts.isFunctionDeclaration(st) && st.name?.text === name) return st;
|
|
414
|
+
if (ts.isVariableStatement(st)) {
|
|
415
|
+
for (const decl of st.declarationList.declarations) {
|
|
416
|
+
if (
|
|
417
|
+
ts.isIdentifier(decl.name) &&
|
|
418
|
+
decl.name.text === name &&
|
|
419
|
+
decl.initializer !== undefined &&
|
|
420
|
+
(ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))
|
|
421
|
+
) {
|
|
422
|
+
return decl.initializer;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let component:
|
|
431
|
+
| ts.FunctionDeclaration
|
|
432
|
+
| ts.FunctionExpression
|
|
433
|
+
| ts.ArrowFunction
|
|
434
|
+
| undefined;
|
|
435
|
+
for (const st of sf.statements) {
|
|
436
|
+
if (
|
|
437
|
+
ts.isFunctionDeclaration(st) &&
|
|
438
|
+
st.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
|
|
439
|
+
) {
|
|
440
|
+
component = st;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
if (ts.isExportAssignment(st) && st.isExportEquals !== true) {
|
|
444
|
+
const expr = st.expression;
|
|
445
|
+
if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) component = expr;
|
|
446
|
+
else if (ts.isIdentifier(expr)) component = resolveTopLevel(expr.text);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// No default export at all, or one this detector cannot resolve to a
|
|
451
|
+
// function — `compile/default-export` owns the first case and the
|
|
452
|
+
// second is not ours to guess at.
|
|
453
|
+
if (component === undefined) return null;
|
|
454
|
+
|
|
455
|
+
const body = component.body;
|
|
456
|
+
// A concise arrow body IS the returned value.
|
|
457
|
+
if (body === undefined || !ts.isBlock(body)) return null;
|
|
458
|
+
|
|
459
|
+
let returnsValue = false;
|
|
460
|
+
let jsxStatementLine: number | undefined;
|
|
461
|
+
const visit = (node: ts.Node): void => {
|
|
462
|
+
if (returnsValue) return;
|
|
463
|
+
if (ts.isReturnStatement(node)) {
|
|
464
|
+
if (node.expression !== undefined) returnsValue = true;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
jsxStatementLine === undefined &&
|
|
469
|
+
ts.isExpressionStatement(node) &&
|
|
470
|
+
(ts.isJsxElement(node.expression) ||
|
|
471
|
+
ts.isJsxFragment(node.expression) ||
|
|
472
|
+
ts.isJsxSelfClosingElement(node.expression))
|
|
473
|
+
) {
|
|
474
|
+
jsxStatementLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
475
|
+
}
|
|
476
|
+
node.forEachChild((child) => {
|
|
477
|
+
// A helper component's own `return` is not this component's.
|
|
478
|
+
if (!isFunctionLike(child)) visit(child);
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
for (const st of body.statements) if (!isFunctionLike(st)) visit(st);
|
|
482
|
+
|
|
483
|
+
return returnsValue ? null : { jsxStatementLine };
|
|
484
|
+
}
|
|
485
|
+
|
|
355
486
|
/**
|
|
356
487
|
* Run all tier 0 (deterministic, no-LLM) checks against component source.
|
|
357
488
|
*
|
|
@@ -412,6 +543,7 @@ export async function runTier0Checks(
|
|
|
412
543
|
tier: 0,
|
|
413
544
|
result: 'fail',
|
|
414
545
|
category: 'compile',
|
|
546
|
+
subcategory: 'build-error',
|
|
415
547
|
severity: 'critical',
|
|
416
548
|
description: `Component failed to compile:\n${errorDetail}`,
|
|
417
549
|
fix: 'Fix the JSX/TypeScript syntax errors listed above',
|
|
@@ -535,6 +667,7 @@ export async function runTier0Checks(
|
|
|
535
667
|
tier: 0,
|
|
536
668
|
result: 'fail',
|
|
537
669
|
category: 'imports',
|
|
670
|
+
subcategory: 'disallowed-package',
|
|
538
671
|
severity: 'critical',
|
|
539
672
|
description: `Import from "${pkg}" is not allowed`,
|
|
540
673
|
fix: `Only import from: ${describeAllowedImports()}`,
|
|
@@ -980,6 +1113,27 @@ export async function runTier0Checks(
|
|
|
980
1113
|
});
|
|
981
1114
|
}
|
|
982
1115
|
|
|
1116
|
+
// ── Component never returns its tree ──────────────────────
|
|
1117
|
+
// The scaffold hands the model `return (` … `);` around the layout.
|
|
1118
|
+
// A root-block rewrite can delete them with it, and bare JSX at
|
|
1119
|
+
// statement position compiles, type-checks and renders NOTHING.
|
|
1120
|
+
const neverReturns = detectComponentNeverReturns(sourceCode);
|
|
1121
|
+
if (neverReturns !== null) {
|
|
1122
|
+
issues.push({
|
|
1123
|
+
tier: 0,
|
|
1124
|
+
result: 'fail',
|
|
1125
|
+
category: 'compile',
|
|
1126
|
+
subcategory: 'renders-nothing',
|
|
1127
|
+
severity: 'critical',
|
|
1128
|
+
description:
|
|
1129
|
+
neverReturns.jsxStatementLine === undefined
|
|
1130
|
+
? 'The component never returns its tree — it renders a blank frame'
|
|
1131
|
+
: `The component never returns its tree: the JSX at line ${neverReturns.jsxStatementLine} sits at statement position, so it is built and thrown away and the frame renders blank`,
|
|
1132
|
+
fix: 'Wrap the tree in the return the boilerplate gave you: `return (` before the root element and `);` after it.',
|
|
1133
|
+
...(neverReturns.jsxStatementLine === undefined ? {} : { line: neverReturns.jsxStatementLine }),
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
983
1137
|
// (Controller-View checks removed — generated components use a
|
|
984
1138
|
// single-function pattern.)
|
|
985
1139
|
|
package/src/contract-context.ts
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* sizing strategies and including the hint inline cuts a class of
|
|
27
27
|
* "designed for fullscreen, rendered in a chat bubble" bugs.
|
|
28
28
|
*/
|
|
29
|
+
import { STYLING_PROFILE_PRECEDENCE_NOTE } from './boilerplate/styling-profile.js';
|
|
30
|
+
import type { RenderingContext as ProtocolRenderingContext } from '@ggui-ai/protocol';
|
|
29
31
|
import type {
|
|
30
32
|
ActionEntry,
|
|
31
33
|
BlueprintVariance,
|
|
@@ -43,17 +45,11 @@ import {
|
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* Rendering context — how and where the component will be displayed.
|
|
46
|
-
* Affects layout strategy, sizing, and interaction patterns.
|
|
47
|
-
*
|
|
48
|
+
* Affects layout strategy, sizing, and interaction patterns. The ONE
|
|
49
|
+
* vocabulary is `@ggui-ai/protocol`'s `renderingContextSchema` (ggui#1000);
|
|
50
|
+
* this is its type, derived — never re-declared here.
|
|
48
51
|
*/
|
|
49
|
-
export
|
|
50
|
-
/** Device category — affects touch targets, column count, density. */
|
|
51
|
-
readonly device: 'mobile' | 'tablet' | 'desktop' | 'spatial';
|
|
52
|
-
/** Shell type — the container the component renders in. */
|
|
53
|
-
readonly shell: 'chat' | 'fullscreen' | 'partial';
|
|
54
|
-
/** Viewport dimensions in CSS pixels (optional). */
|
|
55
|
-
readonly viewport?: { readonly width: number; readonly height: number };
|
|
56
|
-
}
|
|
52
|
+
export type RenderingContext = ProtocolRenderingContext;
|
|
57
53
|
|
|
58
54
|
const SHELL_HINTS: Record<string, string> = {
|
|
59
55
|
chat: `**Chat Shell — Inline card in scrolling conversation**
|
|
@@ -66,9 +62,9 @@ const SHELL_HINTS: Record<string, string> = {
|
|
|
66
62
|
- Width: width: 100% (fills parent bubble). Do NOT set max-width.`,
|
|
67
63
|
|
|
68
64
|
fullscreen: `**Fullscreen Shell — Takes over entire viewport**
|
|
69
|
-
- Container:
|
|
65
|
+
- Container: the whole viewport, edge to edge — the frame stretches your root to its full height (you never set a viewport height yourself)
|
|
70
66
|
- Chrome: NONE — your component IS the entire UI. You own all visual chrome.
|
|
71
|
-
- Sizing: fill
|
|
67
|
+
- Sizing: size to content; to fill a parent that has a height use height: 100% or flex: 1. NEVER min-height: 100vh / 100dvh — the frame already stretches the root, and a viewport height only fights it (tier-0 fail).
|
|
72
68
|
- Padding: component owns ALL padding. Use var(--ggui-spacing-6) or larger for breathing room.
|
|
73
69
|
- Scrolling: component manages its own scroll if content exceeds viewport (overflow-y: auto)
|
|
74
70
|
- Layout: can use multi-column, sidebars, headers/footers. Full creative control.
|
|
@@ -206,6 +202,19 @@ export function buildContractsContext(
|
|
|
206
202
|
if (entry.nextStep) {
|
|
207
203
|
line += `\n Next-step hint: agent intends to call \`${entry.nextStep}\``;
|
|
208
204
|
}
|
|
205
|
+
// ggui#1108: `confirm` was the one declared action field the contract
|
|
206
|
+
// renderer dropped — every other author field (label, description,
|
|
207
|
+
// example, nextStep) reaches the model and this one did not. It is
|
|
208
|
+
// rendered as INFORMATION, not as a rule: the terminal-actions section
|
|
209
|
+
// deliberately does NOT cite it, because citing it measured ZERO —
|
|
210
|
+
// two wordings, n=4 each, `save` + `confirm: true` came back armed 0/4
|
|
211
|
+
// and 0/4. The field's own doc says "whether to show confirmation
|
|
212
|
+
// BEFORE triggering", which is gravity, not one-shot-ness, and the
|
|
213
|
+
// model reads it that way. Emitted only when true, so an absent flag
|
|
214
|
+
// leaves the prompt bytes identical to before this line existed.
|
|
215
|
+
if (entry.confirm === true) {
|
|
216
|
+
line += `\n Author flag: \`confirm\` — the author marked this action grave enough to confirm before it fires, so a second press is a mistake.`;
|
|
217
|
+
}
|
|
209
218
|
return line;
|
|
210
219
|
})
|
|
211
220
|
.join('\n');
|
|
@@ -383,7 +392,10 @@ export function injectContracts(
|
|
|
383
392
|
* Returns an empty string when no fields are populated so the inject
|
|
384
393
|
* helper can no-op cleanly.
|
|
385
394
|
*/
|
|
386
|
-
export function buildVarianceContext(
|
|
395
|
+
export function buildVarianceContext(
|
|
396
|
+
variance: BlueprintVariance,
|
|
397
|
+
options?: { readonly profileDeclared?: boolean },
|
|
398
|
+
): string {
|
|
387
399
|
const lines: string[] = [];
|
|
388
400
|
if (typeof variance.persona === 'string' && variance.persona.length > 0) {
|
|
389
401
|
lines.push(`- **Persona**: ${variance.persona}`);
|
|
@@ -411,6 +423,7 @@ export function buildVarianceContext(variance: BlueprintVariance): string {
|
|
|
411
423
|
'the visual treatment + copy register — but the contract shape (props,',
|
|
412
424
|
'actions, contexts, streams) remains canonical. Variance changes HOW',
|
|
413
425
|
'the UI looks and reads, not WHAT it does.',
|
|
426
|
+
...(options?.profileDeclared === true ? [STYLING_PROFILE_PRECEDENCE_NOTE] : []),
|
|
414
427
|
'',
|
|
415
428
|
...lines,
|
|
416
429
|
].join('\n');
|
|
@@ -420,9 +433,10 @@ export function buildVarianceContext(variance: BlueprintVariance): string {
|
|
|
420
433
|
export function injectVariance(
|
|
421
434
|
userPrompt: string,
|
|
422
435
|
variance?: BlueprintVariance,
|
|
436
|
+
options?: { readonly profileDeclared?: boolean },
|
|
423
437
|
): string {
|
|
424
438
|
if (!variance) return userPrompt;
|
|
425
|
-
const block = buildVarianceContext(variance);
|
|
439
|
+
const block = buildVarianceContext(variance, options);
|
|
426
440
|
if (!block) return userPrompt;
|
|
427
441
|
return userPrompt + '\n\n' + block;
|
|
428
442
|
}
|
package/src/create-harness.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// `adapters/generation-dispatch.ts` at the dispatch boundary; callers
|
|
13
13
|
// that omit them get minimal defaults.
|
|
14
14
|
|
|
15
|
+
import { hasProfile } from "./boilerplate/styling-profile.js";
|
|
15
16
|
import type { CacheTier } from "./fragments/index.js";
|
|
16
17
|
import { compose } from "./compose.js";
|
|
17
18
|
import { buildSystemPrompt, generateBoilerplate } from "./boilerplate.js";
|
|
@@ -90,6 +91,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
90
91
|
axisDelta: composed.promptText,
|
|
91
92
|
designMode,
|
|
92
93
|
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
94
|
+
...(input.profile !== undefined ? { profile: input.profile } : {}),
|
|
93
95
|
});
|
|
94
96
|
const howFragments = composed.fragments.filter(
|
|
95
97
|
(f) => f.promptText && f.promptText.trim().length > 0,
|
|
@@ -189,6 +191,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
189
191
|
// Only a non-default design mode contributes — the constrained id is
|
|
190
192
|
// byte-identical to the pre-`designMode` harness.
|
|
191
193
|
if (designMode !== DEFAULT_DESIGN_MODE) overrideLabels.push(`designMode:${designMode}`);
|
|
194
|
+
if (hasProfile(input.profile)) overrideLabels.push("profile");
|
|
192
195
|
|
|
193
196
|
const id = computeHarnessId({
|
|
194
197
|
classificationHash,
|
|
@@ -221,6 +224,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
221
224
|
name,
|
|
222
225
|
classification,
|
|
223
226
|
designMode,
|
|
227
|
+
...(input.profile !== undefined ? { profile: input.profile } : {}),
|
|
224
228
|
how,
|
|
225
229
|
what,
|
|
226
230
|
check,
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* Callers who want a lightweight single-shot path can build it
|
|
32
32
|
* themselves.
|
|
33
33
|
*/
|
|
34
|
+
import type { SingleComponentParams } from './harness/runtime.js';
|
|
34
35
|
import type {
|
|
35
36
|
GadgetDescriptor,
|
|
36
37
|
GenerationError,
|
|
@@ -55,6 +56,8 @@ import {
|
|
|
55
56
|
} from '@ggui-ai/mcp-server-core';
|
|
56
57
|
import { createGeneratorTools } from './adapters/index.js';
|
|
57
58
|
import { dispatchGeneration } from './adapters/generation-dispatch.js';
|
|
59
|
+
import { hasProfile } from './boilerplate/styling-profile.js';
|
|
60
|
+
import { effortDials } from './effort.js';
|
|
58
61
|
import type { ProviderName } from './adapters/types.js';
|
|
59
62
|
import type { GenerationResult } from './harness/result-types.js';
|
|
60
63
|
import {
|
|
@@ -67,6 +70,7 @@ import type { RenderingContext } from './contract-context.js';
|
|
|
67
70
|
import type { DesignMode } from './design-mode.js';
|
|
68
71
|
import { resolveRoute, applyRouteToEnv } from './adapters/provider-router.js';
|
|
69
72
|
import type { QualityConfig } from './evaluation/types-public.js';
|
|
73
|
+
import type { EvaluationConfig } from './evaluation/types.js';
|
|
70
74
|
import type { AgentConfig } from './harness/llm-router.js';
|
|
71
75
|
|
|
72
76
|
/** The slug for the OSS default seed generator. */
|
|
@@ -89,6 +93,20 @@ export interface CreateUiGeneratorOptions {
|
|
|
89
93
|
readonly maxAttempts?: number;
|
|
90
94
|
/** Maximum evaluation rounds. */
|
|
91
95
|
readonly maxEvalRounds?: number;
|
|
96
|
+
/**
|
|
97
|
+
* In-loop LLM evaluation config (ggui#1001). When set, every generation
|
|
98
|
+
* runs the evaluator tiers after the coding leg (`maxRounds` defaults to
|
|
99
|
+
* `maxEvalRounds`) and the harness result carries `evalResult` — the
|
|
100
|
+
* probe verdict a downstream judge reads. Default: none, as today —
|
|
101
|
+
* a serving deployment leaves it unset; offline minting jobs set a round.
|
|
102
|
+
*/
|
|
103
|
+
readonly evaluation?: EvaluationConfig;
|
|
104
|
+
/**
|
|
105
|
+
* In-loop visual round config (per-canvas judging, a sample, the
|
|
106
|
+
* stylesheet to render under). Forwarded verbatim; never part of the
|
|
107
|
+
* prompt or the identity. Default: none — the round runs as today.
|
|
108
|
+
*/
|
|
109
|
+
readonly visualEvaluation?: SingleComponentParams['visualEvaluation'];
|
|
92
110
|
/** Quality config controlling eval tiers + improvement behavior. */
|
|
93
111
|
readonly qualityConfig?: QualityConfig;
|
|
94
112
|
/**
|
|
@@ -208,6 +226,8 @@ export function createUiGenerator(
|
|
|
208
226
|
enableRuntimeRender = false,
|
|
209
227
|
maxAttempts,
|
|
210
228
|
maxEvalRounds,
|
|
229
|
+
evaluation,
|
|
230
|
+
visualEvaluation,
|
|
211
231
|
qualityConfig,
|
|
212
232
|
gadgetCatalog,
|
|
213
233
|
disableEnvMutation = false,
|
|
@@ -313,6 +333,7 @@ export function createUiGenerator(
|
|
|
313
333
|
const promptWithVariance = injectVariance(
|
|
314
334
|
promptWithRendering,
|
|
315
335
|
input.variance,
|
|
336
|
+
{ profileDeclared: hasProfile(input.profile) },
|
|
316
337
|
);
|
|
317
338
|
|
|
318
339
|
// Resolve appGadgets by precedence:
|
|
@@ -335,6 +356,19 @@ export function createUiGenerator(
|
|
|
335
356
|
resolvedAppGadgets,
|
|
336
357
|
);
|
|
337
358
|
|
|
359
|
+
// `effort` (ggui#1059): a named level's dials override the
|
|
360
|
+
// deployment's options for THIS generation; absent ⇒ untouched.
|
|
361
|
+
// Prompt bytes never move with it (dials only).
|
|
362
|
+
const dials = effortDials(input.profile?.effort);
|
|
363
|
+
const effectiveMaxAttempts = dials?.maxAttempts ?? maxAttempts;
|
|
364
|
+
const effectiveMaxEvalRounds = dials?.maxEvalRounds ?? maxEvalRounds;
|
|
365
|
+
const effectiveEvaluation =
|
|
366
|
+
dials !== undefined && evaluation !== undefined ? { ...evaluation, maxRounds: dials.maxEvalRounds } : evaluation;
|
|
367
|
+
const effectiveVisualEvaluation =
|
|
368
|
+
dials !== undefined && visualEvaluation !== undefined
|
|
369
|
+
? { ...visualEvaluation, passThreshold: dials.selfEvalPassThreshold }
|
|
370
|
+
: visualEvaluation;
|
|
371
|
+
|
|
338
372
|
const result = await dispatchGeneration({
|
|
339
373
|
provider,
|
|
340
374
|
userPrompt,
|
|
@@ -342,8 +376,10 @@ export function createUiGenerator(
|
|
|
342
376
|
tools,
|
|
343
377
|
...(input.contract ? { contract: input.contract } : {}),
|
|
344
378
|
originalPrompt: input.request.prompt,
|
|
345
|
-
...(
|
|
346
|
-
...(
|
|
379
|
+
...(effectiveMaxAttempts !== undefined ? { maxAttempts: effectiveMaxAttempts } : {}),
|
|
380
|
+
...(effectiveMaxEvalRounds !== undefined ? { maxEvalRounds: effectiveMaxEvalRounds } : {}),
|
|
381
|
+
...(effectiveEvaluation !== undefined ? { evaluation: effectiveEvaluation } : {}),
|
|
382
|
+
...(effectiveVisualEvaluation !== undefined ? { visualEvaluation: effectiveVisualEvaluation } : {}),
|
|
347
383
|
...(qualityConfig !== undefined ? { qualityConfig } : {}),
|
|
348
384
|
...(resolvedAppGadgets !== undefined
|
|
349
385
|
? { appGadgets: resolvedAppGadgets }
|
|
@@ -360,6 +396,9 @@ export function createUiGenerator(
|
|
|
360
396
|
...(onRetry !== undefined ? { onRetry } : {}),
|
|
361
397
|
...(designMode !== undefined ? { designMode } : {}),
|
|
362
398
|
...(canvas !== undefined ? { canvas } : {}),
|
|
399
|
+
...(input.profile !== undefined ? { profile: input.profile } : {}),
|
|
400
|
+
// Fixture props reach the in-loop probe + visual round only (never the prompt, never identity).
|
|
401
|
+
...(input.fixtureProps !== undefined ? { fixtureProps: input.fixtureProps } : {}),
|
|
363
402
|
});
|
|
364
403
|
|
|
365
404
|
onGenerated?.(result);
|
package/src/design-mode.ts
CHANGED
|
@@ -77,6 +77,16 @@ export type CanvasDisplayMode = "inline" | "fullscreen";
|
|
|
77
77
|
* canvas display mode. Only `chat` and `inline` are the inline card;
|
|
78
78
|
* everything else is width-governed.
|
|
79
79
|
*/
|
|
80
|
+
/**
|
|
81
|
+
* The display mode a JUDGE canvas stands for (ggui#1100): the inline chat
|
|
82
|
+
* card is `xs-chat-card`; every other canvas is a surface the component
|
|
83
|
+
* owns edge to edge, which the served runtime composes with `fit: 'fill'`
|
|
84
|
+
* — so the judge must too, or it scores a picture the user never sees.
|
|
85
|
+
*/
|
|
86
|
+
export function displayModeForCanvas(canvas: CanvasClass): CanvasDisplayMode {
|
|
87
|
+
return canvas === "xs-chat-card" ? "inline" : "fullscreen";
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
export function canvasDisplayModeForShell(shell: string | undefined): CanvasDisplayMode {
|
|
81
91
|
return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
|
|
82
92
|
}
|
|
@@ -177,6 +187,6 @@ export function describeCanvas(canvas: CanvasClass): string {
|
|
|
177
187
|
return (
|
|
178
188
|
`This component renders on the \`${canvas}\` canvas (${d.label}): ` +
|
|
179
189
|
`${d.minWidthPx}–${d.maxWidthPx}px wide, ${d.host}. ` +
|
|
180
|
-
`Design for the whole range — the host may size the iframe anywhere inside it.`
|
|
190
|
+
`Design for the whole range — the host may size the iframe anywhere inside it, and the host owns the height: size to your content, never to the viewport.`
|
|
181
191
|
);
|
|
182
192
|
}
|
|
@@ -26,7 +26,7 @@ export const DEFAULT_DESIGN_SYSTEM_DOCS = `# Design System - Default Light Theme
|
|
|
26
26
|
- var(--ggui-color-primary-300) - #7dd3fc
|
|
27
27
|
- var(--ggui-color-primary-400) - #38bdf8
|
|
28
28
|
- var(--ggui-color-primary-500) - #0ea5e9
|
|
29
|
-
- var(--ggui-color-primary-600) - #0284c7 (main action
|
|
29
|
+
- var(--ggui-color-primary-600) - #0284c7 (main action FILL — buttons, borders, icon fills; accent TEXT takes the link role below)
|
|
30
30
|
- var(--ggui-color-primary-700) - #0369a1
|
|
31
31
|
- var(--ggui-color-primary-800) - #075985
|
|
32
32
|
- var(--ggui-color-primary-900) - #0c4a6e
|
|
@@ -42,10 +42,11 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
|
|
|
42
42
|
| onContainer | var(--ggui-color-onContainer) | Body text on a card |
|
|
43
43
|
| elevated | var(--ggui-color-elevated) | Menus, popovers, modals, toasts — floats above a card |
|
|
44
44
|
| onElevated | var(--ggui-color-onElevated) | Text on an elevated surface |
|
|
45
|
+
| heroGround | var(--ggui-color-heroGround) | A hero / greeting panel's ground — brand-tinted on a light host, the ink pair on a dark one (Card surface="hero") |
|
|
46
|
+
| onHeroGround | var(--ggui-color-onHeroGround) | Text on the hero ground |
|
|
45
47
|
| sunken | var(--ggui-color-sunken) | Wells, inputs at rest, code blocks — recessed inside a card |
|
|
46
48
|
| onSunken | var(--ggui-color-onSunken) | Muted / secondary text |
|
|
47
|
-
|
|
|
48
|
-
| onContainer | var(--ggui-color-onContainer) | Text on branded containers |
|
|
49
|
+
| link | var(--ggui-color-link) | Accent TEXT — eyebrows, taglines, links, labels, inline arrows: the brand's readable ink, derived per mode to clear 4.5:1 on the container and the ground |
|
|
49
50
|
| outline | var(--ggui-color-outline) | Borders, dividers |
|
|
50
51
|
| outlineVariant | var(--ggui-color-outlineVariant) | Subtle borders |
|
|
51
52
|
|
|
@@ -54,6 +55,7 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
|
|
|
54
55
|
- Body text → \`var(--ggui-color-onContainer)\`
|
|
55
56
|
- Card/panel background → \`var(--ggui-color-container)\`; recessed wells / inputs → \`var(--ggui-color-sunken)\`
|
|
56
57
|
- Secondary/muted text → \`var(--ggui-color-onSunken)\`
|
|
58
|
+
- Accent / branded text (eyebrow, tagline, link, label, inline arrow) → \`var(--ggui-color-link)\` or \`<Text tone="emphasized">\` — never a bare \`primary-<stop>\` as a text color
|
|
57
59
|
- Branded section/header → \`var(--ggui-color-container)\` bg + \`var(--ggui-color-onContainer)\` text
|
|
58
60
|
- Borders/dividers → \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
|
|
59
61
|
|
|
@@ -172,7 +174,8 @@ Use \`var(--ggui-spacing-N)\` for all padding, gap, and margin values. **Never u
|
|
|
172
174
|
- **Layering roles** (ground, container, elevated, sunken + their on* inks; outline): one kind of AREA each — use them for every background/text/border decision; they adapt to any theme
|
|
173
175
|
- **primary-50/100**: Section backgrounds, highlight strips, card headers
|
|
174
176
|
- **primary-200/300**: Borders, dividers, focus rings, input outlines
|
|
175
|
-
- **primary-500/600**:
|
|
177
|
+
- **primary-500/600**: Filled buttons, CTAs, badges, icon fills, active indicators — fills and borders, NOT a text color
|
|
178
|
+
- **link**: Accent TEXT — eyebrows, taglines, links, labels, inline arrows; the brand's readable ink per mode (a bare \`primary-300…600\` as a text color fails tier-0: \`universal.accent_text_ink\`)
|
|
176
179
|
- **primary-700/800/900**: Headings and text on light primary backgrounds
|
|
177
180
|
- **neutral-***: Only when you need a specific shade that semantic tokens don't cover
|
|
178
181
|
|
package/src/effort.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The `effort` reader table (ggui#1059, A2). The wire carries ONLY the level
|
|
2
|
+
// name (`generation.profile.effort`, protocol #1058); this table is the
|
|
3
|
+
// cold-gen READER that maps a name onto the harness's dials, versioned here
|
|
4
|
+
// so a level's meaning (and its price line) can move without a wire change.
|
|
5
|
+
//
|
|
6
|
+
// Boundaries, stated once:
|
|
7
|
+
// - `effort` renders NO prompt text — the prompt is byte-identical under any
|
|
8
|
+
// level (`profile-cross.pin.test.ts`). Dials only.
|
|
9
|
+
// - Absent ⇒ the deployment's own options, untouched. The table is applied
|
|
10
|
+
// only when a level is named.
|
|
11
|
+
// - `selfEvalPassThreshold` is the GENERATOR's in-loop visual
|
|
12
|
+
// self-evaluation threshold (`visualEvaluation.passThreshold`). A serving
|
|
13
|
+
// layer's own acceptance bar is a different number and does not move with
|
|
14
|
+
// `effort`.
|
|
15
|
+
// - `modelTier` / `judgeTier` are REQUESTS to the deployment's router
|
|
16
|
+
// (`fast | balanced | premium`), never a model id: a deployment with one
|
|
17
|
+
// key is never promoted by a profile. The harness does not act on them;
|
|
18
|
+
// the caller that owns model selection reads them from this table.
|
|
19
|
+
import type { AppGenerationProfileEffort, ModelTier } from "@ggui-ai/protocol";
|
|
20
|
+
|
|
21
|
+
/** Bump when a level's meaning changes; the price line beside it follows. */
|
|
22
|
+
export const EFFORT_TABLE_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
export interface EffortDials {
|
|
25
|
+
/** Coding-loop turn cap (`maxAttempts`). */
|
|
26
|
+
readonly maxAttempts: number;
|
|
27
|
+
/** Evaluation rounds the harness may spend (`maxEvalRounds`). */
|
|
28
|
+
readonly maxEvalRounds: number;
|
|
29
|
+
/** The generator's in-loop visual self-evaluation threshold, 0–100 — not a serving layer's acceptance bar. */
|
|
30
|
+
readonly selfEvalPassThreshold: number;
|
|
31
|
+
/** Requested coding-model tier — a request to the deployment's router, never a model id. */
|
|
32
|
+
readonly modelTier: ModelTier;
|
|
33
|
+
/** Requested judge-model tier — same nature. */
|
|
34
|
+
readonly judgeTier: ModelTier;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const EFFORT_DIALS: Readonly<Record<AppGenerationProfileEffort, EffortDials>> = {
|
|
38
|
+
low: { maxAttempts: 5, maxEvalRounds: 1, selfEvalPassThreshold: 65, modelTier: "fast", judgeTier: "fast" },
|
|
39
|
+
medium: { maxAttempts: 8, maxEvalRounds: 2, selfEvalPassThreshold: 70, modelTier: "fast", judgeTier: "balanced" },
|
|
40
|
+
high: { maxAttempts: 8, maxEvalRounds: 3, selfEvalPassThreshold: 75, modelTier: "balanced", judgeTier: "balanced" },
|
|
41
|
+
xhigh: { maxAttempts: 10, maxEvalRounds: 3, selfEvalPassThreshold: 80, modelTier: "premium", judgeTier: "balanced" },
|
|
42
|
+
ultra: { maxAttempts: 12, maxEvalRounds: 4, selfEvalPassThreshold: 85, modelTier: "premium", judgeTier: "premium" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The dials for a named level; `undefined` when no level is named (the deployment's options stand). */
|
|
46
|
+
export function effortDials(effort: AppGenerationProfileEffort | undefined): EffortDials | undefined {
|
|
47
|
+
return effort === undefined ? undefined : EFFORT_DIALS[effort];
|
|
48
|
+
}
|