refactor: streamline samplers and block handling, update dialogs and validation
This commit is contained in:
parent
0d2f81ab3d
commit
e3c3bf75a0
17 changed files with 371 additions and 257 deletions
|
|
@ -72,7 +72,7 @@ export type BlockDefinition = {
|
|||
export const BLOCK_GROUPS: BlockGroup[] = [
|
||||
{
|
||||
kind: "sampler",
|
||||
title: "Sampler",
|
||||
title: "Samplers",
|
||||
description: "Numeric + categorical blocks.",
|
||||
icon: DiceFaces03Icon,
|
||||
},
|
||||
|
|
@ -84,8 +84,8 @@ export const BLOCK_GROUPS: BlockGroup[] = [
|
|||
},
|
||||
{
|
||||
kind: "llm",
|
||||
title: "LLM",
|
||||
description: "Text + structured blocks.",
|
||||
title: "LLM + Models",
|
||||
description: "Generation, providers, and model aliases.",
|
||||
icon: PencilEdit02Icon,
|
||||
},
|
||||
{
|
||||
|
|
@ -182,7 +182,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
kind: "sampler",
|
||||
type: "person",
|
||||
title: "Person",
|
||||
description: "Synthetic person sampler.",
|
||||
description: "Faker person sampler.",
|
||||
icon: UserAccountIcon,
|
||||
dialogKey: "person",
|
||||
createConfig: (id, existing) => makeSamplerConfig(id, "person", existing),
|
||||
|
|
@ -227,7 +227,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
kind: "llm",
|
||||
type: "model_provider",
|
||||
title: "Model Provider",
|
||||
description: "Configure API endpoint + key.",
|
||||
description: "Endpoint, auth, and provider settings.",
|
||||
icon: Shield02Icon,
|
||||
dialogKey: "model_provider",
|
||||
createConfig: (id, existing) => makeModelProviderConfig(id, existing),
|
||||
|
|
@ -236,7 +236,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
kind: "llm",
|
||||
type: "model_config",
|
||||
title: "Model Config",
|
||||
description: "Alias + model + inference params.",
|
||||
description: "Alias, model, provider, and inference params.",
|
||||
icon: Plant01Icon,
|
||||
dialogKey: "model_config",
|
||||
createConfig: (id, existing) => makeModelConfig(id, existing),
|
||||
|
|
@ -291,4 +291,3 @@ export function getBlockDefinitionForConfig(
|
|||
}
|
||||
return getBlockDefinition("expression", "expression");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export function renderBlockDialog(
|
|||
<LlmDialog
|
||||
config={config}
|
||||
modelConfigAliases={modelConfigAliases}
|
||||
modelProviderOptions={modelProviderOptions}
|
||||
onUpdate={update}
|
||||
/>
|
||||
) : null;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ type BlockSheetProps = {
|
|||
container: HTMLDivElement | null;
|
||||
sheetView: SheetView;
|
||||
onViewChange: (sheetView: SheetView) => void;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onAddSampler: (type: SamplerType) => void;
|
||||
onAddSeed: () => void;
|
||||
onAddLlm: (type: LlmType) => void;
|
||||
|
|
@ -128,6 +130,8 @@ export function BlockSheet({
|
|||
container,
|
||||
sheetView,
|
||||
onViewChange,
|
||||
open,
|
||||
onOpenChange,
|
||||
onAddSampler,
|
||||
onAddSeed,
|
||||
onAddLlm,
|
||||
|
|
@ -140,16 +144,26 @@ export function BlockSheet({
|
|||
onImport,
|
||||
}: BlockSheetProps): ReactElement {
|
||||
const sheetTitle = getSheetTitle(sheetView);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||
const expressionBlocks = useMemo(() => getBlocksForKind("expression"), []);
|
||||
const seedBlocks = useMemo(() => getBlocksForKind("seed"), []);
|
||||
const isControlled = typeof open === "boolean";
|
||||
const sheetOpen = isControlled ? (open as boolean) : uncontrolledOpen;
|
||||
|
||||
const setSheetOpen = (nextOpen: boolean) => {
|
||||
if (!isControlled) {
|
||||
setUncontrolledOpen(nextOpen);
|
||||
}
|
||||
onOpenChange?.(nextOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<Sheet
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
if (open) {
|
||||
open={sheetOpen}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setSheetOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
onViewChange("root");
|
||||
}
|
||||
}}
|
||||
|
|
@ -201,17 +215,17 @@ export function BlockSheet({
|
|||
isActive={index === 0}
|
||||
onClick={() => {
|
||||
if (item.kind === "processor") {
|
||||
setOpen(false);
|
||||
setSheetOpen(false);
|
||||
onOpenProcessors();
|
||||
return;
|
||||
}
|
||||
if (item.kind === "seed" && seedBlocks.length === 1) {
|
||||
setOpen(false);
|
||||
setSheetOpen(false);
|
||||
onAddSeed();
|
||||
return;
|
||||
}
|
||||
if (item.kind === "expression" && expressionBlocks.length === 1) {
|
||||
setOpen(false);
|
||||
setSheetOpen(false);
|
||||
onAddExpression();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,13 +253,13 @@ function LlmInputHandles({ items, isTopBottom }: LlmInputHandlesProps): ReactEle
|
|||
key={item.id}
|
||||
className="pointer-events-none relative flex min-w-[80px] flex-1 justify-center pt-2"
|
||||
>
|
||||
<Handle
|
||||
id={item.id}
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="pointer-events-auto !size-2 !border-border !bg-background"
|
||||
style={{ left: "50%", top: 0, transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
<Handle
|
||||
id={item.id}
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"
|
||||
style={{ left: "50%", top: 0, transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -275,7 +275,7 @@ function LlmInputHandles({ items, isTopBottom }: LlmInputHandlesProps): ReactEle
|
|||
id={item.id}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="pointer-events-auto !size-2 !border-border !bg-background"
|
||||
className="pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"
|
||||
style={{ left: -3, top: "50%", transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
<span className="block truncate text-[10px] text-muted-foreground">
|
||||
|
|
@ -383,7 +383,7 @@ function RecipeGraphNodeBase({
|
|||
position={dataInPosition}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
labelClassName="sr-only"
|
||||
handleClassName="pointer-events-auto !size-2 !border-border !bg-background"
|
||||
handleClassName="pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataOut}
|
||||
|
|
@ -392,7 +392,7 @@ function RecipeGraphNodeBase({
|
|||
position={dataOutPosition}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
labelClassName="sr-only"
|
||||
handleClassName="pointer-events-auto !size-2 !border-border !bg-background"
|
||||
handleClassName="pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -405,7 +405,7 @@ function RecipeGraphNodeBase({
|
|||
position={semanticInPosition}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
labelClassName="sr-only"
|
||||
handleClassName="pointer-events-auto !size-2 !border-border !bg-background"
|
||||
handleClassName="pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ function RecipeGraphNodeBase({
|
|||
position={semanticOutPosition}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
labelClassName="sr-only"
|
||||
handleClassName="pointer-events-auto !size-2 !border-border !bg-background"
|
||||
handleClassName="pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20"
|
||||
/>
|
||||
)}
|
||||
</BaseNode>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const RecipeGraphSemanticEdge = memo(function RecipeGraphSemanticEdge({
|
|||
targetPosition,
|
||||
style,
|
||||
markerEnd,
|
||||
selected,
|
||||
}: EdgeProps): ReactElement {
|
||||
const [path] = getSmoothStepPath({
|
||||
sourceX,
|
||||
|
|
@ -29,9 +30,12 @@ export const RecipeGraphSemanticEdge = memo(function RecipeGraphSemanticEdge({
|
|||
path={path}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
strokeDasharray: "4 4",
|
||||
strokeWidth: 1.5,
|
||||
stroke: "var(--muted-foreground)",
|
||||
strokeDasharray: selected ? "7 5" : "6 5",
|
||||
strokeWidth: selected ? 2.3 : 1.8,
|
||||
stroke: selected
|
||||
? "hsl(var(--primary) / 0.9)"
|
||||
: "hsl(var(--foreground) / 0.38)",
|
||||
opacity: selected ? 1 : 0.92,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export function BaseHandle({
|
|||
<Handle
|
||||
{...props}
|
||||
className={cn(
|
||||
"dark:border-secondary dark:bg-secondary h-[11px] w-[11px] rounded-full border border-slate-300 bg-slate-100 transition",
|
||||
"h-[12px] w-[12px] rounded-full border border-border/80 bg-muted shadow-[0_0_0_1px_hsl(var(--background))] transition-all hover:scale-110 hover:border-primary/70 hover:bg-primary/20",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export function DataEdge({
|
|||
data = { path: "auto" },
|
||||
id,
|
||||
markerEnd,
|
||||
selected,
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
|
|
@ -44,8 +45,17 @@ export function DataEdge({
|
|||
targetPosition,
|
||||
});
|
||||
|
||||
const edgeStyle = {
|
||||
stroke: selected
|
||||
? "hsl(var(--primary) / 0.92)"
|
||||
: "hsl(var(--foreground) / 0.42)",
|
||||
strokeWidth: selected ? 2.6 : 2.1,
|
||||
opacity: selected ? 1 : 0.92,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseEdge id={id} path={edgePath} markerEnd={markerEnd} style={style} />
|
||||
<BaseEdge id={id} path={edgePath} markerEnd={markerEnd} style={edgeStyle} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Dialog, DialogContent, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { ReactElement } from "react";
|
||||
import { getBlockDefinitionForConfig } from "../blocks/definitions";
|
||||
import { renderBlockDialog } from "../blocks/registry";
|
||||
import type { NodeConfig, SamplerConfig } from "../types";
|
||||
import { DialogShell } from "./shared/dialog-shell";
|
||||
|
|
@ -30,6 +31,8 @@ export function ConfigDialog({
|
|||
onUpdate,
|
||||
container,
|
||||
}: ConfigDialogProps): ReactElement {
|
||||
const blockDefinition = getBlockDefinitionForConfig(config);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
|
|
@ -39,7 +42,14 @@ export function ConfigDialog({
|
|||
overlayClassName="bg-transparent"
|
||||
className="corner-squircle max-h-[650px] overflow-auto sm:max-w-2xl shadow-border"
|
||||
>
|
||||
<DialogShell />
|
||||
<DialogShell
|
||||
title={blockDefinition ? `${blockDefinition.title} block` : undefined}
|
||||
description={
|
||||
blockDefinition
|
||||
? blockDefinition.description
|
||||
: "Adjust block params before running the flow."
|
||||
}
|
||||
/>
|
||||
{!config && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Select a node to edit.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ const CODE_LANG_OPTIONS = [
|
|||
type LlmGeneralTabProps = {
|
||||
config: LlmConfig;
|
||||
modelConfigAliases: string[];
|
||||
modelProviderOptions: string[];
|
||||
modelAliasAnchorRef: RefObject<HTMLDivElement | null>;
|
||||
onUpdate: (patch: Partial<LlmConfig>) => void;
|
||||
};
|
||||
|
|
@ -48,6 +49,7 @@ type LlmGeneralTabProps = {
|
|||
export function LlmGeneralTab({
|
||||
config,
|
||||
modelConfigAliases,
|
||||
modelProviderOptions,
|
||||
modelAliasAnchorRef,
|
||||
onUpdate,
|
||||
}: LlmGeneralTabProps): ReactElement {
|
||||
|
|
@ -56,11 +58,22 @@ export function LlmGeneralTab({
|
|||
const promptId = `${config.id}-prompt`;
|
||||
const outputFormatId = `${config.id}-output-format`;
|
||||
const systemPromptId = `${config.id}-system-prompt`;
|
||||
const hasModelConfigs = modelConfigAliases.length > 0;
|
||||
const hasModelProviders = modelProviderOptions.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<AvailableVariables configId={config.id} />
|
||||
<NameField value={config.name} onChange={(value) => onUpdate({ name: value })} />
|
||||
{(!hasModelConfigs || !hasModelProviders) && (
|
||||
<div className="rounded-2xl border border-border/60 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
<p className="font-semibold text-foreground">Setup hint</p>
|
||||
<p>
|
||||
{!hasModelProviders && "Add a Model Provider block. "}
|
||||
{!hasModelConfigs && "Add a Model Config block and pick its alias here."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ import { LlmScoresTab } from "./scores-tab";
|
|||
type LlmDialogProps = {
|
||||
config: LlmConfig;
|
||||
modelConfigAliases: string[];
|
||||
modelProviderOptions: string[];
|
||||
onUpdate: (patch: Partial<LlmConfig>) => void;
|
||||
};
|
||||
|
||||
export function LlmDialog({
|
||||
config,
|
||||
modelConfigAliases,
|
||||
modelProviderOptions,
|
||||
onUpdate,
|
||||
}: LlmDialogProps): ReactElement {
|
||||
const modelAliasAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -34,6 +36,7 @@ export function LlmDialog({
|
|||
<LlmGeneralTab
|
||||
config={config}
|
||||
modelConfigAliases={modelConfigAliases}
|
||||
modelProviderOptions={modelProviderOptions}
|
||||
modelAliasAnchorRef={modelAliasAnchorRef}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import type { SamplerConfig } from "../../types";
|
||||
|
|
@ -10,14 +15,38 @@ type CategoryDialogProps = {
|
|||
onUpdate: (patch: Partial<SamplerConfig>) => void;
|
||||
};
|
||||
|
||||
function addChipWithWeight(
|
||||
values: string[] | undefined,
|
||||
weights: Array<number | null> | undefined,
|
||||
value: string,
|
||||
): { values: string[]; weights: Array<number | null> } {
|
||||
return {
|
||||
values: [...(values ?? []), value],
|
||||
weights: [...(weights ?? []), null],
|
||||
};
|
||||
}
|
||||
|
||||
function removeChipWithWeight(
|
||||
values: string[] | undefined,
|
||||
weights: Array<number | null> | undefined,
|
||||
index: number,
|
||||
): { values: string[]; weights: Array<number | null> } {
|
||||
const nextValues = [...(values ?? [])];
|
||||
const nextWeights = [...(weights ?? [])];
|
||||
nextValues.splice(index, 1);
|
||||
nextWeights.splice(index, 1);
|
||||
return { values: nextValues, weights: nextWeights };
|
||||
}
|
||||
|
||||
export function CategoryDialog({
|
||||
config,
|
||||
onUpdate,
|
||||
}: CategoryDialogProps): ReactElement {
|
||||
const [conditionDraft, setConditionDraft] = useState("");
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const conditionInputId = `${config.id}-conditional-rule`;
|
||||
|
||||
const conditional = config.conditional_params ?? {};
|
||||
const conditionalCount = Object.keys(conditional).length;
|
||||
|
||||
const handleAddCondition = () => {
|
||||
const condition = conditionDraft.trim();
|
||||
|
|
@ -62,164 +91,200 @@ export function CategoryDialog({
|
|||
<ChipInput
|
||||
values={config.values ?? []}
|
||||
onAdd={(value) => {
|
||||
const values = [...(config.values ?? []), value];
|
||||
const weights = [...(config.weights ?? []), null];
|
||||
const { values, weights } = addChipWithWeight(
|
||||
config.values,
|
||||
config.weights,
|
||||
value,
|
||||
);
|
||||
onUpdate({ values, weights });
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
const values = [...(config.values ?? [])];
|
||||
const weights = [...(config.weights ?? [])];
|
||||
values.splice(index, 1);
|
||||
weights.splice(index, 1);
|
||||
const { values, weights } = removeChipWithWeight(
|
||||
config.values,
|
||||
config.weights,
|
||||
index,
|
||||
);
|
||||
onUpdate({ values, weights });
|
||||
}}
|
||||
placeholder="Type a value and press Enter"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Weights (optional)
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{(config.values ?? []).map((value, index) => (
|
||||
<div key={`${value}-weight`} className="flex items-center gap-3">
|
||||
<span className="text-xs text-muted-foreground max-w-20 truncate">
|
||||
{value}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag w-full"
|
||||
placeholder="Weight"
|
||||
value={config.weights?.[index] ?? ""}
|
||||
onChange={(event) => {
|
||||
const weights = [...(config.weights ?? [])];
|
||||
weights[index] = event.target.value
|
||||
? Number(event.target.value)
|
||||
: null;
|
||||
onUpdate({ weights });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-2xl border border-border/60 p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Conditional params (category)
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{Object.keys(conditional).length} rules
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id={conditionInputId}
|
||||
className="nodrag"
|
||||
placeholder="Condition (e.g., {{ region }} == 'US')"
|
||||
value={conditionDraft}
|
||||
onChange={(event) => setConditionDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
handleAddCondition();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="button" size="sm" onClick={handleAddCondition}>
|
||||
Add rule
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(conditional).map(([condition, params]) => (
|
||||
<div
|
||||
key={condition}
|
||||
className="space-y-3 rounded-2xl border border-border/60 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold text-foreground">{condition}</p>
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => removeCondition(condition)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<ChipInput
|
||||
values={params.values ?? []}
|
||||
onAdd={(value) => {
|
||||
const values = [...(params.values ?? []), value];
|
||||
const weights = [...(params.weights ?? []), null];
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
conditional_params: {
|
||||
...conditional,
|
||||
[condition]: { ...params, values, weights },
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
const values = [...(params.values ?? [])];
|
||||
const weights = [...(params.weights ?? [])];
|
||||
values.splice(index, 1);
|
||||
weights.splice(index, 1);
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
conditional_params: {
|
||||
...conditional,
|
||||
[condition]: { ...params, values, weights },
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="Type a conditional value and press Enter"
|
||||
/>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<div className="rounded-2xl border border-border/60">
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-3 py-2 text-left"
|
||||
>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Advanced
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Weights and conditional rules.
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{advancedOpen ? "Hide" : "Show"}
|
||||
</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t border-border/60 p-3">
|
||||
<div className="grid gap-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Rule weights (optional)
|
||||
Weights (optional)
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{(params.values ?? []).map((value, index) => (
|
||||
<div
|
||||
key={`${condition}-${value}-${index}-weight`}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground w-28 truncate">
|
||||
{value}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag"
|
||||
placeholder="Weight"
|
||||
value={params.weights?.[index] ?? ""}
|
||||
onChange={(event) => {
|
||||
const weights = [
|
||||
...(params.weights ??
|
||||
Array.from(
|
||||
{ length: (params.values ?? []).length },
|
||||
() => null,
|
||||
)),
|
||||
];
|
||||
weights[index] = event.target.value
|
||||
? Number(event.target.value)
|
||||
: null;
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
conditional_params: {
|
||||
...conditional,
|
||||
[condition]: { ...params, weights },
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{(config.values ?? []).length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Add values first, then set optional weights.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
{(config.values ?? []).map((value, index) => (
|
||||
<div key={`${value}-weight`} className="flex items-center gap-3">
|
||||
<span className="max-w-20 truncate text-xs text-muted-foreground">
|
||||
{value}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag w-full"
|
||||
placeholder="Weight"
|
||||
value={config.weights?.[index] ?? ""}
|
||||
onChange={(event) => {
|
||||
const weights = [...(config.weights ?? [])];
|
||||
weights[index] = event.target.value
|
||||
? Number(event.target.value)
|
||||
: null;
|
||||
onUpdate({ weights });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Conditional params (category)
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{conditionalCount} rules
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id={conditionInputId}
|
||||
className="nodrag"
|
||||
placeholder="Condition (e.g., {{ region }} == 'US')"
|
||||
value={conditionDraft}
|
||||
onChange={(event) => setConditionDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
handleAddCondition();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="button" size="sm" onClick={handleAddCondition}>
|
||||
Add rule
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(conditional).map(([condition, params]) => (
|
||||
<div
|
||||
key={condition}
|
||||
className="space-y-3 rounded-2xl border border-border/60 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold text-foreground">{condition}</p>
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => removeCondition(condition)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<ChipInput
|
||||
values={params.values ?? []}
|
||||
onAdd={(value) => {
|
||||
const { values, weights } = addChipWithWeight(
|
||||
params.values,
|
||||
params.weights,
|
||||
value,
|
||||
);
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
conditional_params: {
|
||||
...conditional,
|
||||
[condition]: { ...params, values, weights },
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
const { values, weights } = removeChipWithWeight(
|
||||
params.values,
|
||||
params.weights,
|
||||
index,
|
||||
);
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
conditional_params: {
|
||||
...conditional,
|
||||
[condition]: { ...params, values, weights },
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="Type a conditional value and press Enter"
|
||||
/>
|
||||
<div className="grid gap-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Rule weights (optional)
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{(params.values ?? []).map((value, index) => (
|
||||
<div
|
||||
key={`${condition}-${value}-${index}-weight`}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<span className="w-28 truncate text-xs text-muted-foreground">
|
||||
{value}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="nodrag"
|
||||
placeholder="Weight"
|
||||
value={params.weights?.[index] ?? ""}
|
||||
onChange={(event) => {
|
||||
const weights = [
|
||||
...(params.weights ??
|
||||
Array.from(
|
||||
{ length: (params.values ?? []).length },
|
||||
() => null,
|
||||
)),
|
||||
];
|
||||
weights[index] = event.target.value
|
||||
? Number(event.target.value)
|
||||
: null;
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
conditional_params: {
|
||||
...conditional,
|
||||
[condition]: { ...params, weights },
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { ReactElement } from "react";
|
||||
import { type ReactElement, useEffect } from "react";
|
||||
import type { SamplerConfig } from "../../types";
|
||||
import { NameField } from "../shared/name-field";
|
||||
|
||||
|
|
@ -24,13 +23,23 @@ export function PersonDialog({
|
|||
const sexId = `${config.id}-person-sex`;
|
||||
const ageRangeId = `${config.id}-person-age-range`;
|
||||
const cityId = `${config.id}-person-city`;
|
||||
const sourceId = `${config.id}-person-source`;
|
||||
|
||||
const updateField = <K extends keyof SamplerConfig>(
|
||||
key: K,
|
||||
value: SamplerConfig[K],
|
||||
) => {
|
||||
onUpdate({ [key]: value } as Partial<SamplerConfig>);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (config.sampler_type !== "person_from_faker") {
|
||||
onUpdate({
|
||||
sampler_type: "person_from_faker",
|
||||
person_with_synthetic_personas: undefined,
|
||||
});
|
||||
}
|
||||
}, [config.sampler_type, onUpdate]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<NameField
|
||||
|
|
@ -38,30 +47,11 @@ export function PersonDialog({
|
|||
onChange={(value) => onUpdate({ name: value })}
|
||||
/>
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={sourceId}
|
||||
>
|
||||
<div className="rounded-2xl border border-border/60 px-3 py-2">
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Source
|
||||
</label>
|
||||
<Select
|
||||
value={config.sampler_type === "person_from_faker" ? "faker" : "person"}
|
||||
onValueChange={(value) =>
|
||||
updateField(
|
||||
"sampler_type",
|
||||
value === "faker" ? "person_from_faker" : "person",
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={sourceId}>
|
||||
<SelectValue placeholder="Select source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="person">Managed dataset</SelectItem>
|
||||
<SelectItem value="faker">Faker</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</p>
|
||||
<p className="text-sm text-foreground">Faker</p>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
|
|
@ -137,22 +127,6 @@ export function PersonDialog({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{config.sampler_type === "person" && (
|
||||
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-2">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Synthetic personas</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Generate persona profiles.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.person_with_synthetic_personas ?? false}
|
||||
onCheckedChange={(value) =>
|
||||
updateField("person_with_synthetic_personas", value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,19 @@ import {
|
|||
} from "@/components/ui/dialog";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
export function DialogShell(): ReactElement {
|
||||
type DialogShellProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export function DialogShell({
|
||||
title = "Configure block",
|
||||
description = "Adjust block params before running the flow.",
|
||||
}: DialogShellProps): ReactElement {
|
||||
return (
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configure block</DialogTitle>
|
||||
<DialogDescription>
|
||||
Adjust block params before running the flow.
|
||||
</DialogDescription>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
Panel,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { PlusSignIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
type ReactElement,
|
||||
useCallback,
|
||||
|
|
@ -157,6 +159,7 @@ export function RecipeStudioPage({
|
|||
const [sheetContainer, setSheetContainer] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [blockSheetOpen, setBlockSheetOpen] = useState(false);
|
||||
const [activeView, setActiveView] = useState<RecipeStudioView>("editor");
|
||||
const [processorsOpen, setProcessorsOpen] = useState(false);
|
||||
const [interactive, setInteractive] = useState(true);
|
||||
|
|
@ -316,6 +319,11 @@ export function RecipeStudioPage({
|
|||
setProcessorsOpen(true);
|
||||
}, [processors, setProcessors]);
|
||||
|
||||
const openRootBlockSheet = useCallback(() => {
|
||||
setSheetView("root");
|
||||
setBlockSheetOpen(true);
|
||||
}, [setSheetView]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<main className="w-full px-6 py-8">
|
||||
|
|
@ -375,11 +383,37 @@ export function RecipeStudioPage({
|
|||
size={1}
|
||||
color="#d4d4d8"
|
||||
/>
|
||||
{nodes.length === 0 && (
|
||||
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openRootBlockSheet}
|
||||
className="pointer-events-auto corner-squircle flex min-h-36 w-full max-w-md flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-border/70 bg-background/75 px-6 py-6 text-center backdrop-blur-[1px] transition hover:border-primary/60 hover:bg-background"
|
||||
>
|
||||
<div className="flex size-12 items-center justify-center rounded-xl border border-border/70 bg-muted/40">
|
||||
<HugeiconsIcon
|
||||
icon={PlusSignIcon}
|
||||
className="size-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
Add your first block
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click to open block library.
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Panel position="top-right" className="m-3">
|
||||
<BlockSheet
|
||||
container={sheetContainer}
|
||||
sheetView={sheetView}
|
||||
onViewChange={setSheetView}
|
||||
open={blockSheetOpen}
|
||||
onOpenChange={setBlockSheetOpen}
|
||||
onAddSampler={addSamplerNode}
|
||||
onAddSeed={addSeedNode}
|
||||
onAddLlm={addLlmNode}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export function makeSamplerConfig(
|
|||
sampler_type: "category",
|
||||
name,
|
||||
drop: false,
|
||||
values: ["A", "B", "C"],
|
||||
weights: [null, null, null],
|
||||
values: [],
|
||||
weights: [],
|
||||
};
|
||||
}
|
||||
if (samplerType === "subcategory") {
|
||||
|
|
@ -42,12 +42,7 @@ export function makeSamplerConfig(
|
|||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
subcategory_parent: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
subcategory_mapping: {
|
||||
// biome-ignore lint/style/useNamingConvention: sample values
|
||||
A: ["A1", "A2"],
|
||||
// biome-ignore lint/style/useNamingConvention: sample values
|
||||
B: ["B1", "B2"],
|
||||
},
|
||||
subcategory_mapping: {},
|
||||
};
|
||||
}
|
||||
if (samplerType === "uniform") {
|
||||
|
|
@ -131,7 +126,7 @@ export function makeSamplerConfig(
|
|||
uuid_format: "",
|
||||
};
|
||||
}
|
||||
if (samplerType === "person_from_faker") {
|
||||
if (samplerType === "person" || samplerType === "person_from_faker") {
|
||||
return {
|
||||
id,
|
||||
kind: "sampler",
|
||||
|
|
@ -153,7 +148,7 @@ export function makeSamplerConfig(
|
|||
id,
|
||||
kind: "sampler",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
sampler_type: "person",
|
||||
sampler_type: "person_from_faker",
|
||||
name,
|
||||
drop: false,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
|
|
@ -164,8 +159,6 @@ export function makeSamplerConfig(
|
|||
person_age_range: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
person_city: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
person_with_synthetic_personas: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +184,7 @@ export function makeLlmConfig(
|
|||
name,
|
||||
drop: false,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
model_alias: "allenai/olmo-3.1-32b-instruct",
|
||||
model_alias: "",
|
||||
prompt:
|
||||
llmType === "judge"
|
||||
? "Evaluate the content using the scoring criteria below."
|
||||
|
|
|
|||
|
|
@ -153,14 +153,12 @@ function buildSamplerParams(
|
|||
if (config.person_locale?.trim()) {
|
||||
params.locale = config.person_locale.trim();
|
||||
}
|
||||
if (config.sampler_type === "person") {
|
||||
if (isValidSex(config.person_sex?.trim())) {
|
||||
params.sex = config.person_sex?.trim();
|
||||
} else if (config.person_sex?.trim()) {
|
||||
if (config.person_sex?.trim()) {
|
||||
if (isValidSex(config.person_sex.trim())) {
|
||||
params.sex = config.person_sex.trim();
|
||||
} else {
|
||||
errors.push(`Person ${config.name}: sex must be Male or Female.`);
|
||||
}
|
||||
} else if (config.person_sex?.trim()) {
|
||||
params.sex = config.person_sex.trim();
|
||||
}
|
||||
if (config.person_city?.trim()) {
|
||||
params.city = config.person_city.trim();
|
||||
|
|
@ -174,11 +172,6 @@ function buildSamplerParams(
|
|||
errors.push(`Person ${config.name}: age range must be like 18-70.`);
|
||||
}
|
||||
}
|
||||
if (config.sampler_type === "person") {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
params.with_synthetic_personas =
|
||||
config.person_with_synthetic_personas ?? undefined;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,10 @@ export function getConfigErrors(config: NodeConfig | null): string[] {
|
|||
if (config.sampler_type === "subcategory" && !config.subcategory_parent) {
|
||||
errors.push("Subcategory needs a parent category column.");
|
||||
}
|
||||
if (config.sampler_type === "person") {
|
||||
if (
|
||||
config.sampler_type === "person" ||
|
||||
config.sampler_type === "person_from_faker"
|
||||
) {
|
||||
if (config.person_sex?.trim()) {
|
||||
const normalized = config.person_sex.trim();
|
||||
if (!isValidSex(normalized)) {
|
||||
|
|
@ -122,14 +125,6 @@ export function getConfigErrors(config: NodeConfig | null): string[] {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (config.sampler_type === "person_from_faker") {
|
||||
if (config.person_age_range?.trim()) {
|
||||
const parsed = parseAgeRange(config.person_age_range);
|
||||
if (!parsed) {
|
||||
errors.push("Person age range must be like 18-70.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.kind === "llm") {
|
||||
if (!config.model_alias.trim()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue