convert to, lines and fixes
This commit is contained in:
parent
4234e23f68
commit
fbb8adbab5
10 changed files with 398 additions and 174 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]>;
|
||||
|
|
|
|||
|
|
@ -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 } };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
134
studio/frontend/src/features/canvas-lab/utils/validation.ts
Normal file
134
studio/frontend/src/features/canvas-lab/utils/validation.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue