From 7ed6ad1e0c5ed8c98eaa0119bbc502124855eb89 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Thu, 26 Feb 2026 14:27:36 +0100 Subject: [PATCH] fix(recipe-studio): support user.* refs validation + toggle user badge details; style user refs/node amber --- .../components/recipe-graph-node.tsx | 10 ++- .../dialogs/shared/available-variables.tsx | 79 +++++++++++++++---- .../src/features/recipe-studio/utils/refs.ts | 15 +++- 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx b/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx index 4d1bbeedc5..0c046659e5 100644 --- a/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx +++ b/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx @@ -97,6 +97,8 @@ const NODE_META = { tone: "bg-orange-50 text-orange-600 border-orange-100", }, } as const; +const USER_NODE_TONE = + "bg-amber-50 text-amber-700 border-amber-100 dark:bg-amber-950/30 dark:text-amber-300 dark:border-amber-900/60"; const SAMPLER_ICONS: Record = { category: Tag01Icon, @@ -342,6 +344,12 @@ function RecipeGraphNodeBase({ (Boolean(config.prompt.trim()) || Boolean(config.system_prompt.trim()) || Boolean((config.scores?.length ?? 0) > 0)); + const iconTone = + config?.kind === "sampler" && + (config.sampler_type === "person" || + config.sampler_type === "person_from_faker") + ? USER_NODE_TONE + : meta.tone; return ( @@ -362,7 +370,7 @@ function RecipeGraphNodeBase({
diff --git a/studio/frontend/src/features/recipe-studio/dialogs/shared/available-variables.tsx b/studio/frontend/src/features/recipe-studio/dialogs/shared/available-variables.tsx index dea66a1442..b726541d53 100644 --- a/studio/frontend/src/features/recipe-studio/dialogs/shared/available-variables.tsx +++ b/studio/frontend/src/features/recipe-studio/dialogs/shared/available-variables.tsx @@ -1,5 +1,5 @@ import { Badge } from "@/components/ui/badge"; -import type { ReactElement } from "react"; +import { type ReactElement, useMemo, useState } from "react"; import { useRecipeStudioStore } from "../../stores/recipe-studio"; import { getAvailableVariableEntries } from "../../utils/variables"; @@ -7,11 +7,33 @@ type AvailableVariablesProps = { configId: string; }; +const USER_EXPANDED_FIELDS = [ + "first_name", + "last_name", + "sex", + "city", + "state", + "age", +] as const; +const USER_BADGE_CLASS = + "corner-squircle border-amber-500/25 bg-amber-500/10 font-mono text-[11px] text-amber-700 dark:text-amber-300"; + export function AvailableVariables({ configId, }: AvailableVariablesProps): ReactElement | null { + const [showUserFields, setShowUserFields] = useState(false); const configs = useRecipeStudioStore((state) => state.configs); const vars = getAvailableVariableEntries(configs, configId); + const variableNames = useMemo(() => new Set(vars.map((entry) => entry.name)), [vars]); + const hasUserRoot = variableNames.has("user"); + const userFieldEntries = useMemo( + () => + USER_EXPANDED_FIELDS.map((field) => ({ + source: "column" as const, + name: `user.${field}`, + })).filter((entry) => !variableNames.has(entry.name)), + [variableNames], + ); if (vars.length === 0) return null; @@ -21,19 +43,48 @@ export function AvailableVariables({ Available references

- {vars.map((v) => ( - - {`{{ ${v.name} }}`} - - ))} + {vars.map((v) => { + const className = + v.name === "user" || v.name.startsWith("user.") + ? USER_BADGE_CLASS + : v.source === "seed" + ? "corner-squircle border-blue-500/25 bg-blue-500/10 font-mono text-[11px] text-blue-700 dark:text-blue-300" + : "corner-squircle font-mono text-[11px]"; + if (v.name !== "user") { + return ( + + {`{{ ${v.name} }}`} + + ); + } + return ( + + ); + })} + {hasUserRoot && showUserFields && + userFieldEntries.map((entry) => ( + + {`{{ ${entry.name} }}`} + + ))}
); diff --git a/studio/frontend/src/features/recipe-studio/utils/refs.ts b/studio/frontend/src/features/recipe-studio/utils/refs.ts index 50f6c5b25f..50c5e3d7f7 100644 --- a/studio/frontend/src/features/recipe-studio/utils/refs.ts +++ b/studio/frontend/src/features/recipe-studio/utils/refs.ts @@ -2,6 +2,19 @@ const JINJA_REF_RE = /{{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g; const JINJA_EXPR_RE = /{{\s*([^{}]+?)\s*}}/g; const SIMPLE_JINJA_EXPR_RE = /^[a-zA-Z_][a-zA-Z0-9_.]*$/; const PLAIN_JINJA_EXPR_RE = /^[a-zA-Z0-9_.\s-]+$/; +const NESTED_REFERENCE_ROOTS = new Set(["user"]); + +function isValidNestedReference(expr: string, validSet: Set): boolean { + if (!expr.includes(".")) { + return false; + } + const parts = expr.split(".").map((part) => part.trim()).filter(Boolean); + if (parts.length < 2) { + return false; + } + const root = parts[0]; + return validSet.has(root) && NESTED_REFERENCE_ROOTS.has(root); +} function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -38,7 +51,7 @@ export function findInvalidJinjaReferences( continue; } if (SIMPLE_JINJA_EXPR_RE.test(expr)) { - if (!validSet.has(expr)) { + if (!validSet.has(expr) && !isValidNestedReference(expr, validSet)) { invalid.add(expr); } continue;