From 763001b78e5d2ae8a78d573c52c97f82aa332bf8 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Fri, 20 Feb 2026 10:36:51 +0100 Subject: [PATCH] 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. --- studio/frontend/src/components/navbar.tsx | 2 - .../components/inline/inline-expression.tsx | 14 +- .../jinja/jinja-ref-autocomplete.tsx | 463 ------------------ .../components/recipe-graph-aux-node.tsx | 16 +- .../dialogs/expression/expression-dialog.tsx | 11 +- .../recipe-studio/dialogs/llm/general-tab.tsx | 15 +- .../recipe-studio/recipe-studio-page.tsx | 6 - .../recipe-studio/stores/recipe-studio.ts | 6 - .../features/recipe-studio/utils/variables.ts | 61 +-- 9 files changed, 29 insertions(+), 565 deletions(-) delete mode 100644 studio/frontend/src/features/recipe-studio/components/jinja/jinja-ref-autocomplete.tsx diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 06d5d25002..db293a80e1 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -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) { 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 90dd3c666b..6f754e04d6 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 @@ -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 (
@@ -52,13 +50,11 @@ export function InlineExpression({ - onUpdate({ expr: value })} + onChange={(event) => onUpdate({ expr: event.target.value })} />
diff --git a/studio/frontend/src/features/recipe-studio/components/jinja/jinja-ref-autocomplete.tsx b/studio/frontend/src/features/recipe-studio/components/jinja/jinja-ref-autocomplete.tsx deleted file mode 100644 index f8b0fb9e2b..0000000000 --- a/studio/frontend/src/features/recipe-studio/components/jinja/jinja-ref-autocomplete.tsx +++ /dev/null @@ -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( - value: string, - onValueChange: (value: string) => void, - items: AvailableRefItem[], - suppress: boolean, -) { - const fieldRef = useRef(null); - const [focused, setFocused] = useState(false); - const [cursor, setCursor] = useState(null); - const [anchor, setAnchor] = useState(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) => { - setFocused(true); - captureCursor(event.currentTarget); - }, - [captureCursor], - ); - - const onBlur = useCallback(() => { - setFocused(false); - }, []); - - const onSelect = useCallback( - (event: React.SyntheticEvent) => { - 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 ( -
- No matches -
- ); - } - - return ( -
- {items.map((item) => ( - - ))} -
- ); -} - -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(value, onValueChange, items, suppress); - - const onChange = useCallback( - (event: ChangeEvent) => { - onValueChange(event.target.value); - captureCursor(event.target); - }, - [captureCursor, onValueChange], - ); - - return ( - -
- - - - {anchor && ( - - - - )} -
- event.preventDefault()} - onCloseAutoFocus={(event) => event.preventDefault()} - > - - -
- ); -} - -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(value, onValueChange, items, suppress); - - const onChange = useCallback( - (event: ChangeEvent) => { - onValueChange(event.target.value); - captureCursor(event.target); - }, - [captureCursor, onValueChange], - ); - - return ( - -
- -