Skip to content

Commit

Permalink
feat: first model provider banner & model provider pages improvements (
Browse files Browse the repository at this point in the history
…#789)

* feat: first model provider banners ux

chore: add link to model provider site

chore: add appropriate links to model providers and otto azure doc

chore: add Default Model button & user friendly labels

feat: open default model aliases on config completion

feat: add icon to configure model header

Update ModelProviderIcon.tsx

* chore: nits & add shadcn alert component

* chore: remove border and slight padding fix on scroll area

* chore: styling fix again
  • Loading branch information
ivyjeong13 authored Dec 5, 2024
1 parent 9e69b19 commit 5a34236
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 113 deletions.
41 changes: 41 additions & 0 deletions ui/admin/app/components/agent/FirstModelProviderBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Link } from "@remix-run/react";
import { $path } from "remix-routes";

import { TypographyH3, TypographyP } from "~/components/Typography";
import { OttoLogo } from "~/components/branding/OttoLogo";
import { Button } from "~/components/ui/button";

export function FirstModelProviderBanner() {
return (
<div className="w-full">
<div className="flex justify-center w-full">
<div className="flex flex-row p-4 min-h-36 justify-end items-center w-[calc(100%-4rem)] rounded-sm mx-8 mt-4 bg-secondary relative overflow-hidden gap-4 max-w-screen-md">
<OttoLogo
hideText
classNames={{
root: "absolute opacity-45 top-[-5rem] left-[-7.5rem]",
image: "h-80 w-80",
}}
/>
<div className="flex flex-col pl-48">
<TypographyH3 className="mb-0.5">
Ready to create your first Agent?
</TypographyH3>
<TypographyP className="text-sm font-light mb-2">
You&apos;re almost there! To start creating or using{" "}
agents, you&apos;ll need access to a LLM (Large
Language Model) <b>Model Provider</b>. Luckily, we
support a variety of providers to help get you
started.
</TypographyP>
<Button className="mt-0 w-fit px-10">
<Link to={$path("/model-providers")}>
Get Started
</Link>
</Button>
</div>
</div>
</div>
</div>
);
}
58 changes: 39 additions & 19 deletions ui/admin/app/components/model-providers/ModelProviderConfigure.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { BoxesIcon, SettingsIcon } from "lucide-react";
import { useState } from "react";
import useSWR from "swr";

import { ModelProvider, ModelProviderConfig } from "~/lib/model/modelProviders";
import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService";

