Skip to content

Commit

Permalink
feat: first model provider banners ux
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ivyjeong13 committed Dec 5, 2024
1 parent ab0b558 commit 3646007
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 93 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 { 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">
<TypographyP className="font-semibold text-2xl mb-0.5">
Ready to create your first Agent?
</TypographyP>
<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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,17 @@ export function DefaultModelAliasForm({
}
}

export function DefaultModelAliasFormDialog() {
export function DefaultModelAliasFormDialog({
disabled,
}: {
disabled?: boolean;
}) {
const [open, setOpen] = useState(false);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Default Model</Button>
<Button disabled={disabled}>Default Model</Button>
</DialogTrigger>

<DialogContent>
Expand Down
18 changes: 18 additions & 0 deletions ui/admin/app/components/model-providers/ModelProviderBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CircleAlert } from "lucide-react";

import { TypographyP } from "~/components/Typography";

export function ModelProviderBanner() {
return (
<div className="flex flex-row p-2 justify-start items-center w-[calc(100%-4rem)] rounded-sm bg-secondary relative overflow-hidden gap-2 max-w-screen-lg">
<CircleAlert className="text-warning" />
<div className="flex flex-col gap-1">
<TypographyP className="font-semibold text-xs">
To use Otto&apos;s features, you&apos;ll need to set up a
Model Provider. Select and configure one below to get
started!
</TypographyP>
</div>
</div>
);
}
56 changes: 38 additions & 18 deletions ui/admin/app/components/model-providers/ModelProviderConfigure.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BoxesIcon, SettingsIcon } from "lucide-react";
import { BoxesIcon } 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 { DefaultModelAliasForm } from "~/components/composed/DefaultModelAliasForm";
import { ModelProviderForm } from "~/components/model-providers/ModelProviderForm";
import { LoadingSpinner } from "~/components/ui/LoadingSpinner";
import { Button } from "~/components/ui/button";
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,8 +94,8 @@ export function ModelProviderConfigureContent({

return (
<>
<DialogHeader>
<DialogTitle className="mb-4 flex items-center gap-2">
<DialogHeader className="space-y-0 border-b-secondary border-b">
<DialogTitle className="flex items-center gap-2 px-6 py-4">
<BoxesIcon />{" "}
{modelProvider.configured
? `Configure ${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
135 changes: 94 additions & 41 deletions ui/admin/app/components/model-providers/ModelProviderForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@remix-run/react";
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,6 +15,7 @@ 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 { Separator } from "~/components/ui/separator";
Expand All @@ -20,6 +24,7 @@ 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 +43,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 +90,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 +145,79 @@ 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",
}}
<div className="flex flex-col">
<div className="flex flex-col gap-4 p-4 max-h-[50vh] overflow-y-auto">
<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
] ? (
<Button variant="ghost" size="icon">
<Link
to={
ModelProviderConfigurationLinks[
modelProvider.id
]
}
>
<CircleHelpIcon className="text-muted-foreground" />
</Link>
</Button>
) : null}
</div>
<NameDescriptionForm
defaultValues={form.watch(
"additionalConfirmParams"
)}
onChange={(values) =>
form.setValue("additionalConfirmParams", values)
}
/>
))}
</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)
}
/>
</>
) : null}
</div>

<div className="flex justify-end">
<div className="flex justify-end px-6 py-4 border-t border-t-secondary">
<Button
form={FORM_ID}
disabled={isLoading}
Expand Down
Loading

0 comments on commit 3646007

Please sign in to comment.