feat(macos): add Canvas A2UI renderer
This commit is contained in:
65
vendor/a2ui/specification/0.8/eval/src/basic_schema_matcher.ts
vendored
Normal file
65
vendor/a2ui/specification/0.8/eval/src/basic_schema_matcher.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SchemaMatcher, ValidationResult } from "./schema_matcher";
|
||||
|
||||
export class BasicSchemaMatcher extends SchemaMatcher {
|
||||
constructor(
|
||||
public propertyPath: string,
|
||||
public propertyValue?: any,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
validate(schema: any): ValidationResult {
|
||||
if (!schema) {
|
||||
const result: ValidationResult = {
|
||||
success: false,
|
||||
error: "Schema is undefined.",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
const pathParts = this.propertyPath.split(".");
|
||||
let actualValue = schema;
|
||||
for (const part of pathParts) {
|
||||
if (actualValue && typeof actualValue === "object") {
|
||||
actualValue = actualValue[part];
|
||||
} else {
|
||||
actualValue = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (actualValue === undefined) {
|
||||
const error = `Failed to find property '${this.propertyPath}'.`;
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
if (this.propertyValue !== undefined) {
|
||||
if (JSON.stringify(actualValue) !== JSON.stringify(this.propertyValue)) {
|
||||
const error = `Property '${
|
||||
this.propertyPath
|
||||
}' has value '${JSON.stringify(
|
||||
actualValue,
|
||||
)}', but expected '${JSON.stringify(this.propertyValue)}'.`;
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
17
vendor/a2ui/specification/0.8/eval/src/dev.ts
vendored
Normal file
17
vendor/a2ui/specification/0.8/eval/src/dev.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "./flows";
|
||||
71
vendor/a2ui/specification/0.8/eval/src/flows.ts
vendored
Normal file
71
vendor/a2ui/specification/0.8/eval/src/flows.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { googleAI } from "@genkit-ai/google-genai";
|
||||
import { genkit, z } from "genkit";
|
||||
import { openAI } from "@genkit-ai/compat-oai/openai";
|
||||
import { anthropic } from "genkitx-anthropic";
|
||||
|
||||
const plugins = [];
|
||||
|
||||
if (process.env.GEMINI_API_KEY) {
|
||||
console.log("Initializing Google AI plugin...");
|
||||
plugins.push(
|
||||
googleAI({
|
||||
apiKey: process.env.GEMINI_API_KEY!,
|
||||
experimental_debugTraces: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
console.log("Initializing OpenAI plugin...");
|
||||
plugins.push(openAI());
|
||||
}
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
console.log("Initializing Anthropic plugin...");
|
||||
plugins.push(anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }));
|
||||
}
|
||||
|
||||
export const ai = genkit({
|
||||
plugins,
|
||||
});
|
||||
|
||||
// Define a UI component generator flow
|
||||
export const componentGeneratorFlow = ai.defineFlow(
|
||||
{
|
||||
name: "componentGeneratorFlow",
|
||||
inputSchema: z.object({
|
||||
prompt: z.string(),
|
||||
model: z.any(),
|
||||
config: z.any().optional(),
|
||||
schema: z.any(),
|
||||
}),
|
||||
outputSchema: z.any(),
|
||||
},
|
||||
async ({ prompt, model, config, schema }) => {
|
||||
// Generate structured component data using the schema from the file
|
||||
const { output } = await ai.generate({
|
||||
prompt,
|
||||
model,
|
||||
output: { contentType: "application/json", jsonSchema: schema },
|
||||
config,
|
||||
});
|
||||
|
||||
if (!output) throw new Error("Failed to generate component");
|
||||
|
||||
return output;
|
||||
},
|
||||
);
|
||||
363
vendor/a2ui/specification/0.8/eval/src/index.ts
vendored
Normal file
363
vendor/a2ui/specification/0.8/eval/src/index.ts
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { componentGeneratorFlow, ai } from "./flows";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { modelsToTest } from "./models";
|
||||
import { prompts, TestPrompt } from "./prompts";
|
||||
import { validateSchema } from "./validator";
|
||||
|
||||
interface InferenceResult {
|
||||
modelName: string;
|
||||
prompt: TestPrompt;
|
||||
component: any;
|
||||
error: any;
|
||||
latency: number;
|
||||
validationResults: string[];
|
||||
runNumber: number;
|
||||
}
|
||||
|
||||
function generateSummary(
|
||||
resultsByModel: Record<string, InferenceResult[]>,
|
||||
results: InferenceResult[],
|
||||
): string {
|
||||
const promptNameWidth = 40;
|
||||
const latencyWidth = 20;
|
||||
const failedRunsWidth = 15;
|
||||
const toolErrorRunsWidth = 20;
|
||||
|
||||
let summary = "# Evaluation Summary";
|
||||
for (const modelName in resultsByModel) {
|
||||
summary += `\n\n## Model: ${modelName}\n\n`;
|
||||
const header = `| ${"Prompt Name".padEnd(
|
||||
promptNameWidth,
|
||||
)} | ${"Avg Latency (ms)".padEnd(latencyWidth)} | ${"Failed Runs".padEnd(
|
||||
failedRunsWidth,
|
||||
)} | ${"Tool Error Runs".padEnd(toolErrorRunsWidth)} |`;
|
||||
const divider = `|${"-".repeat(promptNameWidth + 2)}|${"-".repeat(
|
||||
latencyWidth + 2,
|
||||
)}|${"-".repeat(failedRunsWidth + 2)}|${"-".repeat(
|
||||
toolErrorRunsWidth + 2,
|
||||
)}|`;
|
||||
summary += header;
|
||||
summary += `\n${divider}`;
|
||||
|
||||
const promptsInModel = resultsByModel[modelName].reduce(
|
||||
(acc, result) => {
|
||||
if (!acc[result.prompt.name]) {
|
||||
acc[result.prompt.name] = [];
|
||||
}
|
||||
acc[result.prompt.name].push(result);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, InferenceResult[]>,
|
||||
);
|
||||
|
||||
let totalModelFailedRuns = 0;
|
||||
|
||||
for (const promptName in promptsInModel) {
|
||||
const runs = promptsInModel[promptName];
|
||||
const totalRuns = runs.length;
|
||||
const errorRuns = runs.filter((r) => r.error).length;
|
||||
const failedRuns = runs.filter(
|
||||
(r) => r.error || r.validationResults.length > 0,
|
||||
).length;
|
||||
const totalLatency = runs.reduce((acc, r) => acc + r.latency, 0);
|
||||
const avgLatency = (totalLatency / totalRuns).toFixed(0);
|
||||
|
||||
totalModelFailedRuns += failedRuns;
|
||||
|
||||
const failedRunsStr =
|
||||
failedRuns > 0 ? `${failedRuns} / ${totalRuns}` : "";
|
||||
const errorRunsStr = errorRuns > 0 ? `${errorRuns} / ${totalRuns}` : "";
|
||||
|
||||
summary += `\n| ${promptName.padEnd(
|
||||
promptNameWidth,
|
||||
)} | ${avgLatency.padEnd(latencyWidth)} | ${failedRunsStr.padEnd(
|
||||
failedRunsWidth,
|
||||
)} | ${errorRunsStr.padEnd(toolErrorRunsWidth)} |`;
|
||||
}
|
||||
|
||||
const totalRunsForModel = resultsByModel[modelName].length;
|
||||
summary += `\n\n**Total failed runs:** ${totalModelFailedRuns} / ${totalRunsForModel}`;
|
||||
}
|
||||
|
||||
summary += "\n\n---\n\n## Overall Summary\n";
|
||||
const totalRuns = results.length;
|
||||
const totalToolErrorRuns = results.filter((r) => r.error).length;
|
||||
const totalRunsWithAnyFailure = results.filter(
|
||||
(r) => r.error || r.validationResults.length > 0,
|
||||
).length;
|
||||
const modelsWithFailures = [
|
||||
...new Set(
|
||||
results
|
||||
.filter((r) => r.error || r.validationResults.length > 0)
|
||||
.map((r) => r.modelName),
|
||||
),
|
||||
].join(", ");
|
||||
|
||||
summary += `\n- **Number of tool error runs:** ${totalToolErrorRuns} / ${totalRuns}`;
|
||||
summary += `\n- **Number of runs with any failure (tool error or validation):** ${totalRunsWithAnyFailure} / ${totalRuns}`;
|
||||
const latencies = results.map((r) => r.latency).sort((a, b) => a - b);
|
||||
const totalLatency = latencies.reduce((acc, l) => acc + l, 0);
|
||||
const meanLatency = (totalLatency / totalRuns).toFixed(0);
|
||||
let medianLatency = 0;
|
||||
if (latencies.length > 0) {
|
||||
const mid = Math.floor(latencies.length / 2);
|
||||
if (latencies.length % 2 === 0) {
|
||||
medianLatency = (latencies[mid - 1] + latencies[mid]) / 2;
|
||||
} else {
|
||||
medianLatency = latencies[mid];
|
||||
}
|
||||
}
|
||||
|
||||
summary += `\n- **Mean Latency:** ${meanLatency} ms`;
|
||||
summary += `\n- **Median Latency:** ${medianLatency} ms`;
|
||||
if (modelsWithFailures) {
|
||||
summary += `\n- **Models with at least one failure:** ${modelsWithFailures}`;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
// Run the flow
|
||||
async function main() {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.option("verbose", {
|
||||
alias: "v",
|
||||
type: "boolean",
|
||||
description: "Run with verbose logging",
|
||||
default: false,
|
||||
})
|
||||
.option("keep", {
|
||||
type: "string",
|
||||
description:
|
||||
"Directory to keep output files. If no path is provided, a temporary directory will be created.",
|
||||
coerce: (arg) => (arg === undefined ? true : arg),
|
||||
})
|
||||
.option("runs-per-prompt", {
|
||||
type: "number",
|
||||
description: "Number of times to run each prompt",
|
||||
default: 1,
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
array: true,
|
||||
description: "Filter models by exact name",
|
||||
default: [],
|
||||
choices: modelsToTest.map((m) => m.name),
|
||||
})
|
||||
.option("prompt", {
|
||||
type: "string",
|
||||
description: "Filter prompts by name prefix",
|
||||
})
|
||||
.help()
|
||||
.alias("h", "help").argv;
|
||||
|
||||
const verbose = argv.verbose;
|
||||
const keep = argv.keep;
|
||||
let outputDir: string | null = null;
|
||||
|
||||
if (keep) {
|
||||
if (typeof keep === "string") {
|
||||
outputDir = keep;
|
||||
} else {
|
||||
outputDir = fs.mkdtempSync(path.join(process.cwd(), "a2ui-eval-"));
|
||||
}
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
console.log(`Keeping output in: ${outputDir}`);
|
||||
}
|
||||
|
||||
const runsPerPrompt = argv["runs-per-prompt"];
|
||||
|
||||
let filteredModels = modelsToTest;
|
||||
if (argv.model && argv.model.length > 0) {
|
||||
const modelNames = argv.model as string[];
|
||||
filteredModels = modelsToTest.filter((m) => modelNames.includes(m.name));
|
||||
if (filteredModels.length === 0) {
|
||||
console.error(`No models found matching: ${modelNames.join(", ")}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let filteredPrompts = prompts;
|
||||
if (argv.prompt) {
|
||||
filteredPrompts = prompts.filter((p) =>
|
||||
p.name.startsWith(argv.prompt as string)
|
||||
);
|
||||
if (filteredPrompts.length === 0) {
|
||||
console.error(`No prompt found with prefix "${argv.prompt}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const generationPromises: Promise<InferenceResult>[] = [];
|
||||
|
||||
for (const prompt of filteredPrompts) {
|
||||
const schemaString = fs.readFileSync(
|
||||
path.join(__dirname, prompt.schemaPath),
|
||||
"utf-8"
|
||||
);
|
||||
const schema = JSON.parse(schemaString);
|
||||
for (const modelConfig of filteredModels) {
|
||||
const modelDirName = modelConfig.name.replace(/[\/:]/g, "_");
|
||||
const modelOutputDir = outputDir
|
||||
? path.join(outputDir, modelDirName)
|
||||
: null;
|
||||
if (modelOutputDir && !fs.existsSync(modelOutputDir)) {
|
||||
fs.mkdirSync(modelOutputDir, { recursive: true });
|
||||
}
|
||||
for (let i = 1; i <= runsPerPrompt; i++) {
|
||||
console.log(
|
||||
`Queueing generation for model: ${modelConfig.name}, prompt: ${prompt.name} (run ${i})`
|
||||
);
|
||||
const startTime = Date.now();
|
||||
generationPromises.push(
|
||||
componentGeneratorFlow({
|
||||
prompt: prompt.promptText,
|
||||
model: modelConfig.model,
|
||||
config: modelConfig.config,
|
||||
schema,
|
||||
})
|
||||
.then((component) => {
|
||||
if (modelOutputDir) {
|
||||
const inputPath = path.join(
|
||||
modelOutputDir,
|
||||
`${prompt.name}.input.txt`
|
||||
);
|
||||
fs.writeFileSync(inputPath, prompt.promptText);
|
||||
|
||||
const outputPath = path.join(
|
||||
modelOutputDir,
|
||||
`${prompt.name}.output.json`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
outputPath,
|
||||
JSON.stringify(component, null, 2)
|
||||
);
|
||||
}
|
||||
const validationResults = validateSchema(
|
||||
component,
|
||||
prompt.schemaPath,
|
||||
prompt.matchers
|
||||
);
|
||||
return {
|
||||
modelName: modelConfig.name,
|
||||
prompt,
|
||||
component,
|
||||
error: null,
|
||||
latency: Date.now() - startTime,
|
||||
validationResults,
|
||||
runNumber: i,
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
if (modelOutputDir) {
|
||||
const inputPath = path.join(
|
||||
modelOutputDir,
|
||||
`${prompt.name}.input.txt`
|
||||
);
|
||||
fs.writeFileSync(inputPath, prompt.promptText);
|
||||
|
||||
const errorPath = path.join(
|
||||
modelOutputDir,
|
||||
`${prompt.name}.error.json`
|
||||
);
|
||||
const errorOutput = {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
...error,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
errorPath,
|
||||
JSON.stringify(errorOutput, null, 2)
|
||||
);
|
||||
}
|
||||
return {
|
||||
modelName: modelConfig.name,
|
||||
prompt,
|
||||
component: null,
|
||||
error,
|
||||
latency: Date.now() - startTime,
|
||||
validationResults: [],
|
||||
runNumber: i,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(generationPromises);
|
||||
|
||||
const resultsByModel: Record<string, InferenceResult[]> = {};
|
||||
|
||||
for (const result of results) {
|
||||
if (!resultsByModel[result.modelName]) {
|
||||
resultsByModel[result.modelName] = [];
|
||||
}
|
||||
resultsByModel[result.modelName].push(result);
|
||||
}
|
||||
|
||||
console.log("\n--- Generation Results ---");
|
||||
for (const modelName in resultsByModel) {
|
||||
for (const result of resultsByModel[modelName]) {
|
||||
const hasError = !!result.error;
|
||||
const hasValidationFailures = result.validationResults.length > 0;
|
||||
const hasComponent = !!result.component;
|
||||
|
||||
if (hasError || hasValidationFailures || (verbose && hasComponent)) {
|
||||
console.log(`\n----------------------------------------`);
|
||||
console.log(`Model: ${modelName}`);
|
||||
console.log(`----------------------------------------`);
|
||||
console.log(`\nQuery: ${result.prompt.name} (run ${result.runNumber})`);
|
||||
|
||||
if (hasError) {
|
||||
console.error("Error generating component:", result.error);
|
||||
} else if (hasComponent) {
|
||||
if (hasValidationFailures) {
|
||||
console.log("Validation Failures:");
|
||||
result.validationResults.forEach((failure) =>
|
||||
console.log(`- ${failure}`)
|
||||
);
|
||||
}
|
||||
if (verbose) {
|
||||
if (hasValidationFailures) {
|
||||
console.log("Generated schema:");
|
||||
console.log(JSON.stringify(result.component, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const summary = generateSummary(resultsByModel, results);
|
||||
console.log(summary);
|
||||
if (outputDir) {
|
||||
const summaryPath = path.join(outputDir, "summary.md");
|
||||
fs.writeFileSync(summaryPath, summary);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
50
vendor/a2ui/specification/0.8/eval/src/message_type_matcher.ts
vendored
Normal file
50
vendor/a2ui/specification/0.8/eval/src/message_type_matcher.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SchemaMatcher, ValidationResult } from "./schema_matcher";
|
||||
|
||||
/**
|
||||
* A concrete matcher that verifies the top-level message type.
|
||||
*/
|
||||
export class MessageTypeMatcher extends SchemaMatcher {
|
||||
constructor(private messageType: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
validate(response: object): ValidationResult {
|
||||
if (!response || typeof response !== "object") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Response is not a valid object.",
|
||||
};
|
||||
}
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length === 1 && keys[0] === this.messageType) {
|
||||
return { success: true };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: `Expected top-level message type to be '${
|
||||
this.messageType
|
||||
}', but found '${keys.join(", ")}'`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return `Expected top-level message type to be '${this.messageType}'`;
|
||||
}
|
||||
}
|
||||
68
vendor/a2ui/specification/0.8/eval/src/models.ts
vendored
Normal file
68
vendor/a2ui/specification/0.8/eval/src/models.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { googleAI } from "@genkit-ai/google-genai";
|
||||
import { openAI } from "@genkit-ai/compat-oai/openai";
|
||||
import { claude35Haiku, claude4Sonnet } from "genkitx-anthropic";
|
||||
|
||||
export interface ModelConfiguration {
|
||||
model: any;
|
||||
name: string;
|
||||
config?: any;
|
||||
}
|
||||
|
||||
export const modelsToTest: ModelConfiguration[] = [
|
||||
{
|
||||
model: openAI.model("gpt-5"),
|
||||
name: "gpt-5",
|
||||
config: { reasoning_effort: "minimal" },
|
||||
},
|
||||
{
|
||||
model: openAI.model("gpt-5-mini"),
|
||||
name: "gpt-5-mini",
|
||||
config: { reasoning_effort: "minimal" },
|
||||
},
|
||||
{
|
||||
model: openAI.model("gpt-4.1"),
|
||||
name: "gpt-4.1",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
model: googleAI.model("gemini-2.5-pro"),
|
||||
name: "gemini-2.5-pro-thinking",
|
||||
config: { thinkingConfig: { thinkingBudget: 1000 } },
|
||||
},
|
||||
{
|
||||
model: googleAI.model("gemini-2.5-flash"),
|
||||
name: "gemini-2.5-flash",
|
||||
config: { thinkingConfig: { thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
model: googleAI.model("gemini-2.5-flash-lite"),
|
||||
name: "gemini-2.5-flash-lite",
|
||||
config: { thinkingConfig: { thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
model: claude4Sonnet,
|
||||
name: "claude-4-sonnet",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
model: claude35Haiku,
|
||||
name: "claude-35-haiku",
|
||||
config: {},
|
||||
},
|
||||
];
|
||||
493
vendor/a2ui/specification/0.8/eval/src/prompts.ts
vendored
Normal file
493
vendor/a2ui/specification/0.8/eval/src/prompts.ts
vendored
Normal file
@@ -0,0 +1,493 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { BasicSchemaMatcher } from "./basic_schema_matcher";
|
||||
import { MessageTypeMatcher } from "./message_type_matcher";
|
||||
import { SchemaMatcher } from "./schema_matcher";
|
||||
import { SurfaceUpdateSchemaMatcher } from "./surface_update_schema_matcher";
|
||||
|
||||
export interface TestPrompt {
|
||||
name: string;
|
||||
description: string;
|
||||
schemaPath: string;
|
||||
promptText: string;
|
||||
matchers: SchemaMatcher[];
|
||||
}
|
||||
|
||||
const schemaPath = "../../json/server_to_client_with_standard_catalog.json";
|
||||
|
||||
export const prompts: TestPrompt[] = [
|
||||
{
|
||||
name: "deleteSurface",
|
||||
description: "A DeleteSurface message to remove a UI surface.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message containing a deleteSurface for the surface 'dashboard-surface-1'.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("deleteSurface"),
|
||||
new BasicSchemaMatcher("deleteSurface"),
|
||||
new BasicSchemaMatcher("deleteSurface.surfaceId", "dashboard-surface-1"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "dogBreedGenerator",
|
||||
description:
|
||||
"A prompt to generate a UI for a dog breed information and generator tool.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message containing a surfaceUpdate to describe the following UI:
|
||||
|
||||
A root node has already been created with ID "root".
|
||||
|
||||
A vertical list with:
|
||||
Dog breed information
|
||||
Dog generator
|
||||
|
||||
The dog breed information is a card, which contains a title “Famous Dog breeds”, a header image, and a carousel of different dog breeds. The carousel information should be in the data model at /carousel.
|
||||
|
||||
The dog generator is another card which is a form that generates a fictional dog breed with a description
|
||||
- Title
|
||||
- Description text explaining what it is
|
||||
- Dog breed name (text input)
|
||||
- Number of legs (number input)
|
||||
- Skills (checkboxes)
|
||||
- Button called “Generate” which takes the data above and generates a new dog description
|
||||
- A divider
|
||||
- A section which shows the generated content
|
||||
`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"TextField",
|
||||
"label",
|
||||
"Dog breed name",
|
||||
true
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"TextField",
|
||||
"label",
|
||||
"Number of legs",
|
||||
true
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Generate"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "loginForm",
|
||||
description:
|
||||
'A simple login form with username, password, a "remember me" checkbox, and a submit button.',
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message containing a surfaceUpdate for a login form. It should have a "Login" heading, two text fields for username and password (bound to /login/username and /login/password), a checkbox for "Remember Me" (bound to /login/rememberMe), and a "Sign In" button. The button should trigger a 'login' action, passing the username, password, and rememberMe status in the dynamicContext.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Login"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "username", true),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "password", true),
|
||||
new SurfaceUpdateSchemaMatcher("CheckBox", "label", "Remember Me"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Sign In"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "productGallery",
|
||||
description: "A gallery of products using a list with a template.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message containing a surfaceUpdate for a product gallery. It should display a list of products from the data model at '/products'. Use a template for the list items. Each item should be a Card containing an Image (from '/products/item/imageUrl'), a Text component for the product name (from '/products/item/name'), and a Button labeled "Add to Cart". The button's action should be 'addToCart' and include a staticContext with the product ID, for example, 'productId': 'product123'. You should create a template component and then a list that uses it.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Card"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("Text"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Add to Cart"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "productGalleryData",
|
||||
description:
|
||||
"A DataModelUpdate message to populate the product gallery data.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message containing a dataModelUpdate to populate the data model for the product gallery. The update should target the path '/products' and include at least two products. Each product in the map should have keys 'id', 'name', and 'imageUrl'. For example:
|
||||
{
|
||||
"key": "product1",
|
||||
"valueMap": [
|
||||
{ "key": "id", "valueString": "product1" },
|
||||
{ "key": "name", "valueString": "Awesome Gadget" },
|
||||
{ "key": "imageUrl", "valueString": "https://example.com/gadget.jpg" }
|
||||
]
|
||||
}`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("dataModelUpdate"),
|
||||
new BasicSchemaMatcher("dataModelUpdate.path", "/products"),
|
||||
new BasicSchemaMatcher("dataModelUpdate.contents.0.key"), // Check that the first product key exists
|
||||
new BasicSchemaMatcher("dataModelUpdate.contents.0.valueMap"), // Check that valueMap exists
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "settingsPage",
|
||||
description: "A settings page with tabs and a modal dialog.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message containing a surfaceUpdate for a user settings page. Use a Tabs component with two tabs: "Profile" and "Notifications". The "Profile" tab should contain a simple column with a text field for the user's name. The "Notifications" tab should contain a checkbox for "Enable email notifications". Also, include a Modal component. The modal's entry point should be a button labeled "Delete Account", and its content should be a column with a confirmation text and two buttons: "Confirm Deletion" and "Cancel".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "name", true),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"CheckBox",
|
||||
"label",
|
||||
"Enable email notifications"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Delete Account"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Confirm Deletion"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Cancel"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "dataModelUpdate",
|
||||
description: "A DataModelUpdate message to update user data.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a 'dataModelUpdate' property. This is used to update the client's data model. The scenario is that a user has just logged in, and we need to populate their profile information. Create a single data model update message to set '/user/name' to "John Doe" and '/user/email' to "john.doe@example.com".`,
|
||||
matchers: [new MessageTypeMatcher("dataModelUpdate")],
|
||||
},
|
||||
{
|
||||
name: "uiRoot",
|
||||
description: "A UIRoot message to set the initial UI and data roots.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a 'beginRendering' property. This message tells the client where to start rendering the UI. Set the UI root to a component with ID "mainLayout".`,
|
||||
matchers: [new MessageTypeMatcher("beginRendering")],
|
||||
},
|
||||
{
|
||||
name: "animalKingdomExplorer",
|
||||
description: "A simple, explicit UI to display a hierarchy of animals.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a simplified UI explorer for the Animal Kingdom.
|
||||
|
||||
The UI must have a main 'Heading' with the text "Simple Animal Explorer".
|
||||
|
||||
Below the heading, create a 'Tabs' component with exactly three tabs: "Mammals", "Birds", and "Reptiles".
|
||||
|
||||
Each tab's content should be a 'Column'. The first item in each column must be a 'TextField' with the label "Search...". Below the search field, display the hierarchy for that tab using nested 'Card' components.
|
||||
|
||||
The exact hierarchy to create is as follows:
|
||||
|
||||
**1. "Mammals" Tab:**
|
||||
- A 'Card' for the Class "Mammalia".
|
||||
- Inside the "Mammalia" card, create two 'Card's for the following Orders:
|
||||
- A 'Card' for the Order "Carnivora". Inside this, create 'Card's for these three species: "Lion", "Tiger", "Wolf".
|
||||
- A 'Card' for the Order "Artiodactyla". Inside this, create 'Card's for these two species: "Giraffe", "Hippopotamus".
|
||||
|
||||
**2. "Birds" Tab:**
|
||||
- A 'Card' for the Class "Aves".
|
||||
- Inside the "Aves" card, create three 'Card's for the following Orders:
|
||||
- A 'Card' for the Order "Accipitriformes". Inside this, create a 'Card' for the species: "Bald Eagle".
|
||||
- A 'Card' for the Order "Struthioniformes". Inside this, create a 'Card' for the species: "Ostrich".
|
||||
- A 'Card' for the Order "Sphenisciformes". Inside this, create a 'Card' for the species: "Penguin".
|
||||
|
||||
**3. "Reptiles" Tab:**
|
||||
- A 'Card' for the Class "Reptilia".
|
||||
- Inside the "Reptilia" card, create two 'Card's for the following Orders:
|
||||
- A 'Card' for the Order "Crocodilia". Inside this, create a 'Card' for the species: "Nile Crocodile".
|
||||
- A 'Card' for the Order "Squamata". Inside this, create 'Card's for these two species: "Komodo Dragon", "Ball Python".
|
||||
|
||||
Each species card must contain a 'Row' with an 'Image' and a 'Text' component for the species name. Do not add any other components.
|
||||
|
||||
Each Class and Order card must contain a 'Column' with a 'Text' component with the name, and then the children cards below.
|
||||
|
||||
IMPORTANT: Do not skip any of the classes, orders, or species above. Include every item that is mentioned.
|
||||
`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"Heading",
|
||||
"text",
|
||||
"Simple Animal Explorer"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Search..."),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Class: Mammalia"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Carnivora"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Lion"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Tiger"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Wolf"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Artiodactyla"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Giraffe"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Hippopotamus"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Class: Aves"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Accipitriformes"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Bald Eagle"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Struthioniformes"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Ostrich"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Sphenisciformes"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Penguin"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Class: Reptilia"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Crocodilia"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Nile Crocodile"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Order: Squamata"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Komodo Dragon"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Ball Python"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "recipeCard",
|
||||
description: "A UI to display a recipe with ingredients and instructions.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a recipe card. It should have a 'Heading' for the recipe title, "Classic Lasagna". Below the title, an 'Image' of the lasagna. Then, a 'Row' containing two 'Column's. The first column has a 'Text' heading "Ingredients" and a 'List' of ingredients. The second column has a 'Text' heading "Instructions" and a 'List' of step-by-step instructions. Finally, a 'Button' at the bottom labeled "Watch Video Tutorial".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Classic Lasagna"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Ingredients"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Instructions"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Watch Video Tutorial"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "musicPlayer",
|
||||
description: "A simple music player UI.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a music player. It should be a 'Card' containing a 'Column'. Inside the column, there's an 'Image' for the album art, a 'Text' for the song title "Bohemian Rhapsody", another 'Text' for the artist "Queen", a 'Slider' for the song progress, and a 'Row' with three 'Button's: "Previous", "Play", and "Next".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Bohemian Rhapsody"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Queen"),
|
||||
new SurfaceUpdateSchemaMatcher("Slider"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Previous"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Play"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Next"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "weatherForecast",
|
||||
description: "A UI to display the weather forecast.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a weather forecast UI. It should have a 'Heading' with the city name, "New York". Below it, a 'Row' with the current temperature as a 'Text' component ("68°F") and an 'Image' for the weather icon (e.g., a sun). Below that, a 'Divider'. Then, a 'List' component to display the 5-day forecast. Each item in the list should be a 'Row' with the day, an icon, and high/low temperatures.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "New York"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "68°F"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("List"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "surveyForm",
|
||||
description: "A customer feedback survey form.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a survey form. It should have a 'Heading' "Customer Feedback". Then a 'MultipleChoice' question "How would you rate our service?" with options "Excellent", "Good", "Average", "Poor". Then a 'CheckBox' section for "What did you like?" with options "Product Quality", "Price", "Customer Support". Finally, a 'TextField' with the label "Any other comments?" and a 'Button' labeled "Submit Feedback".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Customer Feedback"),
|
||||
new SurfaceUpdateSchemaMatcher("MultipleChoice", "options", "Excellent"),
|
||||
new SurfaceUpdateSchemaMatcher("CheckBox", "label", "Product Quality"),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"TextField",
|
||||
"label",
|
||||
"Any other comments?"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Submit Feedback"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "flightBooker",
|
||||
description: "A form to search for flights.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a flight booking form. It should have a 'Heading' "Book a Flight". Use a 'Row' for two 'TextField's: "Departure City" and "Arrival City". Below that, another 'Row' for two 'DateTimeInput's: "Departure Date" and "Return Date". Add a 'CheckBox' for "One-way trip". Finally, a 'Button' labeled "Search Flights".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Book a Flight"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Departure City"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Arrival City"),
|
||||
new SurfaceUpdateSchemaMatcher("DateTimeInput"),
|
||||
new SurfaceUpdateSchemaMatcher("CheckBox", "label", "One-way trip"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Search Flights"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "dashboard",
|
||||
description: "A simple dashboard with statistics.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a simple dashboard. It should have a 'Heading' "Sales Dashboard". Below, a 'Row' containing three 'Card's. The first card has a 'Text' "Revenue" and another 'Text' "$50,000". The second card has "New Customers" and "1,200". The third card has "Conversion Rate" and "4.5%".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Sales Dashboard"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Revenue"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "$50,000"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "New Customers"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "1,200"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Conversion Rate"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "4.5%"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "contactCard",
|
||||
description: "A UI to display contact information.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a contact card. It should be a 'Card' with a 'Row'. The row contains an 'Image' (as an avatar) and a 'Column'. The column contains a 'Text' for the name "Jane Doe", a 'Text' for the email "jane.doe@example.com", and a 'Text' for the phone number "(123) 456-7890". Below the main row, add a 'Button' labeled "View on Map".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Jane Doe"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "jane.doe@example.com"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "(123) 456-7890"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "View on Map"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "calendarEventCreator",
|
||||
description: "A form to create a new calendar event.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a calendar event creation form. It should have a 'Heading' "New Event". Include a 'TextField' for the "Event Title". Use a 'Row' for two 'DateTimeInput's for "Start Time" and "End Time". Add a 'CheckBox' labeled "All-day event". Finally, a 'Row' with two 'Button's: "Save" and "Cancel".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "New Event"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Event Title"),
|
||||
new SurfaceUpdateSchemaMatcher("DateTimeInput"),
|
||||
new SurfaceUpdateSchemaMatcher("CheckBox", "label", "All-day event"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Save"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Cancel"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "checkoutPage",
|
||||
description: "A simplified e-commerce checkout page.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a checkout page. It should have a 'Heading' "Checkout". Create a 'Column' for "Shipping Information" with 'TextField's for "Full Name" and "Address". Create another 'Column' for "Payment Information" with 'TextField's for "Card Number" and "Expiry Date". Add a 'Divider'. Show an order summary with a 'Text' component: "Total: $99.99". Finally, a 'Button' labeled "Place Order".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Checkout"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Full Name"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Address"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Card Number"),
|
||||
new SurfaceUpdateSchemaMatcher("TextField", "label", "Expiry Date"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Total: $99.99"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Place Order"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "socialMediaPost",
|
||||
description: "A component representing a social media post.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a social media post. It should be a 'Card' containing a 'Column'. The first item is a 'Row' with an 'Image' (user avatar) and a 'Text' (username "user123"). Below that, a 'Text' component for the post content: "Enjoying the beautiful weather today!". Then, an 'Image' for the main post picture. Finally, a 'Row' with three 'Button's: "Like", "Comment", and "Share".`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "user123"),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"Text",
|
||||
"text",
|
||||
"Enjoying the beautiful weather today!"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Like"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Comment"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Share"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "eCommerceProductPage",
|
||||
description: "A detailed product page for an e-commerce website.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a product details page.
|
||||
The main layout should be a 'Row'.
|
||||
The left side of the row is a 'Column' containing a large main 'Image' of the product, and below it, a 'Row' of three smaller thumbnail 'Image' components.
|
||||
The right side of the row is another 'Column' for product information:
|
||||
- A 'Heading' for the product name, "Premium Leather Jacket".
|
||||
- A 'Text' component for the price, "$299.99".
|
||||
- A 'Divider'.
|
||||
- A 'Text' heading "Select Size", followed by a 'MultipleChoice' component with options "S", "M", "L", "XL".
|
||||
- A 'Text' heading "Select Color", followed by another 'MultipleChoice' component with options "Black", "Brown", "Red".
|
||||
- A 'Button' with the label "Add to Cart".
|
||||
- A 'Text' component for the product description below the button.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"Heading",
|
||||
"text",
|
||||
"Premium Leather Jacket"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "$299.99"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
new SurfaceUpdateSchemaMatcher("MultipleChoice", "options", "S"),
|
||||
new SurfaceUpdateSchemaMatcher("MultipleChoice", "options", "Black"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Add to Cart"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "interactiveDashboard",
|
||||
description: "A dashboard with filters and data cards.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for an interactive analytics dashboard.
|
||||
At the top, a 'Heading' "Company Dashboard".
|
||||
Below the heading, a 'Card' containing a 'Row' of filter controls:
|
||||
- A 'DateTimeInput' with a label for "Start Date".
|
||||
- A 'DateTimeInput' with a label for "End Date".
|
||||
- A 'Button' labeled "Apply Filters".
|
||||
Below the filters card, a 'Row' containing two 'Card's for key metrics:
|
||||
- The first 'Card' has a 'Heading' "Total Revenue" and a 'Text' component showing "$1,234,567".
|
||||
- The second 'Card' has a 'Heading' "New Users" and a 'Text' component showing "4,321".
|
||||
Finally, a large 'Card' at the bottom with a 'Heading' "Revenue Over Time" and a placeholder 'Image' to represent a line chart.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Company Dashboard"),
|
||||
new SurfaceUpdateSchemaMatcher("DateTimeInput"),
|
||||
new SurfaceUpdateSchemaMatcher("Button", "label", "Apply Filters"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Total Revenue"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "$1,234,567"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "New Users"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "4,321"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Revenue Over Time"),
|
||||
new SurfaceUpdateSchemaMatcher("Image"),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "travelItinerary",
|
||||
description: "A multi-day travel itinerary display.",
|
||||
schemaPath,
|
||||
promptText: `Generate a JSON message with a surfaceUpdate property for a travel itinerary for a trip to Paris.
|
||||
It should have a main 'Heading' "Paris Adventure".
|
||||
Below, use a 'List' to display three days. Each item in the list should be a 'Card'.
|
||||
- The first 'Card' (Day 1) should contain a 'Heading' "Day 1: Arrival & Eiffel Tower", and a 'List' of activities for that day: "Check into hotel", "Lunch at a cafe", "Visit the Eiffel Tower".
|
||||
- The second 'Card' (Day 2) should contain a 'Heading' "Day 2: Museums & Culture", and a 'List' of activities: "Visit the Louvre Museum", "Walk through Tuileries Garden", "See the Arc de Triomphe".
|
||||
- The third 'Card' (Day 3) should contain a 'Heading' "Day 3: Art & Departure", and a 'List' of activities: "Visit Musée d'Orsay", "Explore Montmartre", "Depart from CDG".
|
||||
Each activity in the inner lists should be a 'Row' containing a 'CheckBox' (to mark as complete) and a 'Text' component with the activity description.`,
|
||||
matchers: [
|
||||
new MessageTypeMatcher("surfaceUpdate"),
|
||||
new SurfaceUpdateSchemaMatcher("Heading", "text", "Paris Adventure"),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"Heading",
|
||||
"text",
|
||||
"Day 1: Arrival & Eiffel Tower"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"Heading",
|
||||
"text",
|
||||
"Day 2: Museums & Culture"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher(
|
||||
"Heading",
|
||||
"text",
|
||||
"Day 3: Art & Departure"
|
||||
),
|
||||
new SurfaceUpdateSchemaMatcher("Column"),
|
||||
new SurfaceUpdateSchemaMatcher("CheckBox"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Visit the Eiffel Tower"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Visit the Louvre Museum"),
|
||||
new SurfaceUpdateSchemaMatcher("Text", "text", "Explore Montmartre"),
|
||||
],
|
||||
},
|
||||
];
|
||||
24
vendor/a2ui/specification/0.8/eval/src/schema_matcher.ts
vendored
Normal file
24
vendor/a2ui/specification/0.8/eval/src/schema_matcher.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export interface ValidationResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export abstract class SchemaMatcher {
|
||||
abstract validate(schema: any): ValidationResult;
|
||||
}
|
||||
207
vendor/a2ui/specification/0.8/eval/src/surface_update_schema_matcher.ts
vendored
Normal file
207
vendor/a2ui/specification/0.8/eval/src/surface_update_schema_matcher.ts
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SchemaMatcher, ValidationResult } from "./schema_matcher";
|
||||
|
||||
/**
|
||||
* A schema matcher that validates the presence of a component type within a
|
||||
* `surfaceUpdate` message, and optionally validates the presence and value of
|
||||
* a property on that component.
|
||||
*/
|
||||
export class SurfaceUpdateSchemaMatcher extends SchemaMatcher {
|
||||
constructor(
|
||||
public componentType: string,
|
||||
public propertyName?: string,
|
||||
public propertyValue?: any,
|
||||
public caseInsensitive: boolean = false
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getComponentById(components: any[], id: string): any | undefined {
|
||||
return components.find((c: any) => c.id === id);
|
||||
}
|
||||
|
||||
validate(schema: any): ValidationResult {
|
||||
if (!schema.surfaceUpdate) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Expected a 'surfaceUpdate' message but found none.`,
|
||||
};
|
||||
}
|
||||
if (!Array.isArray(schema.surfaceUpdate.components)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `'surfaceUpdate' message does not contain a 'components' array.`,
|
||||
};
|
||||
}
|
||||
|
||||
const components = schema.surfaceUpdate.components;
|
||||
|
||||
for (const c of components) {
|
||||
if (c.component && Object.keys(c.component).length > 1) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Component ID '${c.id}' has multiple component types defined: ${Object.keys(c.component).join(", ")}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const matchingComponents = components.filter(
|
||||
(c: any) => c.component && c.component[this.componentType]
|
||||
);
|
||||
|
||||
if (matchingComponents.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to find component of type '${this.componentType}'.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.propertyName) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
for (const component of matchingComponents) {
|
||||
const properties = component.component[this.componentType];
|
||||
if (properties) {
|
||||
// Check for property directly on the component
|
||||
if (properties[this.propertyName] !== undefined) {
|
||||
if (this.propertyValue === undefined) {
|
||||
return { success: true };
|
||||
}
|
||||
const actualValue = properties[this.propertyName];
|
||||
if (this.valueMatches(actualValue, this.propertyValue)) {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Specifically for Buttons, check for label in a child Text component
|
||||
if (
|
||||
this.componentType === "Button" &&
|
||||
this.propertyName === "label" &&
|
||||
properties.child
|
||||
) {
|
||||
const childComponent = this.getComponentById(
|
||||
components,
|
||||
properties.child
|
||||
);
|
||||
if (
|
||||
childComponent &&
|
||||
childComponent.component &&
|
||||
childComponent.component.Text
|
||||
) {
|
||||
const textValue = childComponent.component.Text.text;
|
||||
if (this.valueMatches(textValue, this.propertyValue)) {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.propertyValue !== undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to find component of type '${this.componentType}' with property '${this.propertyName}' containing value ${JSON.stringify(this.propertyValue)}.`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to find component of type '${this.componentType}' with property '${this.propertyName}'.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private valueMatches(actualValue: any, expectedValue: any): boolean {
|
||||
if (actualValue === null || actualValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const compareStrings = (s1: string, s2: string) => {
|
||||
return this.caseInsensitive
|
||||
? s1.toLowerCase() === s2.toLowerCase()
|
||||
: s1 === s2;
|
||||
};
|
||||
|
||||
// Handle new literal/path object structure
|
||||
if (typeof actualValue === "object" && !Array.isArray(actualValue)) {
|
||||
if (actualValue.literalString !== undefined) {
|
||||
return (
|
||||
typeof expectedValue === "string" &&
|
||||
compareStrings(actualValue.literalString, expectedValue)
|
||||
);
|
||||
}
|
||||
if (actualValue.literalNumber !== undefined) {
|
||||
return actualValue.literalNumber === expectedValue;
|
||||
}
|
||||
if (actualValue.literalBoolean !== undefined) {
|
||||
return actualValue.literalBoolean === expectedValue;
|
||||
}
|
||||
// Could also have a 'path' key, but for matching we'd expect a literal value in expectedValue
|
||||
}
|
||||
|
||||
// Handle array cases (e.g., for MultipleChoice options)
|
||||
if (Array.isArray(actualValue)) {
|
||||
for (const item of actualValue) {
|
||||
if (typeof item === "object" && item !== null) {
|
||||
// Check if the item itself is a bound value object
|
||||
if (
|
||||
item.literalString !== undefined &&
|
||||
typeof expectedValue === "string" &&
|
||||
compareStrings(item.literalString, expectedValue)
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
item.literalNumber !== undefined &&
|
||||
item.literalNumber === expectedValue
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
item.literalBoolean !== undefined &&
|
||||
item.literalBoolean === expectedValue
|
||||
)
|
||||
return true;
|
||||
|
||||
// Check for structures like MultipleChoice options {label: {literalString: ...}, value: ...}
|
||||
if (
|
||||
item.label &&
|
||||
typeof item.label === "object" &&
|
||||
item.label.literalString !== undefined &&
|
||||
typeof expectedValue === "string" &&
|
||||
compareStrings(item.label.literalString, expectedValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (item.value === expectedValue) {
|
||||
return true;
|
||||
}
|
||||
} else if (
|
||||
typeof item === "string" &&
|
||||
typeof expectedValue === "string" &&
|
||||
compareStrings(item, expectedValue)
|
||||
) {
|
||||
return true;
|
||||
} else if (item === expectedValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct comparison
|
||||
return JSON.stringify(actualValue) === JSON.stringify(expectedValue);
|
||||
}
|
||||
}
|
||||
521
vendor/a2ui/specification/0.8/eval/src/validator.ts
vendored
Normal file
521
vendor/a2ui/specification/0.8/eval/src/validator.ts
vendored
Normal file
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SurfaceUpdateSchemaMatcher } from "./surface_update_schema_matcher";
|
||||
import { SchemaMatcher } from "./schema_matcher";
|
||||
|
||||
export function validateSchema(
|
||||
data: any,
|
||||
schemaName: string,
|
||||
matchers?: SchemaMatcher[],
|
||||
): string[] {
|
||||
const errors: string[] = [];
|
||||
if (data.surfaceUpdate) {
|
||||
validateSurfaceUpdate(data.surfaceUpdate, errors);
|
||||
} else if (data.dataModelUpdate) {
|
||||
validateDataModelUpdate(data.dataModelUpdate, errors);
|
||||
} else if (data.beginRendering) {
|
||||
validateBeginRendering(data.beginRendering, errors);
|
||||
} else if (data.deleteSurface) {
|
||||
validateDeleteSurface(data.deleteSurface, errors);
|
||||
} else {
|
||||
errors.push(
|
||||
"A2UI Protocol message must have one of: surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface.",
|
||||
);
|
||||
}
|
||||
|
||||
if (matchers) {
|
||||
for (const matcher of matchers) {
|
||||
const result = matcher.validate(data);
|
||||
if (!result.success) {
|
||||
errors.push(result.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateDeleteSurface(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("DeleteSurface must have a 'surfaceId' property.");
|
||||
}
|
||||
const allowed = ["surfaceId"];
|
||||
for (const key in data) {
|
||||
if (!allowed.includes(key)) {
|
||||
errors.push(`DeleteSurface has unexpected property: ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateSurfaceUpdate(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("SurfaceUpdate must have a 'surfaceId' property.");
|
||||
}
|
||||
if (!data.components || !Array.isArray(data.components)) {
|
||||
errors.push("SurfaceUpdate must have a 'components' array.");
|
||||
return;
|
||||
}
|
||||
|
||||
const componentIds = new Set<string>();
|
||||
for (const c of data.components) {
|
||||
if (c.id) {
|
||||
if (componentIds.has(c.id)) {
|
||||
errors.push(`Duplicate component ID found: ${c.id}`);
|
||||
}
|
||||
componentIds.add(c.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const component of data.components) {
|
||||
validateComponent(component, componentIds, errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDataModelUpdate(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("DataModelUpdate must have a 'surfaceId' property.");
|
||||
}
|
||||
|
||||
const allowedTopLevel = ["surfaceId", "path", "contents"];
|
||||
for (const key in data) {
|
||||
if (!allowedTopLevel.includes(key)) {
|
||||
errors.push(`DataModelUpdate has unexpected property: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.contents)) {
|
||||
errors.push("DataModelUpdate must have a 'contents' array.");
|
||||
return;
|
||||
}
|
||||
|
||||
const validateValueProperty = (
|
||||
item: any,
|
||||
itemErrors: string[],
|
||||
prefix: string,
|
||||
) => {
|
||||
const valueProps = [
|
||||
"valueString",
|
||||
"valueNumber",
|
||||
"valueBoolean",
|
||||
"valueMap",
|
||||
];
|
||||
let valueCount = 0;
|
||||
let foundValueProp = "";
|
||||
for (const prop of valueProps) {
|
||||
if (item[prop] !== undefined) {
|
||||
valueCount++;
|
||||
foundValueProp = prop;
|
||||
}
|
||||
}
|
||||
if (valueCount !== 1) {
|
||||
itemErrors.push(
|
||||
`${prefix} must have exactly one value property (${valueProps.join(", ")}), found ${valueCount}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundValueProp === "valueMap") {
|
||||
if (!Array.isArray(item.valueMap)) {
|
||||
itemErrors.push(`${prefix} 'valueMap' must be an array.`);
|
||||
return;
|
||||
}
|
||||
item.valueMap.forEach((mapItem: any, index: number) => {
|
||||
if (!mapItem.key) {
|
||||
itemErrors.push(
|
||||
`${prefix} 'valueMap' item at index ${index} is missing a 'key'.`,
|
||||
);
|
||||
}
|
||||
const mapValueProps = ["valueString", "valueNumber", "valueBoolean"];
|
||||
let mapValueCount = 0;
|
||||
for (const prop of mapValueProps) {
|
||||
if (mapItem[prop] !== undefined) {
|
||||
mapValueCount++;
|
||||
}
|
||||
}
|
||||
if (mapValueCount !== 1) {
|
||||
itemErrors.push(
|
||||
`${prefix} 'valueMap' item at index ${index} must have exactly one value property (${mapValueProps.join(", ")}), found ${mapValueCount}.`,
|
||||
);
|
||||
}
|
||||
const allowedMapKeys = ["key", ...mapValueProps];
|
||||
for (const key in mapItem) {
|
||||
if (!allowedMapKeys.includes(key)) {
|
||||
itemErrors.push(
|
||||
`${prefix} 'valueMap' item at index ${index} has unexpected property: ${key}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
data.contents.forEach((item: any, index: number) => {
|
||||
if (!item.key) {
|
||||
errors.push(
|
||||
`DataModelUpdate 'contents' item at index ${index} is missing a 'key'.`,
|
||||
);
|
||||
}
|
||||
validateValueProperty(
|
||||
item,
|
||||
errors,
|
||||
`DataModelUpdate 'contents' item at index ${index}`,
|
||||
);
|
||||
const allowedKeys = [
|
||||
"key",
|
||||
"valueString",
|
||||
"valueNumber",
|
||||
"valueBoolean",
|
||||
"valueMap",
|
||||
];
|
||||
for (const key in item) {
|
||||
if (!allowedKeys.includes(key)) {
|
||||
errors.push(
|
||||
`DataModelUpdate 'contents' item at index ${index} has unexpected property: ${key}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateBeginRendering(data: any, errors: string[]) {
|
||||
if (data.surfaceId === undefined) {
|
||||
errors.push("BeginRendering message must have a 'surfaceId' property.");
|
||||
}
|
||||
if (!data.root) {
|
||||
errors.push("BeginRendering message must have a 'root' property.");
|
||||
}
|
||||
}
|
||||
|
||||
function validateBoundValue(
|
||||
prop: any,
|
||||
propName: string,
|
||||
componentId: string,
|
||||
componentType: string,
|
||||
errors: string[],
|
||||
) {
|
||||
if (typeof prop !== "object" || prop === null || Array.isArray(prop)) {
|
||||
errors.push(
|
||||
`Component '${componentId}' of type '${componentType}' property '${propName}' must be an object.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const keys = Object.keys(prop);
|
||||
const allowedKeys = [
|
||||
"literalString",
|
||||
"literalNumber",
|
||||
"literalBoolean",
|
||||
"path",
|
||||
];
|
||||
let validKeyCount = 0;
|
||||
for (const key of keys) {
|
||||
if (allowedKeys.includes(key)) {
|
||||
validKeyCount++;
|
||||
}
|
||||
}
|
||||
if (validKeyCount !== 1 || keys.length !== 1) {
|
||||
errors.push(
|
||||
`Component '${componentId}' of type '${componentType}' property '${propName}' must have exactly one key from [${allowedKeys.join(", ")}]. Found: ${keys.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateComponent(
|
||||
component: any,
|
||||
allIds: Set<string>,
|
||||
errors: string[],
|
||||
) {
|
||||
if (!component.id) {
|
||||
errors.push(`Component is missing an 'id'.`);
|
||||
return;
|
||||
}
|
||||
if (!component.component) {
|
||||
errors.push(`Component '${component.id}' is missing 'component'.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentTypes = Object.keys(component.component);
|
||||
if (componentTypes.length !== 1) {
|
||||
errors.push(
|
||||
`Component '${component.id}' must have exactly one property in 'component', but found ${componentTypes.length}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentType = componentTypes[0];
|
||||
const properties = component.component[componentType];
|
||||
|
||||
const checkRequired = (props: string[]) => {
|
||||
for (const prop of props) {
|
||||
if (properties[prop] === undefined) {
|
||||
errors.push(
|
||||
`Component '${component.id}' of type '${componentType}' is missing required property '${prop}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkRefs = (ids: (string | undefined)[]) => {
|
||||
for (const id of ids) {
|
||||
if (id && !allIds.has(id)) {
|
||||
errors.push(
|
||||
`Component '${component.id}' references non-existent component ID '${id}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (componentType) {
|
||||
case "Heading":
|
||||
checkRequired(["text"]);
|
||||
if (properties.text)
|
||||
validateBoundValue(
|
||||
properties.text,
|
||||
"text",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "Text":
|
||||
checkRequired(["text"]);
|
||||
if (properties.text)
|
||||
validateBoundValue(
|
||||
properties.text,
|
||||
"text",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "Image":
|
||||
checkRequired(["url"]);
|
||||
if (properties.url)
|
||||
validateBoundValue(
|
||||
properties.url,
|
||||
"url",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "Video":
|
||||
checkRequired(["url"]);
|
||||
if (properties.url)
|
||||
validateBoundValue(
|
||||
properties.url,
|
||||
"url",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "AudioPlayer":
|
||||
checkRequired(["url"]);
|
||||
if (properties.url)
|
||||
validateBoundValue(
|
||||
properties.url,
|
||||
"url",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
if (properties.description)
|
||||
validateBoundValue(
|
||||
properties.description,
|
||||
"description",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "TextField":
|
||||
checkRequired(["label"]);
|
||||
if (properties.label)
|
||||
validateBoundValue(
|
||||
properties.label,
|
||||
"label",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
if (properties.text)
|
||||
validateBoundValue(
|
||||
properties.text,
|
||||
"text",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "DateTimeInput":
|
||||
checkRequired(["value"]);
|
||||
if (properties.value)
|
||||
validateBoundValue(
|
||||
properties.value,
|
||||
"value",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "MultipleChoice":
|
||||
checkRequired(["selections", "options"]);
|
||||
if (properties.selections) {
|
||||
if (
|
||||
typeof properties.selections !== "object" ||
|
||||
properties.selections === null ||
|
||||
(!properties.selections.literalArray && !properties.selections.path)
|
||||
) {
|
||||
errors.push(
|
||||
`Component '${component.id}' of type '${componentType}' property 'selections' must have either 'literalArray' or 'path'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(properties.options)) {
|
||||
properties.options.forEach((option: any, index: number) => {
|
||||
if (!option.label)
|
||||
errors.push(
|
||||
`Component '${component.id}' option at index ${index} missing 'label'.`,
|
||||
);
|
||||
if (option.label)
|
||||
validateBoundValue(
|
||||
option.label,
|
||||
"label",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
if (!option.value)
|
||||
errors.push(
|
||||
`Component '${component.id}' option at index ${index} missing 'value'.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "Slider":
|
||||
checkRequired(["value"]);
|
||||
if (properties.value)
|
||||
validateBoundValue(
|
||||
properties.value,
|
||||
"value",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "CheckBox":
|
||||
checkRequired(["value", "label"]);
|
||||
if (properties.value)
|
||||
validateBoundValue(
|
||||
properties.value,
|
||||
"value",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
if (properties.label)
|
||||
validateBoundValue(
|
||||
properties.label,
|
||||
"label",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
case "Row":
|
||||
case "Column":
|
||||
case "List":
|
||||
checkRequired(["children"]);
|
||||
if (properties.children && Array.isArray(properties.children)) {
|
||||
const hasExplicit = !!properties.children.explicitList;
|
||||
const hasTemplate = !!properties.children.template;
|
||||
if ((hasExplicit && hasTemplate) || (!hasExplicit && !hasTemplate)) {
|
||||
errors.push(
|
||||
`Component '${component.id}' must have either 'explicitList' or 'template' in children, but not both or neither.`,
|
||||
);
|
||||
}
|
||||
if (hasExplicit) {
|
||||
checkRefs(properties.children.explicitList);
|
||||
}
|
||||
if (hasTemplate) {
|
||||
checkRefs([properties.children.template?.componentId]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "Card":
|
||||
checkRequired(["child"]);
|
||||
checkRefs([properties.child]);
|
||||
break;
|
||||
case "Tabs":
|
||||
checkRequired(["tabItems"]);
|
||||
if (properties.tabItems && Array.isArray(properties.tabItems)) {
|
||||
properties.tabItems.forEach((tab: any) => {
|
||||
if (!tab.title) {
|
||||
errors.push(
|
||||
`Tab item in component '${component.id}' is missing a 'title'.`,
|
||||
);
|
||||
}
|
||||
if (!tab.child) {
|
||||
errors.push(
|
||||
`Tab item in component '${component.id}' is missing a 'child'.`,
|
||||
);
|
||||
}
|
||||
checkRefs([tab.child]);
|
||||
if (tab.title)
|
||||
validateBoundValue(
|
||||
tab.title,
|
||||
"title",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "Modal":
|
||||
checkRequired(["entryPointChild", "contentChild"]);
|
||||
checkRefs([properties.entryPointChild, properties.contentChild]);
|
||||
break;
|
||||
case "Button":
|
||||
checkRequired(["child", "action"]);
|
||||
checkRefs([properties.child]);
|
||||
if (!properties.action || !properties.action.name) {
|
||||
errors.push(
|
||||
`Component '${component.id}' Button action is missing a 'name'.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "Divider":
|
||||
// No required properties
|
||||
break;
|
||||
case "Icon":
|
||||
checkRequired(["name"]);
|
||||
if (properties.name)
|
||||
validateBoundValue(
|
||||
properties.name,
|
||||
"name",
|
||||
component.id,
|
||||
componentType,
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
errors.push(
|
||||
`Unknown component type '${componentType}' in component '${component.id}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user