import { ModelProviderForm } from "~/components/model-providers/ModelProviderForm";
import { ModelProviderIcon } from "~/components/model-providers/ModelProviderIcon";
import { DefaultModelAliasForm } from "~/components/model/shared/DefaultModelAliasForm";
import { LoadingSpinner } from "~/components/ui/LoadingSpinner";
import { Button } from "~/components/ui/button";
import {
Expand All @@ -25,24 +26,45 @@ export function ModelProviderConfigure({
modelProvider,
}: ModelProviderConfigureProps) {
const [dialogIsOpen, setDialogIsOpen] = useState(false);
const [showDefaultModelAliasForm, setShowDefaultModelAliasForm] =
useState(false);

const handleDone = () => {
setDialogIsOpen(false);
setShowDefaultModelAliasForm(false);
};

return (
<Dialog open={dialogIsOpen} onOpenChange={setDialogIsOpen}>
<DialogTrigger asChild>
<Button size="icon" variant="ghost" className="mt-0">
<SettingsIcon />
<Button
variant={modelProvider.configured ? "secondary" : "accent"}
className="mt-0 w-full"
>
{modelProvider.configured ? "Modify" : "Configure"}
</Button>
</DialogTrigger>

<DialogDescription hidden>
Configure Model Provider
</DialogDescription>

<DialogContent>
<ModelProviderConfigureContent
modelProvider={modelProvider}
onSuccess={() => setDialogIsOpen(false)}
/>
<DialogContent className="p-0 gap-0">
{showDefaultModelAliasForm ? (
<div className="p-6">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 pb-4">
Configure Default Model Aliases
</DialogTitle>
</DialogHeader>
<DefaultModelAliasForm onSuccess={handleDone} />
</div>
) : (
<ModelProviderConfigureContent
modelProvider={modelProvider}
onSuccess={() => setShowDefaultModelAliasForm(true)}
/>
)}
</DialogContent>
</Dialog>
);
Expand Down Expand Up @@ -72,9 +94,9 @@ export function ModelProviderConfigureContent({

return (
<>
<DialogHeader>
<DialogTitle className="mb-4 flex items-center gap-2">
<BoxesIcon />{" "}
<DialogHeader className="space-y-0">
<DialogTitle className="flex items-center gap-2 px-6 py-4">
<ModelProviderIcon modelProvider={modelProvider} />{" "}
{modelProvider.configured
? `Configure ${modelProvider.name}`
: `Set Up ${modelProvider.name}`}
Expand All @@ -83,14 +105,12 @@ export function ModelProviderConfigureContent({
{revealModelProvider.isLoading ? (
<LoadingSpinner />
) : (
<>
<ModelProviderForm
modelProviderId={modelProvider.id}
onSuccess={handleSuccess}
parameters={parameters ?? {}}
requiredParameters={requiredParameters ?? []}
/>
</>
<ModelProviderForm
modelProvider={modelProvider}
onSuccess={handleSuccess}
parameters={parameters ?? {}}
requiredParameters={requiredParameters ?? []}
/>
)}
</>
);
Expand Down
148 changes: 105 additions & 43 deletions ui/admin/app/components/model-providers/ModelProviderForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircleHelpIcon } from "lucide-react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { mutate } from "swr";
import { z } from "zod";

import { ModelProviderConfig } from "~/lib/model/modelProviders";
import { ModelProvider, ModelProviderConfig } from "~/lib/model/modelProviders";
import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService";

import { TypographyH4 } from "~/components/Typography";
Expand All @@ -12,14 +14,18 @@ import {
ParamFormValues,
} from "~/components/composed/NameDescriptionForm";
import { ControlledInput } from "~/components/form/controlledInputs";
import { ModelProviderConfigurationLinks } from "~/components/model-providers/constants";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import { Link } from "~/components/ui/link";
import { ScrollArea } from "~/components/ui/scroll-area";
import { Separator } from "~/components/ui/separator";
import { useAsync } from "~/hooks/useAsync";

const formSchema = z.object({
requiredConfigParams: z.array(
z.object({
label: z.string(),
name: z.string().min(1, {
message: "Name is required.",
}),
Expand All @@ -38,11 +44,30 @@ const formSchema = z.object({

export type ModelProviderFormValues = z.infer<typeof formSchema>;

const translateUserFriendlyLabel = (label: string) => {
const fieldsToStrip = [
"OTTO8_OPENAI_MODEL_PROVIDER",
"OTTO8_AZURE_OPENAI_MODEL_PROVIDER",
"OTTO8_ANTHROPIC_MODEL_PROVIDER",
"OTTO8_OLLAMA_MODEL_PROVIDER",
"OTTO8_VOYAGE_MODEL_PROVIDER",
];

return fieldsToStrip
.reduce((acc, field) => {
return acc.replace(field, "");
}, label)
.toLowerCase()
.replace(/_/g, " ")
.replace(/\b\w/g, (char: string) => char.toUpperCase());
};

const getInitialRequiredParams = (
requiredParameters: string[],
parameters: ModelProviderConfig
): ModelProviderFormValues["requiredConfigParams"] =>
requiredParameters.map((requiredParameterKey) => ({
label: translateUserFriendlyLabel(requiredParameterKey),
name: requiredParameterKey,
value: parameters[requiredParameterKey] ?? "",
}));
Expand All @@ -66,21 +91,23 @@ const getInitialAdditionalParams = (
};

export function ModelProviderForm({
modelProviderId,
modelProvider,
onSuccess,
parameters,
requiredParameters,
}: {
modelProviderId: string;
modelProvider: ModelProvider;
onSuccess: (config: ModelProviderConfig) => void;
parameters: ModelProviderConfig;
requiredParameters: string[];
}) {
const configureModelProvider = useAsync(
ModelProviderApiService.configureModelProviderById,
{
onSuccess: () =>
mutate(ModelProviderApiService.getModelProviders.key()),
onSuccess: () => {
mutate(ModelProviderApiService.getModelProviders.key());
toast.success(`${modelProvider.name} configured successfully.`);
},
}
);

Expand Down Expand Up @@ -119,52 +146,87 @@ export function ModelProviderForm({
);

await configureModelProvider.execute(
modelProviderId,
modelProvider.id,
allConfigParams
);
onSuccess(allConfigParams);
}
);

const FORM_ID = "model-provider-form";
const showCustomConfiguration =
modelProvider.id === "azure-openai-model-provider";
return (
<div className="flex flex-col gap-4">
<TypographyH4 className="font-semibold text-md">
Required Configuration
</TypographyH4>
<Form {...form}>
<form
id={FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-8"
>
{requiredConfigParamFields.fields.map((field, i) => (
<ControlledInput
key={field.id}
label={field.name}
control={form.control}
name={`requiredConfigParams.${i}.value`}
classNames={{
wrapper: "flex-auto bg-background",
}}
/>
))}
</form>
</Form>

<Separator className="my-4" />

<TypographyH4 className="font-semibold text-md">
Custom Configuration (Optional)
</TypographyH4>
<NameDescriptionForm
defaultValues={form.watch("additionalConfirmParams")}
onChange={(values) =>
form.setValue("additionalConfirmParams", values)
}
/>

<div className="flex justify-end">
<div className="flex flex-col">
<ScrollArea className="max-h-[50vh]">
<div className="flex flex-col gap-4 p-4">
<TypographyH4 className="font-semibold text-md">
Required Configuration
</TypographyH4>
<Form {...form}>
<form
id={FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
{requiredConfigParamFields.fields.map(
(field, i) => (
<ControlledInput
key={field.id}
label={field.label}
control={form.control}
name={`requiredConfigParams.${i}.value`}
classNames={{
wrapper: "flex-auto bg-background",
}}
/>
)
)}
</form>
</Form>

{showCustomConfiguration ? (
<>
<Separator className="my-4" />

<div className="flex items-center gap-2">
<TypographyH4 className="font-semibold text-md">
Custom Configuration (Optional)
</TypographyH4>
{ModelProviderConfigurationLinks[
modelProvider.id
] ? (
<Link
as="button"
variant="ghost"
size="icon"
to={
ModelProviderConfigurationLinks[
modelProvider.id
]
}
>
<CircleHelpIcon className="text-muted-foreground" />
</Link>
) : null}
</div>
<NameDescriptionForm
defaultValues={form.watch(
"additionalConfirmParams"
)}
onChange={(values) =>
form.setValue(
"additionalConfirmParams",
values
)
}
/>
</>
) : null}
</div>
</ScrollArea>

<div className="flex justify-end px-6 py-4">
<Button
form={FORM_ID}
disabled={isLoading}
Expand Down
29 changes: 29 additions & 0 deletions ui/admin/app/components/model-providers/ModelProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { BoxesIcon } from "lucide-react";

import { ModelProvider } from "~/lib/model/modelProviders";
import { cn } from "~/lib/utils";

import { CommonModelProviderIds } from "~/components/model-providers/constants";

export function ModelProviderIcon({
modelProvider,
size = "md",
}: {
modelProvider: ModelProvider;
size?: "md" | "lg";
}) {
return modelProvider.icon ? (
<img
src={modelProvider.icon}
alt={modelProvider.name}
className={cn({
"w-6 h-6": size === "md",
"w-16 h-16": size === "lg",
"dark:invert":
modelProvider.id !== CommonModelProviderIds.AZURE_OPENAI,
})}
/>
) : (
<BoxesIcon className="w-16 h-16 color-primary" />
);
}
Loading

0 comments on commit 5a34236

Please sign in to comment.