Skip to content

Commit

Permalink
enhance: add icon for agents (#1399)
Browse files Browse the repository at this point in the history
* enhance: add icon for agents

* add icon/agent name to chat interface

* add timestamp to message

* AvatarFallback to agent name's first letter

* Update AssistantIcon.svelte

* make icon in agent form smaller

* change location of agent images
  • Loading branch information
ivyjeong13 authored Jan 24, 2025
1 parent 76f464e commit 1337fea
Show file tree
Hide file tree
Showing 26 changed files with 859 additions and 104 deletions.
71 changes: 54 additions & 17 deletions ui/admin/app/components/agent/AgentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { AgentIcon } from "~/components/agent/icon/AgentIcon";
import {
ControlledAutosizeTextarea,
ControlledInput,
Expand All @@ -18,6 +19,14 @@ const formSchema = z.object({
description: z.string().optional(),
prompt: z.string().optional(),
model: z.string().optional(),
icons: z
.object({
icon: z.string(),
iconDark: z.string(),
collapsed: z.string(),
collapsedDark: z.string(),
})
.optional(),
});

export type AgentInfoFormValues = z.infer<typeof formSchema>;
Expand All @@ -26,9 +35,15 @@ type AgentFormProps = {
agent: AgentInfoFormValues;
onSubmit?: (values: AgentInfoFormValues) => void;
onChange?: (values: AgentInfoFormValues) => void;
hideImageField?: boolean;
};

export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) {
export function AgentForm({
agent,
onSubmit,
onChange,
hideImageField,
}: AgentFormProps) {
const form = useForm<AgentInfoFormValues>({
resolver: zodResolver(formSchema),
mode: "onChange",
Expand All @@ -37,6 +52,7 @@ export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) {
description: agent.description || "",
prompt: agent.prompt || "",
model: agent.model || "",
icons: agent.icons,
},
});

Expand All @@ -63,22 +79,20 @@ export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) {
return (
<Form {...form}>
<form onSubmit={handleSubmit} className="space-y-4">
<ControlledInput
variant="ghost"
autoComplete="off"
control={form.control}
name="name"
className="text-3xl"
/>

<ControlledInput
variant="ghost"
control={form.control}
autoComplete="off"
name="description"
placeholder="Add a description..."
className="text-xl text-muted-foreground"
/>
{hideImageField ? (
renderTitleDescription()
) : (
<div className="flex items-center justify-start gap-2">
<AgentIcon
name={agent.name}
icons={agent.icons}
onChange={(icons) => form.setValue("icons", icons)}
/>
<div className="flex flex-col gap-2">
{renderTitleDescription()}
</div>
</div>
)}

<h4 className="flex items-center gap-2 border-b pb-2">
<BrainIcon className="h-5 w-5" />
Expand All @@ -98,4 +112,27 @@ export function AgentForm({ agent, onSubmit, onChange }: AgentFormProps) {
</form>
</Form>
);

function renderTitleDescription() {
return (
<>
<ControlledInput
variant="ghost"
autoComplete="off"
control={form.control}
name="name"
className="text-3xl"
/>

<ControlledInput
variant="ghost"
control={form.control}
autoComplete="off"
name="description"
placeholder="Add a description..."
className="text-xl text-muted-foreground"
/>
</>
);
}
}
146 changes: 146 additions & 0 deletions ui/admin/app/components/agent/icon/AgentIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { EraserIcon, LinkIcon, PaintbrushIcon, PencilIcon } from "lucide-react";
import { useState } from "react";

import { AgentIcons } from "~/lib/model/agents";
import { AppTheme } from "~/lib/service/themeService";
import { cn } from "~/lib/utils/cn";

import { AgentImageUrl } from "~/components/agent/icon/AgentImageUrl";
import { useTheme } from "~/components/theme";
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";

const iconOptions = [
"obot_alt_1.svg",
"obot_alt_2.svg",
"obot_alt_3.svg",
"obot_alt_4.svg",
"obot_alt_5.svg",
"obot_alt_6.svg",
"obot_alt_7.svg",
"obot_alt_8.svg",
"obot_alt_9.svg",
"obot_alt_10.svg",
];

type AgentIconProps = {
icons?: AgentIcons;
onChange: (icons?: AgentIcons) => void;
name?: string;
};

export function AgentIcon({ icons, onChange, name }: AgentIconProps) {
const { theme } = useTheme();
const [imageUrlDialogOpen, setImageUrlDialogOpen] = useState(false);

const { icon = "", iconDark = "" } = icons ?? {};
const isDarkMode = theme === AppTheme.Dark;
return (
<>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-xl" className="group relative">
<Avatar className="size-20">
<AvatarImage
src={iconDark && isDarkMode ? iconDark : icon}
className={cn({
"dark:invert": !iconDark && isDarkMode,
})}
/>
<AvatarFallback className="text-[3.5rem] font-semibold">
{name?.charAt(0) ?? ""}
</AvatarFallback>
</Avatar>
<div className="absolute -right-1 top-0 items-center justify-center rounded-full bg-primary-foreground p-2 opacity-0 drop-shadow-md transition group-hover:opacity-100">
<PencilIcon className="!h-4 !w-4" />
</div>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Change Agent Icon</TooltipContent>
</Tooltip>
<DropdownMenuContent className="w-52" align="start">
<DropdownMenuSub>
<DropdownMenuSubTrigger className="flex items-center gap-2">
<PaintbrushIcon size={16} /> Select Icon
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{renderIconOptions()}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => setImageUrlDialogOpen(true)}
>
<LinkIcon size={16} /> Use Image URL
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
onChange(undefined);
}}
>
<EraserIcon size={16} /> Clear
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<AgentImageUrl
open={imageUrlDialogOpen}
onOpenChange={setImageUrlDialogOpen}
icons={icons}
onChange={onChange}
/>
</>
);

function renderIconOptions() {
return (
<div className="grid grid-cols-5 gap-2 p-2">
{iconOptions.map((icon) => (
<DropdownMenuItem
key={icon}
onClick={() => {
onChange({
icon: generateIconUrl(icon),
iconDark: "",
collapsed: "",
collapsedDark: "",
});
}}
>
<img
src={generateIconUrl(icon)}
alt="Agent Icon"
className={cn("h-8 w-8", {
"dark:invert": isDarkMode,
})}
/>
</DropdownMenuItem>
))}
</div>
);
}

function generateIconUrl(icon: string) {
return `${window.location.protocol}//${window.location.host}/agent/images/${icon}`;
}
}
Loading

0 comments on commit 1337fea

Please sign in to comment.