refactor: remove Jinja autocomplete components and simplify variable handling

- Deleted `jinja-ref-autocomplete` components and related hooks.
- Replaced custom Jinja variable autocomplete with standard `Textarea` and `Input` components.
- Streamlined variable handling logic by replacing `getAvailableRefItems` with `getAvailableVariables`.
- Removed unused state (`flowMoving`) and redundant logic tied to Jinja-specific functionality.
This commit is contained in:
Shine1i 2026-02-20 10:36:51 +01:00
commit 763001b78e
9 changed files with 29 additions and 565 deletions

View file

@ -100,8 +100,6 @@ export function Navbar() {
{NAV_ITEMS.map((item) => {
const active =
pathname === item.href || pathname.startsWith(`${item.href}/`);
if (!item.enabled) {
const active = pathname === item.href;
const disabledByTraining =
isTrainingRunning && item.href !== "/studio";
if (!item.enabled || disabledByTraining) {

View file

@ -1,4 +1,5 @@
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
@ -9,8 +10,7 @@ import {
import type { ReactElement } from "react";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import type { ExpressionConfig, ExpressionDtype } from "../../types";
import { getAvailableRefItems } from "../../utils/variables";
import { JinjaRefInput } from "../jinja/jinja-ref-autocomplete";
import { getAvailableVariables } from "../../utils/variables";
import { InlineField } from "./inline-field";
type InlineExpressionProps = {
@ -25,9 +25,7 @@ export function InlineExpression({
onUpdate,
}: InlineExpressionProps): ReactElement {
const configs = useRecipeStudioStore((state) => state.configs);
const flowMoving = useRecipeStudioStore((state) => state.flowMoving);
const items = getAvailableRefItems(configs, config.id);
const vars = items.map((item) => item.ref);
const vars = getAvailableVariables(configs, config.id);
return (
<div className="space-y-3">
@ -52,13 +50,11 @@ export function InlineExpression({
</Select>
</InlineField>
<InlineField label="Expression">
<JinjaRefInput
<Input
className="nodrag h-8 w-full text-xs"
placeholder="{{ column_name }}"
value={config.expr}
items={items}
suppress={flowMoving}
onValueChange={(value) => onUpdate({ expr: value })}
onChange={(event) => onUpdate({ expr: event.target.value })}
/>
</InlineField>
</div>

View file

@ -1,463 +0,0 @@
import {
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import {
BalanceScaleIcon,
Clock01Icon,
CodeIcon,
CodeSimpleIcon,
EqualSignIcon,
FingerPrintIcon,
FunctionIcon,
Parabola02Icon,
PencilEdit02Icon,
Plant01Icon,
Tag01Icon,
TagsIcon,
UserAccountIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
type ChangeEvent,
type FocusEvent,
type ReactElement,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import type { AvailableRefItem } from "../../utils/variables";
type CaretAnchor = { x: number; y: number; height: number };
const MAX_RESULTS = 50;
function isInViewport(el: HTMLElement): boolean {
const boundsEl = el.closest(".react-flow") as HTMLElement | null;
const bounds = boundsEl?.getBoundingClientRect() ?? {
left: 0,
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
};
const rect = el.getBoundingClientRect();
return (
rect.bottom >= bounds.top &&
rect.top <= bounds.bottom &&
rect.right >= bounds.left &&
rect.left <= bounds.right
);
}
function getJinjaContext(
value: string,
cursor: number,
): { start: number; replaceEnd: number; query: string } | null {
if (cursor < 0) return null;
const openIdx = value.lastIndexOf("{{", Math.max(0, cursor - 1));
if (openIdx === -1) return null;
const closeIdx = value.indexOf("}}", openIdx + 2);
if (closeIdx !== -1 && closeIdx < cursor) return null;
return {
start: openIdx,
replaceEnd: closeIdx === -1 ? cursor : closeIdx + 2,
query: value.slice(openIdx + 2, cursor).trim(),
};
}
function getItemIcon(item: AvailableRefItem) {
if (item.kind === "expression") return FunctionIcon;
if (item.kind === "seed") return Plant01Icon;
if (item.kind === "llm") {
if (item.subtype === "structured") return CodeIcon;
if (item.subtype === "code") return CodeSimpleIcon;
if (item.subtype === "judge") return BalanceScaleIcon;
return PencilEdit02Icon;
}
if (item.subtype === "category") return Tag01Icon;
if (item.subtype === "subcategory") return TagsIcon;
if (item.subtype === "gaussian") return Parabola02Icon;
if (item.subtype === "uniform" || item.subtype === "bernoulli") return EqualSignIcon;
if (item.subtype === "datetime" || item.subtype === "timedelta") return Clock01Icon;
if (item.subtype === "uuid") return FingerPrintIcon;
if (item.subtype === "person" || item.subtype === "person_from_faker") return UserAccountIcon;
return Tag01Icon;
}
function useJinjaRefAutocomplete<T extends HTMLInputElement | HTMLTextAreaElement>(
value: string,
onValueChange: (value: string) => void,
items: AvailableRefItem[],
suppress: boolean,
) {
const fieldRef = useRef<T | null>(null);
const [focused, setFocused] = useState(false);
const [cursor, setCursor] = useState<number | null>(null);
const [anchor, setAnchor] = useState<CaretAnchor | null>(null);
const [inView, setInView] = useState(true);
const ctx = useMemo(() => {
if (!focused || cursor == null) return null;
return getJinjaContext(value, cursor);
}, [focused, cursor, value]);
const filtered = useMemo(() => {
if (!ctx) return [];
const q = ctx.query.toLowerCase();
const next = q
? items.filter((v) => v.ref.toLowerCase().includes(q))
: items.slice();
return next.slice(0, MAX_RESULTS);
}, [ctx, items]);
const open = !suppress && inView && Boolean(ctx && anchor) && items.length > 0;
const getCaretAnchor = useCallback((el: T, pos: number) => {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const mirror = document.createElement("div");
mirror.style.position = "fixed";
mirror.style.left = `${rect.left}px`;
mirror.style.top = `${rect.top}px`;
mirror.style.visibility = "hidden";
mirror.style.pointerEvents = "none";
mirror.style.whiteSpace = el instanceof HTMLTextAreaElement ? "pre-wrap" : "pre";
mirror.style.wordBreak = "break-word";
mirror.style.boxSizing = style.boxSizing;
mirror.style.width = `${rect.width}px`;
mirror.style.height = `${rect.height}px`;
mirror.style.overflow = "auto";
mirror.style.fontFamily = style.fontFamily;
mirror.style.fontSize = style.fontSize;
mirror.style.fontWeight = style.fontWeight;
mirror.style.letterSpacing = style.letterSpacing;
mirror.style.lineHeight = style.lineHeight;
mirror.style.padding = style.padding;
mirror.style.border = style.border;
mirror.style.textTransform = style.textTransform;
mirror.style.textIndent = style.textIndent;
const content = el.value ?? "";
const before = content.slice(0, pos);
const after = content.slice(pos) || ".";
mirror.textContent = before;
const span = document.createElement("span");
span.textContent = after;
mirror.appendChild(span);
document.body.appendChild(mirror);
mirror.scrollTop = (el as unknown as { scrollTop?: number }).scrollTop ?? 0;
mirror.scrollLeft = (el as unknown as { scrollLeft?: number }).scrollLeft ?? 0;
const spanRect = span.getBoundingClientRect();
document.body.removeChild(mirror);
let height = spanRect.height;
if (!Number.isFinite(height) || height <= 0) {
const lhRaw = style.lineHeight;
if (lhRaw && lhRaw !== "normal") {
height = Number.parseFloat(lhRaw);
} else {
height = Number.parseFloat(style.fontSize) * 1.2;
}
}
return {
x: spanRect.left - rect.left,
y: spanRect.top - rect.top,
height,
};
}, []);
const captureCursor = useCallback((el: T | null) => {
if (!el) return;
const pos = el.selectionStart;
if (typeof pos !== "number") {
setCursor(null);
setAnchor(null);
setInView(true);
return;
}
setCursor(pos);
setAnchor(getCaretAnchor(el, pos));
setInView(isInViewport(el));
}, [getCaretAnchor]);
useEffect(() => {
if (suppress) return;
if (!focused) return;
requestAnimationFrame(() => {
captureCursor(fieldRef.current);
});
}, [captureCursor, focused, suppress]);
const insertRef = useCallback(
(refName: string) => {
if (!ctx) return;
const replacement = `{{ ${refName} }}`;
const next =
value.slice(0, ctx.start) + replacement + value.slice(ctx.replaceEnd);
onValueChange(next);
const nextCursor = ctx.start + replacement.length;
requestAnimationFrame(() => {
const el = fieldRef.current;
if (!el) return;
el.focus();
el.setSelectionRange(nextCursor, nextCursor);
captureCursor(el);
});
},
[captureCursor, ctx, onValueChange, value],
);
const onFocus = useCallback(
(event: FocusEvent<T>) => {
setFocused(true);
captureCursor(event.currentTarget);
},
[captureCursor],
);
const onBlur = useCallback(() => {
setFocused(false);
}, []);
const onSelect = useCallback(
(event: React.SyntheticEvent<T>) => {
captureCursor(event.currentTarget);
},
[captureCursor],
);
return {
fieldRef,
open,
filtered,
insertRef,
onFocus,
onBlur,
onSelect,
captureCursor,
anchor,
};
}
function RefList({
items,
onPick,
}: {
items: AvailableRefItem[];
onPick: (value: string) => void;
}): ReactElement {
if (items.length === 0) {
return (
<div className="px-2 py-2 text-xs text-muted-foreground">
No matches
</div>
);
}
return (
<div className="max-h-64 overflow-auto p-1">
{items.map((item) => (
<button
key={item.ref}
type="button"
className="corner-squircle flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
onMouseDown={(e) => e.preventDefault()}
onClick={() => onPick(item.ref)}
>
<span className="corner-squircle flex size-8 shrink-0 items-center justify-center rounded-md border border-border/60 bg-muted/30">
<HugeiconsIcon icon={getItemIcon(item)} strokeWidth={2} className="size-4" />
</span>
<span className="min-w-0 flex-1 font-mono text-[13px]">
<span className="block truncate">{item.ref}</span>
</span>
<span className="corner-squircle shrink-0 rounded-md bg-muted/40 px-2 py-1 text-[11px] text-muted-foreground">
{item.valueType ?? `${item.kind}:${item.subtype}`}
</span>
</button>
))}
</div>
);
}
export function JinjaRefInput({
value,
onValueChange,
items,
suppress = false,
id,
placeholder,
className,
disabled,
}: {
value: string;
onValueChange: (value: string) => void;
items: AvailableRefItem[];
suppress?: boolean;
id?: string;
placeholder?: string;
className?: string;
disabled?: boolean;
}): ReactElement {
const {
fieldRef,
open,
filtered,
insertRef,
onFocus,
onBlur,
onSelect,
captureCursor,
anchor,
} = useJinjaRefAutocomplete<HTMLInputElement>(value, onValueChange, items, suppress);
const onChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
onValueChange(event.target.value);
captureCursor(event.target);
},
[captureCursor, onValueChange],
);
return (
<Popover open={open}>
<div className="relative">
<PopoverTrigger asChild={true}>
<Input
ref={fieldRef}
id={id}
disabled={disabled}
className={cn(className)}
placeholder={placeholder}
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
onSelect={onSelect}
/>
</PopoverTrigger>
{anchor && (
<PopoverAnchor asChild={true}>
<span
className="pointer-events-none absolute"
style={{
left: anchor.x,
top: anchor.y + anchor.height,
width: 1,
height: 1,
}}
/>
</PopoverAnchor>
)}
</div>
<PopoverContent
align="start"
side="bottom"
sideOffset={8}
className="corner-squircle nodrag nopan w-[360px] gap-0 rounded-xl p-1"
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
>
<RefList items={filtered} onPick={insertRef} />
</PopoverContent>
</Popover>
);
}
export function JinjaRefTextarea({
value,
onValueChange,
items,
suppress = false,
id,
placeholder,
className,
disabled,
}: {
value: string;
onValueChange: (value: string) => void;
items: AvailableRefItem[];
suppress?: boolean;
id?: string;
placeholder?: string;
className?: string;
disabled?: boolean;
}): ReactElement {
const {
fieldRef,
open,
filtered,
insertRef,
onFocus,
onBlur,
onSelect,
captureCursor,
anchor,
} = useJinjaRefAutocomplete<HTMLTextAreaElement>(value, onValueChange, items, suppress);
const onChange = useCallback(
(event: ChangeEvent<HTMLTextAreaElement>) => {
onValueChange(event.target.value);
captureCursor(event.target);
},
[captureCursor, onValueChange],
);
return (
<Popover open={open}>
<div className="relative">
<PopoverTrigger asChild={true}>
<Textarea
ref={fieldRef}
id={id}
disabled={disabled}
className={cn(className)}
placeholder={placeholder}
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
onSelect={onSelect}
/>
</PopoverTrigger>
{anchor && (
<PopoverAnchor asChild={true}>
<span
className="pointer-events-none absolute"
style={{
left: anchor.x,
top: anchor.y + anchor.height,
width: 1,
height: 1,
}}
/>
</PopoverAnchor>
)}
</div>
<PopoverContent
align="start"
side="bottom"
sideOffset={8}
className="corner-squircle nodrag nopan w-[300px] gap-0 rounded-xl p-1"
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
>
<RefList items={filtered} onPick={insertRef} />
</PopoverContent>
</Popover>
);
}

View file

@ -15,9 +15,8 @@ import { MAX_NODE_WIDTH, MIN_NODE_WIDTH } from "../constants";
import { useRecipeStudioStore } from "../stores/recipe-studio";
import type { LayoutDirection, LlmConfig, Score, ScoreOption } from "../types";
import { HANDLE_IDS } from "../utils/handles";
import { getAvailableRefItems, getAvailableVariables } from "../utils/variables";
import { getAvailableVariables } from "../utils/variables";
import { BaseNode, BaseNodeContent, BaseNodeHeader, BaseNodeHeaderTitle } from "./rf-ui/base-node";
import { JinjaRefTextarea } from "./jinja/jinja-ref-autocomplete";
type PromptField = "prompt" | "system_prompt";
@ -87,8 +86,6 @@ function AuxNodeBase({
data,
}: NodeProps<RecipeGraphAuxNodeType>): ReactElement | null {
const config = useRecipeStudioStore((state) => state.configs[data.llmId]);
const configs = useRecipeStudioStore((state) => state.configs);
const flowMoving = useRecipeStudioStore((state) => state.flowMoving);
const updateConfig = useRecipeStudioStore((state) => state.updateConfig);
const updateNodeInternals = useUpdateNodeInternals();
@ -105,7 +102,6 @@ function AuxNodeBase({
if (data.kind === "llm-prompt-input") {
const value = data.field === "prompt" ? config.prompt : config.system_prompt;
const items = getAvailableRefItems(configs, data.llmId);
return (
<BaseNode className="corner-squircle w-full min-w-0 rounded-lg border-border/60 bg-card shadow-sm">
<NodeResizer
@ -124,13 +120,13 @@ function AuxNodeBase({
<BaseNodeHeaderTitle className="text-xs">{data.title}</BaseNodeHeaderTitle>
</BaseNodeHeader>
<BaseNodeContent className="gap-2 px-3 py-2">
<JinjaRefTextarea
<Textarea
className="corner-squircle nodrag max-h-40 min-h-[88px] w-full resize-none overflow-y-auto text-xs"
value={value}
items={items}
suppress={flowMoving}
onValueChange={(next) =>
updateConfig(data.llmId, { [data.field]: next } as Partial<LlmConfig>)
onChange={(event) =>
updateConfig(data.llmId, {
[data.field]: event.target.value,
} as Partial<LlmConfig>)
}
/>
<AuxVariableBadges llmId={data.llmId} />

View file

@ -5,11 +5,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import type { ReactElement } from "react";
import type { ExpressionConfig, ExpressionDtype } from "../../types";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import { getAvailableRefItems } from "../../utils/variables";
import { JinjaRefTextarea } from "../../components/jinja/jinja-ref-autocomplete";
import { AvailableVariables } from "../shared/available-variables";
import { NameField } from "../shared/name-field";
@ -24,8 +22,6 @@ export function ExpressionDialog({
config,
onUpdate,
}: ExpressionDialogProps): ReactElement {
const configs = useRecipeStudioStore((state) => state.configs);
const items = getAvailableRefItems(configs, config.id);
const dtypeId = `${config.id}-dtype`;
const exprId = `${config.id}-expr`;
const updateField = <K extends keyof ExpressionConfig>(
@ -73,13 +69,12 @@ export function ExpressionDialog({
>
Expression (Jinja2)
</label>
<JinjaRefTextarea
<Textarea
id={exprId}
className="corner-squircle nodrag"
placeholder="{{ category_1 }} - {{ subcategory_1 }}"
value={config.expr}
items={items}
onValueChange={(value) => updateField("expr", value)}
onChange={(event) => updateField("expr", event.target.value)}
/>
<p className="text-xs text-muted-foreground">
Use Jinja2. Reference columns like {"{{ column_name }}"}.

View file

@ -16,9 +16,6 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { type ReactElement, type RefObject } from "react";
import type { LlmConfig } from "../../types";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import { getAvailableRefItems } from "../../utils/variables";
import { JinjaRefTextarea } from "../../components/jinja/jinja-ref-autocomplete";
import { AvailableVariables } from "../shared/available-variables";
import { NameField } from "../shared/name-field";
@ -54,8 +51,6 @@ export function LlmGeneralTab({
modelAliasAnchorRef,
onUpdate,
}: LlmGeneralTabProps): ReactElement {
const configs = useRecipeStudioStore((state) => state.configs);
const items = getAvailableRefItems(configs, config.id);
const modelAliasId = `${config.id}-model-alias`;
const codeLangId = `${config.id}-code-lang`;
const promptId = `${config.id}-prompt`;
@ -139,12 +134,11 @@ export function LlmGeneralTab({
>
Prompt
</label>
<JinjaRefTextarea
<Textarea
id={promptId}
className="corner-squircle nodrag"
value={config.prompt}
items={items}
onValueChange={(value) => onUpdate({ prompt: value })}
onChange={(event) => onUpdate({ prompt: event.target.value })}
/>
</div>
{config.llm_type === "structured" && (
@ -172,12 +166,11 @@ export function LlmGeneralTab({
>
System prompt (optional)
</label>
<JinjaRefTextarea
<Textarea
id={systemPromptId}
className="corner-squircle nodrag"
value={config.system_prompt}
items={items}
onValueChange={(value) => onUpdate({ system_prompt: value })}
onChange={(event) => onUpdate({ system_prompt: event.target.value })}
/>
</div>
</div>

View file

@ -115,7 +115,6 @@ export function RecipeStudioPage({
setAuxNodeSize,
syncAuxNodePositions,
syncAuxNodeSizes,
setFlowMoving,
} = useRecipeStudioStore(
useShallow((state) => ({
nodes: state.nodes,
@ -151,7 +150,6 @@ export function RecipeStudioPage({
setAuxNodeSize: state.setAuxNodeSize,
syncAuxNodePositions: state.syncAuxNodePositions,
syncAuxNodeSizes: state.syncAuxNodeSizes,
setFlowMoving: state.setFlowMoving,
})),
);
const [sheetContainer, setSheetContainer] = useState<HTMLDivElement | null>(
@ -333,10 +331,6 @@ export function RecipeStudioPage({
onConnect={onConnect}
onNodeClick={handleNodeClick}
isValidConnection={isValidConnection}
onMoveStart={() => setFlowMoving(true)}
onMoveEnd={() => setFlowMoving(false)}
onNodeDragStart={() => setFlowMoving(true)}
onNodeDragStop={() => setFlowMoving(false)}
nodesDraggable={interactive}
nodesConnectable={interactive}
elementsSelectable={interactive}

View file

@ -45,14 +45,12 @@ type RecipeStudioState = {
auxNodeSizes: Record<string, { width: number; height: number }>;
configs: Record<string, NodeConfig>;
processors: RecipeProcessorConfig[];
flowMoving: boolean;
sheetView: SheetView;
activeConfigId: string | null;
dialogOpen: boolean;
layoutDirection: LayoutDirection;
nextId: number;
nextY: number;
setFlowMoving: (moving: boolean) => void;
setSheetView: (view: SheetView) => void;
setProcessors: (processors: RecipeProcessorConfig[]) => void;
setDialogOpen: (open: boolean) => void;
@ -92,7 +90,6 @@ const INITIAL_STATE = {
auxNodeSizes: {},
configs: {},
processors: [],
flowMoving: false,
sheetView: "root",
activeConfigId: null,
dialogOpen: false,
@ -107,7 +104,6 @@ const INITIAL_STATE = {
| "auxNodeSizes"
| "configs"
| "processors"
| "flowMoving"
| "sheetView"
| "activeConfigId"
| "dialogOpen"
@ -133,7 +129,6 @@ function buildAddedNodeState(
export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
...INITIAL_STATE,
setFlowMoving: (moving) => set({ flowMoving: moving }),
setSheetView: (view) => set({ sheetView: view }),
setProcessors: (processors) => set({ processors }),
setDialogOpen: (open) => set({ dialogOpen: open }),
@ -209,7 +204,6 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
auxNodeSizes: {},
activeConfigId: null,
dialogOpen: false,
flowMoving: false,
sheetView: "root",
})),
setAuxNodePosition: (id, position) =>

View file

@ -1,39 +1,22 @@
import type { NodeConfig } from "../types";
export type AvailableRefItem = {
ref: string;
kind: Exclude<NodeConfig["kind"], "model_provider" | "model_config">;
subtype: string;
valueType?: string;
};
function getStructuredRefs(
llmName: string,
outputFormat: string,
): Array<{ ref: string; valueType?: string }> {
function getStructuredRefs(llmName: string, outputFormat: string): string[] {
try {
const schema = JSON.parse(outputFormat);
if (!(schema?.properties && typeof schema.properties === "object")) {
return [];
}
return Object.keys(schema.properties).map((key) => {
const prop = schema.properties[key];
const valueType =
prop && typeof prop === "object" && typeof prop.type === "string"
? prop.type
: undefined;
return { ref: `${llmName}.${key}`, valueType };
});
return Object.keys(schema.properties).map((key) => `${llmName}.${key}`);
} catch {
return [];
}
}
export function getAvailableRefItems(
export function getAvailableVariables(
configs: Record<string, NodeConfig>,
currentId: string,
): AvailableRefItem[] {
const items: AvailableRefItem[] = [];
): string[] {
const vars: string[] = [];
for (const config of Object.values(configs)) {
if (config.id === currentId) {
@ -44,20 +27,12 @@ export function getAvailableRefItems(
}
if (config.kind === "sampler") {
items.push({
ref: config.name,
kind: "sampler",
subtype: config.sampler_type,
});
vars.push(config.name);
continue;
}
if (config.kind === "expression") {
items.push({
ref: config.name,
kind: "expression",
subtype: config.dtype,
});
vars.push(config.name);
continue;
}
@ -65,7 +40,7 @@ export function getAvailableRefItems(
for (const col of config.seed_columns ?? []) {
const name = col.trim();
if (!name) continue;
items.push({ ref: name, kind: "seed", subtype: "seed" });
vars.push(name);
}
continue;
}
@ -74,26 +49,12 @@ export function getAvailableRefItems(
continue;
}
items.push({ ref: config.name, kind: "llm", subtype: config.llm_type });
vars.push(config.name);
if (config.llm_type !== "structured" || !config.output_format) {
continue;
}
for (const ref of getStructuredRefs(config.name, config.output_format)) {
items.push({
ref: ref.ref,
kind: "llm",
subtype: config.llm_type,
valueType: ref.valueType,
});
}
vars.push(...getStructuredRefs(config.name, config.output_format));
}
return items;
}
export function getAvailableVariables(
configs: Record<string, NodeConfig>,
currentId: string,
): string[] {
return getAvailableRefItems(configs, currentId).map((item) => item.ref);
return vars;
}