model config and provider fixes and inline dialog
This commit is contained in:
parent
7350e52f2f
commit
2a9a332ce3
10 changed files with 506 additions and 41 deletions
|
|
@ -34,13 +34,17 @@ function DialogClose({
|
|||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
position = "fixed",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay> & {
|
||||
position?: "fixed" | "absolute";
|
||||
}) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50",
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 inset-0 isolate z-50",
|
||||
position === "fixed" ? "fixed" : "absolute",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -52,17 +56,29 @@ function DialogContent({
|
|||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
container,
|
||||
position = "fixed",
|
||||
overlayClassName,
|
||||
overlayPosition,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
container?: HTMLElement | null;
|
||||
position?: "fixed" | "absolute";
|
||||
overlayClassName?: string;
|
||||
overlayPosition?: "fixed" | "absolute";
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPortal container={container ?? undefined}>
|
||||
<DialogOverlay
|
||||
className={overlayClassName}
|
||||
position={overlayPosition ?? position}
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
|
||||
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
|
||||
position === "fixed" ? "fixed" : "absolute",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -325,11 +325,13 @@ export function CanvasLabPage(): ReactElement {
|
|||
config={config}
|
||||
categoryOptions={categoryOptions}
|
||||
onUpdate={updateConfig}
|
||||
container={sheetContainer}
|
||||
/>
|
||||
<ImportDialog
|
||||
open={importOpen}
|
||||
onOpenChange={setImportOpen}
|
||||
onImport={handleImport}
|
||||
container={sheetContainer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type ConfigDialogProps = {
|
|||
config: NodeConfig | null;
|
||||
categoryOptions: SamplerConfig[];
|
||||
onUpdate: (id: string, patch: Partial<NodeConfig>) => void;
|
||||
container?: HTMLDivElement | null;
|
||||
};
|
||||
|
||||
export function ConfigDialog({
|
||||
|
|
@ -20,10 +21,17 @@ export function ConfigDialog({
|
|||
config,
|
||||
categoryOptions,
|
||||
onUpdate,
|
||||
container,
|
||||
}: ConfigDialogProps): ReactElement {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogContent
|
||||
container={container}
|
||||
position="absolute"
|
||||
overlayPosition="absolute"
|
||||
overlayClassName="bg-transparent"
|
||||
className="sm:max-w-2xl shadow-border"
|
||||
>
|
||||
<DialogShell />
|
||||
{!config && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -7,29 +7,31 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { type ReactElement, useState } from "react";
|
||||
|
||||
type ImportDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onImport: (value: string) => string | null;
|
||||
container?: HTMLDivElement | null;
|
||||
};
|
||||
|
||||
export function ImportDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onImport,
|
||||
container,
|
||||
}: ImportDialogProps): ReactElement {
|
||||
const [value, setValue] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const payloadId = "canvas-import-payload";
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) {
|
||||
setValue("");
|
||||
setError(null);
|
||||
}
|
||||
}, [open]);
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const message = onImport(value);
|
||||
|
|
@ -37,12 +39,18 @@ export function ImportDialog({
|
|||
setError(message);
|
||||
return;
|
||||
}
|
||||
onOpenChange(false);
|
||||
handleOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[85vh] overflow-auto sm:max-w-2xl">
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
container={container}
|
||||
position="absolute"
|
||||
overlayPosition="absolute"
|
||||
overlayClassName="bg-transparent"
|
||||
className="max-h-[85vh] overflow-auto sm:max-w-2xl"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import recipe</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
} from "@/components/ui/combobox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -8,7 +16,8 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { ReactElement } from "react";
|
||||
import { type ReactElement, useMemo, useRef } from "react";
|
||||
import { useCanvasLabStore } from "../../stores/canvas-lab";
|
||||
import type { LlmConfig, Score, ScoreOption } from "../../types";
|
||||
import { NameField } from "../shared/name-field";
|
||||
|
||||
|
|
@ -37,11 +46,20 @@ type LlmDialogProps = {
|
|||
};
|
||||
|
||||
export function LlmDialog({ config, onUpdate }: LlmDialogProps): ReactElement {
|
||||
const configs = useCanvasLabStore((state) => state.configs);
|
||||
const modelConfigAliases = useMemo(
|
||||
() =>
|
||||
Object.values(configs)
|
||||
.filter((item) => item.kind === "model_config")
|
||||
.map((item) => item.name),
|
||||
[configs],
|
||||
);
|
||||
const modelAliasId = `${config.id}-model-alias`;
|
||||
const codeLangId = `${config.id}-code-lang`;
|
||||
const promptId = `${config.id}-prompt`;
|
||||
const outputFormatId = `${config.id}-output-format`;
|
||||
const systemPromptId = `${config.id}-system-prompt`;
|
||||
const modelAliasAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const scores = config.scores ?? [];
|
||||
const updateField = <K extends keyof LlmConfig>(
|
||||
key: K,
|
||||
|
|
@ -118,12 +136,37 @@ export function LlmDialog({ config, onUpdate }: LlmDialogProps): ReactElement {
|
|||
>
|
||||
Model alias
|
||||
</label>
|
||||
<Input
|
||||
id={modelAliasId}
|
||||
className="nodrag"
|
||||
value={config.model_alias}
|
||||
onChange={(event) => updateField("model_alias", event.target.value)}
|
||||
/>
|
||||
<div ref={modelAliasAnchorRef}>
|
||||
<Combobox
|
||||
items={modelConfigAliases}
|
||||
filteredItems={modelConfigAliases}
|
||||
filter={null}
|
||||
value={config.model_alias || null}
|
||||
onValueChange={(value) => updateField("model_alias", value ?? "")}
|
||||
onInputValueChange={(value) => updateField("model_alias", value)}
|
||||
itemToStringValue={(value) => value}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
id={modelAliasId}
|
||||
className="nodrag w-full"
|
||||
placeholder="Pick model alias or type"
|
||||
/>
|
||||
<ComboboxContent anchor={modelAliasAnchorRef}>
|
||||
<ComboboxEmpty>No model configs found</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(alias: string) => (
|
||||
<ComboboxItem key={alias} value={alias}>
|
||||
{alias}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Pick a model config alias. Matching node link becomes semantic.
|
||||
</p>
|
||||
</div>
|
||||
{config.llm_type === "code" && (
|
||||
<div className="grid gap-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
} from "@/components/ui/combobox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { ReactElement } from "react";
|
||||
import { type ReactElement, useMemo, useRef } from "react";
|
||||
import type { ModelConfig } from "../../types";
|
||||
import { useCanvasLabStore } from "../../stores/canvas-lab";
|
||||
import { NameField } from "../shared/name-field";
|
||||
|
|
@ -14,17 +22,20 @@ export function ModelConfigDialog({
|
|||
config,
|
||||
onUpdate,
|
||||
}: ModelConfigDialogProps): ReactElement {
|
||||
const providerOptions = useCanvasLabStore((state) =>
|
||||
Object.values(state.configs)
|
||||
.filter((item) => item.kind === "model_provider")
|
||||
.map((item) => item.name),
|
||||
const configs = useCanvasLabStore((state) => state.configs);
|
||||
const providerOptions = useMemo(
|
||||
() =>
|
||||
Object.values(configs)
|
||||
.filter((item) => item.kind === "model_provider")
|
||||
.map((item) => item.name),
|
||||
[configs],
|
||||
);
|
||||
const modelId = `${config.id}-model`;
|
||||
const providerId = `${config.id}-provider`;
|
||||
const providerListId = `${config.id}-provider-list`;
|
||||
const tempId = `${config.id}-temperature`;
|
||||
const topPId = `${config.id}-top-p`;
|
||||
const maxTokensId = `${config.id}-max-tokens`;
|
||||
const providerAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const updateField = <K extends keyof ModelConfig>(
|
||||
key: K,
|
||||
value: ModelConfig[K],
|
||||
|
|
@ -60,18 +71,37 @@ export function ModelConfigDialog({
|
|||
>
|
||||
Provider name
|
||||
</label>
|
||||
<Input
|
||||
id={providerId}
|
||||
className="nodrag"
|
||||
value={config.provider}
|
||||
list={providerListId}
|
||||
onChange={(event) => updateField("provider", event.target.value)}
|
||||
/>
|
||||
<datalist id={providerListId}>
|
||||
{providerOptions.map((provider) => (
|
||||
<option key={provider} value={provider} />
|
||||
))}
|
||||
</datalist>
|
||||
<div ref={providerAnchorRef}>
|
||||
<Combobox
|
||||
items={providerOptions}
|
||||
filteredItems={providerOptions}
|
||||
filter={null}
|
||||
value={config.provider || null}
|
||||
onValueChange={(value) => updateField("provider", value ?? "")}
|
||||
onInputValueChange={(value) => updateField("provider", value)}
|
||||
itemToStringValue={(value) => value}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
id={providerId}
|
||||
className="nodrag w-full"
|
||||
placeholder="Pick provider or type name"
|
||||
/>
|
||||
<ComboboxContent anchor={providerAnchorRef}>
|
||||
<ComboboxEmpty>No providers found</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(provider: string) => (
|
||||
<ComboboxItem key={provider} value={provider}>
|
||||
{provider}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Pick provider name from list. Matching node link becomes semantic.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -251,6 +251,37 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
|
|||
}
|
||||
}
|
||||
|
||||
const hasModelAliasPatch = Object.prototype.hasOwnProperty.call(
|
||||
patch,
|
||||
"model_alias",
|
||||
);
|
||||
if (current.kind === "llm" && hasModelAliasPatch) {
|
||||
const nextAlias =
|
||||
(patch as Partial<NodeConfig> & { model_alias?: string }).model_alias ?? "";
|
||||
edges = edges.filter((edge) => {
|
||||
if (edge.target !== id) {
|
||||
return true;
|
||||
}
|
||||
const source = configs[edge.source];
|
||||
return !(source && source.kind === "model_config");
|
||||
});
|
||||
if (nextAlias) {
|
||||
const modelConfigId = findNodeIdByName(configs, nextAlias);
|
||||
if (modelConfigId) {
|
||||
edges = addEdge(
|
||||
{
|
||||
source: modelConfigId,
|
||||
target: id,
|
||||
sourceHandle: null,
|
||||
targetHandle: null,
|
||||
type: "semantic",
|
||||
},
|
||||
edges,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCategoryConfig(current)) {
|
||||
const nextCategory = isCategoryConfig(next) ? next : current;
|
||||
const oldValues = current.values ?? [];
|
||||
|
|
|
|||
|
|
@ -2,6 +2,21 @@ import type { Edge } from "@xyflow/react";
|
|||
import type { NodeConfig } from "../../types";
|
||||
import { extractRefs } from "./helpers";
|
||||
|
||||
function isSemanticConnection(source: NodeConfig, target: NodeConfig): boolean {
|
||||
if (source.kind === "model_provider" && target.kind === "model_config") {
|
||||
return true;
|
||||
}
|
||||
if (source.kind === "model_config" && target.kind === "llm") {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
source.kind === "sampler" &&
|
||||
source.sampler_type === "category" &&
|
||||
target.kind === "sampler" &&
|
||||
target.sampler_type === "subcategory"
|
||||
);
|
||||
}
|
||||
|
||||
export function buildEdges(
|
||||
configs: NodeConfig[],
|
||||
nameToId: Map<string, string>,
|
||||
|
|
@ -9,6 +24,7 @@ export function buildEdges(
|
|||
): Edge[] {
|
||||
const edges: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
const configByName = new Map(configs.map((config) => [config.name, config]));
|
||||
const addEdgeByName = (from: string, to: string, type?: string) => {
|
||||
const sourceId = nameToId.get(from);
|
||||
const targetId = nameToId.get(to);
|
||||
|
|
@ -20,11 +36,17 @@ export function buildEdges(
|
|||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
const source = configByName.get(from);
|
||||
const target = configByName.get(to);
|
||||
const normalizedType =
|
||||
source && target && isSemanticConnection(source, target)
|
||||
? "semantic"
|
||||
: (type ?? "canvas");
|
||||
edges.push({
|
||||
id: `e-${key}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type: type ?? "canvas",
|
||||
type: normalizedType,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,24 @@ export type CanvasPayloadResult = {
|
|||
payload: CanvasPayload;
|
||||
};
|
||||
|
||||
function isSemanticRelation(
|
||||
source: NodeConfig,
|
||||
target: NodeConfig,
|
||||
): boolean {
|
||||
if (source.kind === "model_provider" && target.kind === "model_config") {
|
||||
return true;
|
||||
}
|
||||
if (source.kind === "model_config" && target.kind === "llm") {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
source.kind === "sampler" &&
|
||||
source.sampler_type === "category" &&
|
||||
target.kind === "sampler" &&
|
||||
target.sampler_type === "subcategory"
|
||||
);
|
||||
}
|
||||
|
||||
function parseNumber(value?: string): number | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
|
|
@ -485,7 +503,10 @@ export function buildCanvasPayload(
|
|||
{
|
||||
from: source.name,
|
||||
to: target.name,
|
||||
type: edge.type ?? "canvas",
|
||||
type:
|
||||
edge.type === "semantic" || isSemanticRelation(source, target)
|
||||
? "semantic"
|
||||
: "canvas",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue