Skip to content
Snippets Groups Projects
Unverified Commit 9c3014de authored by Sean Hatfield's avatar Sean Hatfield Committed by GitHub
Browse files

[FEAT] Agent skills UI redesign (#1565)


* WIP agent settings redesign

* WIP rework new agent skill UI

* WIP save bar/agent styles

* WIP update settings fix

* desktop agent config UI implementation

* remove unneeded files

* fix sql and web browsing plugins not starting & add default badges

* fix serply merge conflict

* review: cleanup unused files/folders/components

* refactor components

* refactor components

* fix order of customized skills

---------

Co-authored-by: default avatartimothycarambat <rambat1010@gmail.com>
parent c74ba350
No related branches found
No related tags found
No related merge requests found
Showing
with 241 additions and 11 deletions
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
"target": "esnext", "target": "esnext",
"jsx": "react", "jsx": "react",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
} }
} }
\ No newline at end of file
...@@ -21,6 +21,7 @@ const AdminInvites = lazy(() => import("@/pages/Admin/Invitations")); ...@@ -21,6 +21,7 @@ const AdminInvites = lazy(() => import("@/pages/Admin/Invitations"));
const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces")); const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces"));
const AdminSystem = lazy(() => import("@/pages/Admin/System")); const AdminSystem = lazy(() => import("@/pages/Admin/System"));
const AdminLogs = lazy(() => import("@/pages/Admin/Logging")); const AdminLogs = lazy(() => import("@/pages/Admin/Logging"));
const AdminAgents = lazy(() => import("@/pages/Admin/Agents"));
const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats")); const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats"));
const GeneralAppearance = lazy( const GeneralAppearance = lazy(
() => import("@/pages/GeneralSettings/Appearance") () => import("@/pages/GeneralSettings/Appearance")
...@@ -106,6 +107,10 @@ export default function App() { ...@@ -106,6 +107,10 @@ export default function App() {
path="/settings/vector-database" path="/settings/vector-database"
element={<AdminRoute Component={GeneralVectorDatabase} />} element={<AdminRoute Component={GeneralVectorDatabase} />}
/> />
<Route
path="/settings/agents"
element={<AdminRoute Component={AdminAgents} />}
/>
<Route <Route
path="/settings/event-logs" path="/settings/event-logs"
element={<AdminRoute Component={AdminLogs} />} element={<AdminRoute Component={AdminLogs} />}
......
import { Warning } from "@phosphor-icons/react";
export default function ContextualSaveBar({
showing = false,
onSave,
onCancel,
}) {
if (!showing) return null;
return (
<div className="fixed top-0 left-0 right-0 h-14 bg-[#18181B] flex items-center justify-end px-4 z-[9999]">
<div className="absolute left-1/2 transform -translate-x-1/2 flex items-center gap-x-2">
<Warning size={18} className="text-white" />
<p className="text-white font-medium text-xs">Unsaved Changes</p>
</div>
<div className="flex items-center gap-x-2">
<button
className="border-none text-white font-medium text-sm px-[10px] py-[6px] rounded-md bg-white/5 hover:bg-white/10"
onClick={onCancel}
>
Cancel
</button>
<button
className="border-none text-[#222628] font-medium text-sm px-[10px] py-[6px] rounded-md bg-[#46C8FF] hover:bg-[#3DB5E8]"
onClick={onSave}
>
Save
</button>
</div>
</div>
);
}
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
EyeSlash, EyeSlash,
SplitVertical, SplitVertical,
Microphone, Microphone,
Robot,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import { USER_BACKGROUND_COLOR } from "@/utils/constants"; import { USER_BACKGROUND_COLOR } from "@/utils/constants";
...@@ -258,6 +259,15 @@ const SidebarOptions = ({ user = null }) => ( ...@@ -258,6 +259,15 @@ const SidebarOptions = ({ user = null }) => (
flex={true} flex={true}
allowedRole={["admin", "manager"]} allowedRole={["admin", "manager"]}
/> />
<Option
href={paths.settings.agentSkills()}
btnText="Agent Skills"
icon={<Robot className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
<Option <Option
href={paths.settings.appearance()} href={paths.settings.appearance()}
btnText="Appearance" btnText="Appearance"
......
frontend/src/media/agents/generate-charts.png

169 KiB

frontend/src/media/agents/generate-save-files.png

172 KiB

frontend/src/media/agents/rag-memory.png

171 KiB

frontend/src/media/agents/scrape-websites.png

171 KiB

frontend/src/media/agents/sql-agent.png

171 KiB

frontend/src/media/agents/view-summarize.png

170 KiB

import { Tooltip } from "react-tooltip";
export function DefaultBadge({ title }) {
return (
<>
<span
className="w-fit"
data-tooltip-id={`default-skill-${title}`}
data-tooltip-content="This skill is enabled by default and cannot be turned off."
>
<div className="flex items-center gap-x-1 w-fit rounded-full bg-[#F4FFD0]/10 px-2.5 py-0.5 text-sm font-medium text-sky-400 shadow-sm cursor-pointer">
<div className="text-[#F4FFD0] text-[12px] leading-[15px]">
Default
</div>
</div>
</span>
<Tooltip
id={`default-skill-${title}`}
place="bottom"
delayShow={300}
className="tooltip !text-xs"
/>
</>
);
}
import React from "react";
import { DefaultBadge } from "../Badges/default";
export default function DefaultSkillPanel({ title, description, image, icon }) {
return (
<div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex w-full justify-between items-center">
<div className="flex items-center gap-x-2">
{icon &&
React.createElement(icon, {
size: 24,
color: "white",
weight: "bold",
})}
<label htmlFor="name" className="text-white text-md font-bold">
{title}
</label>
<DefaultBadge title={title} />
</div>
</div>
<img src={image} alt={title} className="w-full rounded-md" />
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{description}
</p>
</div>
</div>
);
}
import React from "react"; import React from "react";
export default function GenericSkill({
export default function GenericSkillPanel({
title, title,
description, description,
skill, skill,
toggleSkill, toggleSkill,
enabled = false, enabled = false,
disabled = false, disabled = false,
image,
icon,
}) { }) {
return ( return (
<div className="border-b border-white/40 pb-4"> <div className="p-2">
<div className="flex flex-col"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex w-full justify-between items-center"> <div className="flex items-center gap-x-2">
<label htmlFor="name" className="block input-label"> {icon &&
React.createElement(icon, {
size: 24,
color: "white",
weight: "bold",
})}
<label htmlFor="name" className="text-white text-md font-bold">
{title} {title}
</label> </label>
<label <label
className={`border-none relative inline-flex items-center mt-2 ${ className={`border-none relative inline-flex items-center ml-auto ${
disabled ? "cursor-not-allowed" : "cursor-pointer" disabled ? "cursor-not-allowed" : "cursor-pointer"
}`} }`}
> >
...@@ -24,12 +33,13 @@ export default function GenericSkill({ ...@@ -24,12 +33,13 @@ export default function GenericSkill({
disabled={disabled} disabled={disabled}
className="peer sr-only" className="peer sr-only"
checked={enabled} checked={enabled}
onClick={() => toggleSkill(skill)} onChange={() => toggleSkill(skill)}
/> />
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> <div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label> </label>
</div> </div>
<img src={image} alt={title} className="w-full rounded-md" />
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{description} {description}
</p> </p>
......
...@@ -74,8 +74,8 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { ...@@ -74,8 +74,8 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
// to the parent container form so we don't have nested forms. // to the parent container form so we don't have nested forms.
return createPortal( return createPortal(
<ModalWrapper isOpen={isOpen}> <ModalWrapper isOpen={isOpen}>
<div className="relative w-1/3 max-h-full "> <div className="relative w-1/3 max-h-full mt-8">
<div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[90vh] overflow-y-scroll no-scroll"> <div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[85vh] overflow-y-scroll no-scroll">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white"> <h3 className="text-xl font-semibold text-white">
New SQL Connection New SQL Connection
......
import React, { useState } from "react"; import React, { useState } from "react";
import DBConnection from "./DBConnection"; import DBConnection from "./DBConnection";
import { Plus } from "@phosphor-icons/react"; import { Plus, Database } from "@phosphor-icons/react";
import NewSQLConnection from "./NewConnectionModal"; import NewSQLConnection from "./NewConnectionModal";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import SQLAgentImage from "@/media/agents/sql-agent.png";
export default function AgentSQLConnectorSelection({ export default function AgentSQLConnectorSelection({
skill, skill,
...@@ -15,89 +16,94 @@ export default function AgentSQLConnectorSelection({ ...@@ -15,89 +16,94 @@ export default function AgentSQLConnectorSelection({
const [connections, setConnections] = useState( const [connections, setConnections] = useState(
settings?.preferences?.agent_sql_connections || [] settings?.preferences?.agent_sql_connections || []
); );
return ( return (
<> <>
<div className="border-b border-white/40 pb-4"> <div className="p-2">
<div className="flex flex-col"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex w-full justify-between items-center"> <div className="flex items-center gap-x-2">
<label htmlFor="name" className="block input-label"> <Database size={24} color="white" weight="bold" />
<label htmlFor="name" className="text-white text-md font-bold">
SQL Agent SQL Agent
</label> </label>
<label className="border-none relative inline-flex cursor-pointer items-center mt-2"> <label className="border-none relative inline-flex cursor-pointer items-center ml-auto">
<input <input
type="checkbox" type="checkbox"
className="peer sr-only" className="peer sr-only"
checked={enabled} checked={enabled}
onClick={() => toggleSkill(skill)} onChange={() => toggleSkill(skill)}
/> />
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label> </label>
</div> </div>
<img
src={SQLAgentImage}
alt="SQL Agent"
className="w-full rounded-md"
/>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Enable your agent to be able to leverage SQL to answer you questions Enable your agent to be able to leverage SQL to answer you questions
by connecting to various SQL database providers. by connecting to various SQL database providers.
</p> </p>
</div> {enabled && (
{enabled && ( <>
<> <input
<input name="system::agent_sql_connections"
name="system::agent_sql_connections" type="hidden"
type="hidden" value={JSON.stringify(connections)}
value={JSON.stringify(connections)} />
/> <input
<input type="hidden"
type="hidden" value={JSON.stringify(
value={JSON.stringify( connections.filter((conn) => conn.action !== "remove")
connections.filter((conn) => conn.action !== "remove") )}
)} />
/> <div className="flex flex-col mt-2 gap-y-2">
<div className="flex flex-col mt-2 gap-y-2"> <p className="text-white font-semibold text-sm">
<p className="text-white font-semibold text-sm"> Your database connections
Your database connections </p>
</p> <div className="flex flex-col gap-y-3">
<div className="flex flex-col gap-y-3"> {connections
{connections .filter((connection) => connection.action !== "remove")
.filter((connection) => connection.action !== "remove") .map((connection) => (
.map((connection) => ( <DBConnection
<DBConnection key={connection.database_id}
key={connection.database_id} connection={connection}
connection={connection} onRemove={(databaseId) => {
onRemove={(databaseId) => { setHasChanges(true);
setConnections((prev) => setConnections((prev) =>
prev.map((conn) => { prev.map((conn) => {
if (conn.database_id === databaseId) if (conn.database_id === databaseId)
return { ...conn, action: "remove" }; return { ...conn, action: "remove" };
return conn; return conn;
}) })
); );
}} }}
setHasChanges={setHasChanges}
/>
))}
<button
type="button"
onClick={openModal}
className="w-fit relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg"
>
<div className="flex w-full gap-x-2 items-center p-4">
<div className="bg-zinc-600 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center">
<Plus
weight="bold"
size={14}
className="shrink-0 text-slate-100"
/> />
))}
<button
type="button"
onClick={openModal}
className="w-fit relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg"
>
<div className="flex w-full gap-x-2 items-center p-4">
<div className="bg-zinc-600 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center">
<Plus
weight="bold"
size={14}
className="shrink-0 text-slate-100"
/>
</div>
<p className="text-left text-slate-100 text-sm">
New SQL connection
</p>
</div> </div>
<p className="text-left text-slate-100 text-sm"> </button>
New SQL connection </div>
</p>
</div>
</button>
</div> </div>
</div> </>
</> )}
)} </div>
</div> </div>
<NewSQLConnection <NewSQLConnection
isOpen={isOpen} isOpen={isOpen}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment