convert to, lines and fixes

This commit is contained in:
shine1i 2026-02-04 16:33:42 +01:00
commit fbb8adbab5
10 changed files with 398 additions and 174 deletions

View file

@ -171,7 +171,9 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
icon: Database02Icon,
createConfig: (id, existing) => makeSamplerConfig(id, "person", existing),
renderDialog: ({ config, onUpdate }) =>
config.kind === "sampler" && config.sampler_type === "person" ? (
config.kind === "sampler" &&
(config.sampler_type === "person" ||
config.sampler_type === "person_from_faker") ? (
<PersonDialog
config={config}
onUpdate={(patch) => onUpdate(config.id, patch)}
@ -262,7 +264,11 @@ export function getBlockDefinitionForConfig(
return null;
}
if (config.kind === "sampler") {
return getBlockDefinition("sampler", config.sampler_type);
const samplerType =
config.sampler_type === "person_from_faker"
? "person"
: config.sampler_type;
return getBlockDefinition("sampler", samplerType);
}
if (config.kind === "llm") {
return getBlockDefinition("llm", config.llm_type);

View file

@ -1,4 +1,11 @@
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { ReactElement } from "react";
import type { SamplerConfig } from "../../types";
import { NameField } from "../shared/name-field";
@ -14,6 +21,7 @@ export function GaussianDialog({
}: GaussianDialogProps): ReactElement {
const meanId = `${config.id}-gaussian-mean`;
const stdId = `${config.id}-gaussian-std`;
const convertId = `${config.id}-gaussian-convert`;
return (
<div className="space-y-4">
<NameField
@ -52,6 +60,33 @@ export function GaussianDialog({
/>
</div>
</div>
<div className="grid gap-2">
<label
className="text-xs font-semibold uppercase text-muted-foreground"
htmlFor={convertId}
>
Convert to
</label>
<Select
value={config.convert_to ?? "none"}
onValueChange={(value) =>
onUpdate({
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: value === "none" ? undefined : (value as "int" | "float" | "str"),
})
}
>
<SelectTrigger className="nodrag w-full" id={convertId}>
<SelectValue placeholder="No conversion" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="int">int</SelectItem>
<SelectItem value="float">float</SelectItem>
<SelectItem value="str">str</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}

View file

@ -1,4 +1,11 @@
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import type { ReactElement } from "react";
import type { SamplerConfig } from "../../types";
@ -17,6 +24,7 @@ export function PersonDialog({
const sexId = `${config.id}-person-sex`;
const ageRangeId = `${config.id}-person-age-range`;
const cityId = `${config.id}-person-city`;
const sourceId = `${config.id}-person-source`;
const updateField = <K extends keyof SamplerConfig>(
key: K,
value: SamplerConfig[K],
@ -30,6 +38,31 @@ export function PersonDialog({
onChange={(value) => onUpdate({ name: value })}
/>
<div className="grid gap-3">
<div className="grid gap-2">
<label
className="text-xs font-semibold uppercase text-muted-foreground"
htmlFor={sourceId}
>
Source
</label>
<Select
value={config.sampler_type === "person_from_faker" ? "faker" : "person"}
onValueChange={(value) =>
updateField(
"sampler_type",
value === "faker" ? "person_from_faker" : "person",
)
}
>
<SelectTrigger className="nodrag w-full" id={sourceId}>
<SelectValue placeholder="Select source" />
</SelectTrigger>
<SelectContent>
<SelectItem value="person">Managed dataset</SelectItem>
<SelectItem value="faker">Faker</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="grid gap-2">
<label
@ -54,14 +87,21 @@ export function PersonDialog({
>
Sex
</label>
<Input
id={sexId}
className="nodrag"
value={config.person_sex ?? ""}
onChange={(event) =>
updateField("person_sex", event.target.value)
<Select
value={config.person_sex?.trim() ? config.person_sex : "any"}
onValueChange={(value) =>
updateField("person_sex", value === "any" ? "" : value)
}
/>
>
<SelectTrigger className="nodrag w-full" id={sexId}>
<SelectValue placeholder="Any" />
</SelectTrigger>
<SelectContent>
<SelectItem value="any">Any</SelectItem>
<SelectItem value="Male">Male</SelectItem>
<SelectItem value="Female">Female</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<label
@ -77,6 +117,7 @@ export function PersonDialog({
onChange={(event) =>
updateField("person_age_range", event.target.value)
}
placeholder="18-70"
/>
</div>
<div className="grid gap-2">
@ -96,34 +137,22 @@ export function PersonDialog({
/>
</div>
</div>
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-2">
<div>
<p className="text-sm font-semibold">Synthetic personas</p>
<p className="text-xs text-muted-foreground">
Generate persona profiles.
</p>
{config.sampler_type === "person" && (
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-2">
<div>
<p className="text-sm font-semibold">Synthetic personas</p>
<p className="text-xs text-muted-foreground">
Generate persona profiles.
</p>
</div>
<Switch
checked={config.person_with_synthetic_personas ?? false}
onCheckedChange={(value) =>
updateField("person_with_synthetic_personas", value)
}
/>
</div>
<Switch
checked={config.person_with_synthetic_personas ?? false}
onCheckedChange={(value) =>
updateField("person_with_synthetic_personas", value)
}
/>
</div>
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-2">
<div>
<p className="text-sm font-semibold">Sample dataset</p>
<p className="text-xs text-muted-foreground">
Use dataset when available.
</p>
</div>
<Switch
checked={config.person_sample_dataset_when_available ?? false}
onCheckedChange={(value) =>
updateField("person_sample_dataset_when_available", value)
}
/>
</div>
)}
</div>
</div>
);

View file

@ -1,4 +1,11 @@
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { ReactElement } from "react";
import type { SamplerConfig } from "../../types";
import { NameField } from "../shared/name-field";
@ -14,6 +21,7 @@ export function UniformDialog({
}: UniformDialogProps): ReactElement {
const lowId = `${config.id}-uniform-low`;
const highId = `${config.id}-uniform-high`;
const convertId = `${config.id}-uniform-convert`;
return (
<div className="space-y-4">
<NameField
@ -52,6 +60,33 @@ export function UniformDialog({
/>
</div>
</div>
<div className="grid gap-2">
<label
className="text-xs font-semibold uppercase text-muted-foreground"
htmlFor={convertId}
>
Convert to
</label>
<Select
value={config.convert_to ?? "none"}
onValueChange={(value) =>
onUpdate({
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: value === "none" ? undefined : (value as "int" | "float" | "str"),
})
}
>
<SelectTrigger className="nodrag w-full" id={convertId}>
<SelectValue placeholder="No conversion" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="int">int</SelectItem>
<SelectItem value="float">float</SelectItem>
<SelectItem value="str">str</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}

View file

@ -7,7 +7,8 @@ export type SamplerType =
| "gaussian"
| "datetime"
| "uuid"
| "person";
| "person"
| "person_from_faker";
export type LlmType = "text" | "structured" | "code";
@ -28,6 +29,8 @@ export type SamplerConfig = {
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: SamplerType;
name: string;
// biome-ignore lint/style/useNamingConvention: api schema
convert_to?: "float" | "int" | "str";
values?: string[];
weights?: Array<number | null>;
low?: string;
@ -53,8 +56,6 @@ export type SamplerConfig = {
// biome-ignore lint/style/useNamingConvention: api schema
person_with_synthetic_personas?: boolean;
// biome-ignore lint/style/useNamingConvention: api schema
person_sample_dataset_when_available?: boolean;
// biome-ignore lint/style/useNamingConvention: api schema
subcategory_parent?: string;
// biome-ignore lint/style/useNamingConvention: api schema
subcategory_mapping?: Record<string, string[]>;

View file

@ -57,16 +57,7 @@ export function isValidCanvasConnection(
}
const source = configs[connection.source];
const target = configs[connection.target];
if (!(source && target)) {
return false;
}
if (isSubcategoryConfig(target)) {
return isCategoryConfig(source);
}
if (isLlmConfig(target) || isExpressionConfig(target)) {
return true;
}
return false;
return Boolean(source && target);
}
export function applyCanvasConnection(
@ -99,7 +90,7 @@ export function applyCanvasConnection(
};
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
}
if (isSubcategoryConfig(target)) {
if (isSubcategoryConfig(target) && isCategoryConfig(source)) {
const next = syncSubcategoryMapping(target, source);
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
}

View file

@ -16,6 +16,7 @@ const SAMPLER_TYPES: SamplerType[] = [
"datetime",
"uuid",
"person",
"person_from_faker",
];
const EXPRESSION_DTYPES: ExpressionDtype[] = ["str", "int", "float", "bool"];
@ -31,6 +32,11 @@ function parseSampler(
errors.push(`Sampler ${name}: unsupported sampler_type.`);
return null;
}
const convertTo = readString(column.convert_to);
const normalizedConvertTo =
convertTo && ["float", "int", "str"].includes(convertTo)
? (convertTo as "float" | "int" | "str")
: undefined;
const params =
typeof column.params === "object" && column.params
? (column.params as Record<string, unknown>)
@ -48,6 +54,8 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "category",
name,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
values,
weights,
};
@ -68,6 +76,8 @@ function parseSampler(
sampler_type: "subcategory",
name,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
subcategory_parent: readString(params.category) ?? "",
// biome-ignore lint/style/useNamingConvention: api schema
subcategory_mapping: mapping,
@ -80,6 +90,8 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "uniform",
name,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
low: readNumberString(params.low),
high: readNumberString(params.high),
};
@ -91,6 +103,8 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "gaussian",
name,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
mean: readNumberString(params.mean),
std: readNumberString(params.std),
};
@ -103,6 +117,8 @@ function parseSampler(
sampler_type: "datetime",
name,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
datetime_start: readString(params.start) ?? "",
// biome-ignore lint/style/useNamingConvention: api schema
datetime_end: readString(params.end) ?? "",
@ -118,34 +134,45 @@ function parseSampler(
sampler_type: "uuid",
name,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
uuid_format: readString(params.format) ?? "",
};
}
return {
const ageRange =
Array.isArray(params.age_range) &&
params.age_range.length === 2 &&
params.age_range.every((item) => typeof item === "number")
? `${params.age_range[0]}-${params.age_range[1]}`
: readString(params.age_range) ?? "";
const base = {
id,
kind: "sampler",
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "person",
name,
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: samplerType as SamplerType,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
person_locale: readString(params.locale) ?? "",
// biome-ignore lint/style/useNamingConvention: api schema
person_sex: readString(params.sex) ?? "",
// biome-ignore lint/style/useNamingConvention: api schema
person_age_range: readString(params.age_range) ?? "",
person_age_range: ageRange,
// biome-ignore lint/style/useNamingConvention: api schema
person_city: readString(params.city) ?? "",
// biome-ignore lint/style/useNamingConvention: api schema
person_with_synthetic_personas:
typeof params.with_synthetic_personas === "boolean"
? params.with_synthetic_personas
: false,
// biome-ignore lint/style/useNamingConvention: api schema
person_sample_dataset_when_available:
typeof params.sample_dataset_when_available === "boolean"
? params.sample_dataset_when_available
: false,
};
if (samplerType === "person") {
return {
...base,
// biome-ignore lint/style/useNamingConvention: api schema
person_with_synthetic_personas:
typeof params.with_synthetic_personas === "boolean"
? params.with_synthetic_personas
: false,
};
}
return base;
}
function parseLlm(

View file

@ -8,6 +8,7 @@ import type {
SamplerConfig,
SamplerType,
} from "../types";
export { getConfigErrors } from "./validation";
const SAMPLER_LABELS: Record<SamplerType, string> = {
category: "Category",
@ -17,6 +18,7 @@ const SAMPLER_LABELS: Record<SamplerType, string> = {
datetime: "Datetime",
uuid: "UUID",
person: "Person",
person_from_faker: "Person (Faker)",
};
const LLM_LABELS: Record<LlmType, string> = {
@ -130,6 +132,23 @@ export function makeSamplerConfig(
uuid_format: "",
};
}
if (samplerType === "person_from_faker") {
return {
id,
kind: "sampler",
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "person_from_faker",
name,
// biome-ignore lint/style/useNamingConvention: api schema
person_locale: "",
// biome-ignore lint/style/useNamingConvention: api schema
person_sex: "",
// biome-ignore lint/style/useNamingConvention: api schema
person_age_range: "",
// biome-ignore lint/style/useNamingConvention: api schema
person_city: "",
};
}
return {
id,
kind: "sampler",
@ -146,8 +165,6 @@ export function makeSamplerConfig(
person_city: "",
// biome-ignore lint/style/useNamingConvention: api schema
person_with_synthetic_personas: false,
// biome-ignore lint/style/useNamingConvention: api schema
person_sample_dataset_when_available: false,
};
}
@ -170,7 +187,7 @@ export function makeLlmConfig(
llm_type: llmType,
name,
// biome-ignore lint/style/useNamingConvention: api schema
model_alias: "stepfun/step-3.5-flash:free",
model_alias: "allenai/olmo-3.1-32b-instruct",
prompt: "Write a response.",
// biome-ignore lint/style/useNamingConvention: api schema
system_prompt: "",
@ -267,98 +284,3 @@ export function isExpressionConfig(
): config is ExpressionConfig {
return Boolean(config && config.kind === "expression");
}
function parseNumber(value?: string): number | null {
if (!value) {
return null;
}
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: validation rules
export function getConfigErrors(config: NodeConfig | null): string[] {
if (!config) {
return [];
}
const errors: string[] = [];
if (!config.name.trim()) {
errors.push("Name is required.");
}
if (config.kind === "sampler") {
if (config.sampler_type === "category") {
const values = config.values ?? [];
if (values.length < 2) {
errors.push("Category needs at least 2 values.");
}
const weights = config.weights ?? [];
const hasWeights = weights.some((weight) => weight !== null);
if (hasWeights && weights.some((weight) => weight === null)) {
errors.push("Weights must be set for all values.");
}
}
if (config.sampler_type === "uniform") {
const low = parseNumber(config.low);
const high = parseNumber(config.high);
if (low === null || high === null) {
errors.push("Uniform low/high must be numbers.");
} else if (low >= high) {
errors.push("Uniform low must be < high.");
}
}
if (config.sampler_type === "gaussian") {
const mean = parseNumber(config.mean);
const std = parseNumber(config.std);
if (mean === null || std === null) {
errors.push("Gaussian mean/std must be numbers.");
} else if (std <= 0) {
errors.push("Gaussian std must be > 0.");
}
}
if (config.sampler_type === "datetime") {
if (!config.datetime_unit) {
errors.push("Datetime unit required.");
}
if (config.datetime_start && config.datetime_end) {
const start = new Date(config.datetime_start).getTime();
const end = new Date(config.datetime_end).getTime();
if (!(Number.isFinite(start) && Number.isFinite(end))) {
errors.push("Datetime start/end must be valid.");
} else if (start >= end) {
errors.push("Datetime start must be before end.");
}
}
}
if (config.sampler_type === "subcategory" && !config.subcategory_parent) {
errors.push("Subcategory needs a parent category column.");
}
}
if (config.kind === "llm") {
if (!config.model_alias.trim()) {
errors.push("Model alias is required.");
}
if (!config.prompt.trim()) {
errors.push("Prompt is required.");
}
if (config.llm_type === "code" && !config.code_lang) {
errors.push("Code language is required.");
}
if (config.llm_type === "structured") {
if (!config.output_format?.trim()) {
errors.push("Output format is required.");
} else {
try {
JSON.parse(config.output_format);
} catch {
errors.push("Output format must be valid JSON.");
}
}
}
}
if (config.kind === "expression") {
if (!config.expr.trim()) {
errors.push("Expression is required.");
}
}
return errors;
}

View file

@ -23,7 +23,7 @@ const DEFAULT_PROVIDER = {
const DEFAULT_CONFIG = {
provider: "openrouter",
model: "stepfun/step-3.5-flash:free",
model: "allenai/olmo-3.1-32b-instruct",
// biome-ignore lint/style/useNamingConvention: api schema
inference_parameters: {
temperature: 0.7,
@ -66,6 +66,29 @@ function parseNumber(value?: string): number | null {
return Number.isFinite(num) ? num : null;
}
function parseAgeRange(value?: string): [number, number] | null {
if (!value) {
return null;
}
const parts = value.split(/[^0-9.]+/).filter(Boolean);
if (parts.length !== 2) {
return null;
}
const min = Number(parts[0]);
const max = Number(parts[1]);
if (!Number.isFinite(min) || !Number.isFinite(max)) {
return null;
}
return [min, max];
}
function isValidSex(value?: string): value is "Male" | "Female" {
if (!value) {
return false;
}
return value === "Male" || value === "Female";
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: per type logic
function buildSamplerParams(
config: SamplerConfig,
@ -121,18 +144,37 @@ function buildSamplerParams(
format: config.uuid_format ?? undefined,
};
}
return {
locale: config.person_locale ?? undefined,
sex: config.person_sex ?? undefined,
const params: Record<string, unknown> = {};
if (config.person_locale?.trim()) {
params.locale = config.person_locale.trim();
}
if (config.sampler_type === "person") {
if (isValidSex(config.person_sex?.trim())) {
params.sex = config.person_sex?.trim();
} else if (config.person_sex?.trim()) {
errors.push(`Person ${config.name}: sex must be Male or Female.`);
}
} else if (config.person_sex?.trim()) {
params.sex = config.person_sex.trim();
}
if (config.person_city?.trim()) {
params.city = config.person_city.trim();
}
if (config.person_age_range?.trim()) {
const parsed = parseAgeRange(config.person_age_range);
if (parsed) {
// biome-ignore lint/style/useNamingConvention: api schema
params.age_range = parsed;
} else {
errors.push(`Person ${config.name}: age range must be like 18-70.`);
}
}
if (config.sampler_type === "person") {
// biome-ignore lint/style/useNamingConvention: api schema
age_range: config.person_age_range ?? undefined,
city: config.person_city ?? undefined,
// biome-ignore lint/style/useNamingConvention: api schema
with_synthetic_personas: config.person_with_synthetic_personas ?? undefined,
// biome-ignore lint/style/useNamingConvention: api schema
sample_dataset_when_available:
config.person_sample_dataset_when_available ?? undefined,
};
params.with_synthetic_personas =
config.person_with_synthetic_personas ?? undefined;
}
return params;
}
function buildLlmColumn(
@ -233,6 +275,8 @@ export function buildCanvasPayload(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: config.sampler_type,
params: buildSamplerParams(config, errors),
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: config.convert_to ?? undefined,
});
} else if (config.kind === "llm") {
columns.push(buildLlmColumn(config, errors));

View file

@ -0,0 +1,134 @@
import type { NodeConfig } from "../types";
function parseNumber(value?: string): number | null {
if (!value) {
return null;
}
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
function parseAgeRange(value?: string): [number, number] | null {
if (!value) {
return null;
}
const parts = value.split(/[^0-9.]+/).filter(Boolean);
if (parts.length !== 2) {
return null;
}
const min = Number(parts[0]);
const max = Number(parts[1]);
if (!Number.isFinite(min) || !Number.isFinite(max)) {
return null;
}
return [min, max];
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: validation rules
export function getConfigErrors(config: NodeConfig | null): string[] {
if (!config) {
return [];
}
const errors: string[] = [];
if (!config.name.trim()) {
errors.push("Name is required.");
}
if (config.kind === "sampler") {
if (config.sampler_type === "category") {
const values = config.values ?? [];
if (values.length < 2) {
errors.push("Category needs at least 2 values.");
}
const weights = config.weights ?? [];
const hasWeights = weights.some((weight) => weight !== null);
if (hasWeights && weights.some((weight) => weight === null)) {
errors.push("Weights must be set for all values.");
}
}
if (config.sampler_type === "uniform") {
const low = parseNumber(config.low);
const high = parseNumber(config.high);
if (low === null || high === null) {
errors.push("Uniform low/high must be numbers.");
} else if (low >= high) {
errors.push("Uniform low must be < high.");
}
}
if (config.sampler_type === "gaussian") {
const mean = parseNumber(config.mean);
const std = parseNumber(config.std);
if (mean === null || std === null) {
errors.push("Gaussian mean/std must be numbers.");
} else if (std <= 0) {
errors.push("Gaussian std must be > 0.");
}
}
if (config.sampler_type === "datetime") {
if (!config.datetime_unit) {
errors.push("Datetime unit required.");
}
if (config.datetime_start && config.datetime_end) {
const start = new Date(config.datetime_start).getTime();
const end = new Date(config.datetime_end).getTime();
if (!(Number.isFinite(start) && Number.isFinite(end))) {
errors.push("Datetime start/end must be valid.");
} else if (start >= end) {
errors.push("Datetime start must be before end.");
}
}
}
if (config.sampler_type === "subcategory" && !config.subcategory_parent) {
errors.push("Subcategory needs a parent category column.");
}
if (config.sampler_type === "person") {
if (config.person_sex?.trim()) {
const normalized = config.person_sex.trim();
if (!(normalized === "Male" || normalized === "Female")) {
errors.push("Person sex must be Male or Female.");
}
}
if (config.person_age_range?.trim()) {
const parsed = parseAgeRange(config.person_age_range);
if (!parsed) {
errors.push("Person age range must be like 18-70.");
}
}
}
if (config.sampler_type === "person_from_faker") {
if (config.person_age_range?.trim()) {
const parsed = parseAgeRange(config.person_age_range);
if (!parsed) {
errors.push("Person age range must be like 18-70.");
}
}
}
}
if (config.kind === "llm") {
if (!config.model_alias.trim()) {
errors.push("Model alias is required.");
}
if (!config.prompt.trim()) {
errors.push("Prompt is required.");
}
if (config.llm_type === "code" && !config.code_lang) {
errors.push("Code language is required.");
}
if (config.llm_type === "structured") {
if (!config.output_format?.trim()) {
errors.push("Output format is required.");
} else {
try {
JSON.parse(config.output_format);
} catch {
errors.push("Output format must be valid JSON.");
}
}
}
}
if (config.kind === "expression") {
if (!config.expr.trim()) {
errors.push("Expression is required.");
}
}
return errors;
}