From aaf62095fefc06a3a41b4c62406166a57c640095 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Thu, 26 Feb 2026 13:35:07 +0100 Subject: [PATCH] feat(recipe-studio): add jinja ref validation UI for llm/expression fields --- .../components/inline/inline-expression.tsx | 6 +++ .../components/recipe-graph-aux-node.tsx | 19 +++++--- .../dialogs/expression/expression-dialog.tsx | 26 +++++++++++ .../recipe-studio/dialogs/llm/general-tab.tsx | 44 ++++++++++++++++++- .../src/features/recipe-studio/utils/refs.ts | 34 ++++++++++++++ 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx b/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx index 16a36d1ad5..206a068a78 100644 --- a/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx +++ b/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx @@ -9,6 +9,7 @@ import { import type { ReactElement } from "react"; import { useRecipeStudioStore } from "../../stores/recipe-studio"; import type { ExpressionConfig, ExpressionDtype } from "../../types"; +import { findInvalidJinjaReferences } from "../../utils/refs"; import { getAvailableVariableEntries } from "../../utils/variables"; import { AvailableReferencesInline } from "../shared/available-references-inline"; import { InlineField } from "./inline-field"; @@ -26,6 +27,10 @@ export function InlineExpression({ }: InlineExpressionProps): ReactElement { const configs = useRecipeStudioStore((state) => state.configs); const vars = getAvailableVariableEntries(configs, config.id); + const invalidRefs = findInvalidJinjaReferences( + config.expr, + vars.map((entry) => entry.name), + ); return (
@@ -52,6 +57,7 @@ export function InlineExpression({ 0} placeholder="{{ column_name }}" value={config.expr} onChange={(event) => onUpdate({ expr: event.target.value })} diff --git a/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx b/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx index 2aa1988102..bbb5dead41 100644 --- a/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx +++ b/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx @@ -13,6 +13,7 @@ import { useRecipeStudioStore } from "../stores/recipe-studio"; import type { LlmConfig, Score, ScoreOption } from "../types"; import { AUX_HANDLE_CLASS } from "../utils/handle-layout"; import { HANDLE_IDS } from "../utils/handles"; +import { findInvalidJinjaReferences } from "../utils/refs"; import { getAvailableVariableEntries } from "../utils/variables"; import { AvailableReferencesInline } from "./shared/available-references-inline"; import { BaseNode, BaseNodeContent, BaseNodeHeader, BaseNodeHeaderTitle } from "./rf-ui/base-node"; @@ -56,10 +57,12 @@ function updateOptionAt( ); } -function AuxVariableBadges({ llmId }: { llmId: string }): ReactElement | null { - const configs = useRecipeStudioStore((state) => state.configs); - const vars = getAvailableVariableEntries(configs, llmId); - return ; +function AuxVariableBadges({ + entries, +}: { + entries: ReturnType; +}): ReactElement | null { + return ; } function AuxNodeBase({ @@ -67,6 +70,7 @@ function AuxNodeBase({ data, }: NodeProps): ReactElement | null { const config = useRecipeStudioStore((state) => state.configs[data.llmId]); + const configs = useRecipeStudioStore((state) => state.configs); const updateConfig = useRecipeStudioStore((state) => state.updateConfig); const updateNodeInternals = useUpdateNodeInternals(); @@ -117,6 +121,10 @@ function AuxNodeBase({ if (data.kind === "llm-prompt-input") { const value = data.field === "prompt" ? config.prompt : config.system_prompt; + const variableEntries = getAvailableVariableEntries(configs, data.llmId); + const availableRefs = variableEntries.map((entry) => entry.name); + const hasInvalidRefs = + findInvalidJinjaReferences(value, availableRefs).length > 0; return ( @@ -125,6 +133,7 @@ function AuxNodeBase({