fix(recipe-studio): support user.* refs validation + toggle user badge details; style user refs/node amber
This commit is contained in:
parent
00a869f837
commit
7ed6ad1e0c
3 changed files with 88 additions and 16 deletions
|
|
@ -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<SamplerType, IconType> = {
|
||||
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 (
|
||||
<BaseNode className="corner-squircle relative w-full min-w-0 overflow-visible rounded-lg border-border/60 shadow-sm">
|
||||
|
|
@ -362,7 +370,7 @@ function RecipeGraphNodeBase({
|
|||
<div
|
||||
className={cn(
|
||||
"corner-squircle flex size-7 items-center justify-center rounded-md border",
|
||||
meta.tone,
|
||||
iconTone,
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={icon} className="size-3.5" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{vars.map((v) => (
|
||||
<Badge
|
||||
key={`${v.source}:${v.name}`}
|
||||
variant="secondary"
|
||||
className={
|
||||
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]"
|
||||
}
|
||||
>
|
||||
{`{{ ${v.name} }}`}
|
||||
</Badge>
|
||||
))}
|
||||
{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 (
|
||||
<Badge
|
||||
key={`${v.source}:${v.name}`}
|
||||
variant="secondary"
|
||||
className={className}
|
||||
>
|
||||
{`{{ ${v.name} }}`}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={`${v.source}:${v.name}`}
|
||||
type="button"
|
||||
onClick={() => setShowUserFields((prev) => !prev)}
|
||||
className="cursor-pointer"
|
||||
aria-expanded={showUserFields}
|
||||
>
|
||||
<Badge variant="secondary" className={className}>
|
||||
{`{{ ${v.name} }}`}
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{hasUserRoot && showUserFields &&
|
||||
userFieldEntries.map((entry) => (
|
||||
<Badge
|
||||
key={`user-expanded:${entry.name}`}
|
||||
variant="secondary"
|
||||
className={USER_BADGE_CLASS}
|
||||
>
|
||||
{`{{ ${entry.name} }}`}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<string>): 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue