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:
parent
6dd0e11439
commit
763001b78e
9 changed files with 29 additions and 565 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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 }}"}.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue