Skip to content

Commit

Permalink
fix: sort by name, model provider & filter for agent's model dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
ivyjeong13 committed Dec 17, 2024
1 parent 88e0781 commit a26950c
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 31 deletions.
66 changes: 36 additions & 30 deletions ui/admin/app/components/agent/AgentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,17 @@ import { useForm } from "react-hook-form";
import useSWR from "swr";
import { z } from "zod";

import { ModelUsage } from "~/lib/model/models";
import { Model, ModelUsage } from "~/lib/model/models";
import { ModelApiService } from "~/lib/service/api/modelApiService";

import { TypographyH4 } from "~/components/Typography";
import { ComboBox } from "~/components/composed/ComboBox";
import {
ControlledAutosizeTextarea,
ControlledCustomInput,
ControlledInput,
} from "~/components/form/controlledInputs";
import { Form } from "~/components/ui/form";
import {
Select,
SelectContent,
SelectEmptyItem,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";

const formSchema = z.object({
name: z.string().min(1, {
Expand Down Expand Up @@ -86,6 +79,7 @@ export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) {
onSubmit?.({ ...agent, ...values })
);

const modelOptionsByGroup = getModelOptionsByModelProvider(models);
return (
<Form {...form}>
<form onSubmit={handleSubmit} className="space-y-4">
Expand Down Expand Up @@ -125,30 +119,42 @@ export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) {
name="model"
>
{({ field: { ref: _, ...field } }) => (
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Use System Default" />
</SelectTrigger>

<SelectContent>
<SelectEmptyItem>
Use System Default
</SelectEmptyItem>

{models.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.name || m.id}
{" - "}
<span className="text-muted-foreground">
{m.modelProvider}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<ComboBox
allowClear
clearLabel="Use System Default"
placeholder="Use System Default"
value={models.find((m) => m.id === field.value)}
onChange={(value) =>
field.onChange(value?.id ?? "")
}
options={modelOptionsByGroup}
/>
)}
</ControlledCustomInput>
</form>
</Form>
);

function getModelOptionsByModelProvider(models: Model[]) {
const byModelProviderGroups = models.reduce(
(acc, model) => {
acc[model.modelProvider] = acc[model.modelProvider] || [];
acc[model.modelProvider].push(model);
return acc;
},
{} as Record<string, Model[]>
);

return Object.entries(byModelProviderGroups).map(
([modelProvider, models]) => {
const sorted = models.sort((a, b) =>
(a.name ?? "").localeCompare(b.name ?? "")
);
return {
heading: modelProvider,
value: sorted,
};
}
);
}
}
178 changes: 178 additions & 0 deletions ui/admin/app/components/composed/ComboBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use client";

import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { ReactNode, useState } from "react";

import { Button } from "~/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import { Drawer, DrawerContent, DrawerTrigger } from "~/components/ui/drawer";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { useIsMobile } from "~/hooks/use-mobile";

type BaseOption = {
id: string;
name?: string | undefined;
};

type GroupedOption<T extends BaseOption> = {
heading: string;
value: T[];
};

type ComboBoxProps<T extends BaseOption> = {
allowClear?: boolean;
clearLabel?: ReactNode;
onChange: (option: T | null) => void;
options: T[] | GroupedOption<T>[];
placeholder?: string;
value?: T | null;
};

export function ComboBox<T extends BaseOption>({
disabled,
placeholder,
value,
...props
}: {
disabled?: boolean;
} & ComboBoxProps<T>) {
const [open, setOpen] = useState(false);
const isMobile = useIsMobile();

if (!isMobile) {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{renderButtonContent()}</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<ComboBoxList setOpen={setOpen} value={value} {...props} />
</PopoverContent>
</Popover>
);
}

return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>{renderButtonContent()}</DrawerTrigger>
<DrawerContent>
<div className="mt-4 border-t">
<ComboBoxList setOpen={setOpen} value={value} {...props} />
</div>
</DrawerContent>
</Drawer>
);

function renderButtonContent() {
return (
<Button
disabled={disabled}
endContent={<ChevronsUpDownIcon />}
variant="outline"
className="px-3 w-full font-normal justify-start rounded-sm"
classNames={{
content: "w-full justify-between",
}}
>
<span className="text-ellipsis overflow-hidden">
{value ? value.name : placeholder}
</span>
</Button>
);
}
}

function ComboBoxList<T extends BaseOption>({
allowClear,
clearLabel,
onChange,
options,
placeholder = "Filter...",
setOpen,
value,
}: { setOpen: (open: boolean) => void } & ComboBoxProps<T>) {
const isGrouped = options.every((option) => "heading" in option);
return (
<Command>
<CommandInput placeholder={placeholder} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{allowClear && (
<CommandGroup>
<CommandItem
onSelect={() => {
onChange(null);
setOpen(false);
}}
>
{clearLabel ?? "Clear Selection"}
</CommandItem>
</CommandGroup>
)}
{isGrouped
? renderGroupedOptions(options)
: renderUngroupedOptions(options)}
</CommandList>
</Command>
);

function renderGroupedOptions(items: GroupedOption<T>[]) {
return items.map((group) => (
<CommandGroup key={group.heading} heading={group.heading}>
{group.value.map((option) => (
<CommandItem
key={option.id}
value={option.name}
onSelect={(name) => {
const match =
group.value.find((opt) => opt.name === name) ||
null;
onChange(match);
setOpen(false);
}}
className="justify-between"
>
{option.name || option.id}{" "}
{value?.id === option.id && (
<CheckIcon className="w-4 h-4" />
)}
</CommandItem>
))}
</CommandGroup>
));
}

function renderUngroupedOptions(items: T[]) {
return (
<CommandGroup>
{items.map((option) => (
<CommandItem
key={option.id}
value={option.name}
onSelect={(name) => {
const match =
items.find((opt) => opt.name === name) || null;
onChange(match);
setOpen(false);
}}
className="justify-between"
>
{option.name || option.id}{" "}
{value?.id === option.id && (
<CheckIcon className="w-4 h-4" />
)}
</CommandItem>
))}
</CommandGroup>
);
}
}
11 changes: 10 additions & 1 deletion ui/admin/app/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
loading?: boolean;
startContent?: React.ReactNode;
endContent?: React.ReactNode;
classNames?: {
content?: string;
};
};

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
Expand All @@ -64,6 +67,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
startContent,
endContent,
children,
classNames,
...props
},
ref
Expand Down Expand Up @@ -93,7 +97,12 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{endContent}
</div>
) : (
<div className="flex items-center gap-2">
<div
className={cn(
"flex items-center gap-2",
classNames?.content
)}
>
{startContent}
{children}
{endContent}
Expand Down
Loading

0 comments on commit a26950c

Please sign in to comment.