[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: timothycarambat <rambat1010@gmail.com>
|
@ -4,7 +4,9 @@
|
|||
"target": "esnext",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ const AdminInvites = lazy(() => import("@/pages/Admin/Invitations"));
|
|||
const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces"));
|
||||
const AdminSystem = lazy(() => import("@/pages/Admin/System"));
|
||||
const AdminLogs = lazy(() => import("@/pages/Admin/Logging"));
|
||||
const AdminAgents = lazy(() => import("@/pages/Admin/Agents"));
|
||||
const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats"));
|
||||
const GeneralAppearance = lazy(
|
||||
() => import("@/pages/GeneralSettings/Appearance")
|
||||
|
@ -106,6 +107,10 @@ export default function App() {
|
|||
path="/settings/vector-database"
|
||||
element={<AdminRoute Component={GeneralVectorDatabase} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents"
|
||||
element={<AdminRoute Component={AdminAgents} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/event-logs"
|
||||
element={<AdminRoute Component={AdminLogs} />}
|
||||
|
|
32
frontend/src/components/ContextualSaveBar/index.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
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 {
|
|||
EyeSlash,
|
||||
SplitVertical,
|
||||
Microphone,
|
||||
Robot,
|
||||
} from "@phosphor-icons/react";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import { USER_BACKGROUND_COLOR } from "@/utils/constants";
|
||||
|
@ -258,6 +259,15 @@ const SidebarOptions = ({ user = null }) => (
|
|||
flex={true}
|
||||
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
|
||||
href={paths.settings.appearance()}
|
||||
btnText="Appearance"
|
||||
|
|
BIN
frontend/src/media/agents/generate-charts.png
Normal file
After ![]() (image error) Size: 169 KiB |
BIN
frontend/src/media/agents/generate-save-files.png
Normal file
After ![]() (image error) Size: 172 KiB |
BIN
frontend/src/media/agents/rag-memory.png
Normal file
After ![]() (image error) Size: 171 KiB |
BIN
frontend/src/media/agents/scrape-websites.png
Normal file
After ![]() (image error) Size: 171 KiB |
BIN
frontend/src/media/agents/sql-agent.png
Normal file
After ![]() (image error) Size: 171 KiB |
BIN
frontend/src/media/agents/view-summarize.png
Normal file
After ![]() (image error) Size: 170 KiB |
25
frontend/src/pages/Admin/Agents/Badges/default.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
29
frontend/src/pages/Admin/Agents/DefaultSkillPanel/index.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,21 +1,30 @@
|
|||
import React from "react";
|
||||
export default function GenericSkill({
|
||||
|
||||
export default function GenericSkillPanel({
|
||||
title,
|
||||
description,
|
||||
skill,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
disabled = false,
|
||||
image,
|
||||
icon,
|
||||
}) {
|
||||
return (
|
||||
<div className="border-b border-white/40 pb-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
<div className="p-2">
|
||||
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
|
||||
<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>
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
|
@ -24,12 +33,13 @@ export default function GenericSkill({
|
|||
disabled={disabled}
|
||||
className="peer sr-only"
|
||||
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>
|
||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||
</label>
|
||||
</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>
|
|
@ -74,8 +74,8 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
|
|||
// to the parent container form so we don't have nested forms.
|
||||
return createPortal(
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="relative w-1/3 max-h-full ">
|
||||
<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 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-[85vh] overflow-y-scroll no-scroll">
|
||||
<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">
|
||||
New SQL Connection
|
Before ![]() (image error) Size: 38 KiB After ![]() (image error) Size: 38 KiB ![]() ![]() |
Before ![]() (image error) Size: 13 KiB After ![]() (image error) Size: 13 KiB ![]() ![]() |
Before ![]() (image error) Size: 46 KiB After ![]() (image error) Size: 46 KiB ![]() ![]() |
117
frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import React, { useState } from "react";
|
||||
import DBConnection from "./DBConnection";
|
||||
import { Plus, Database } from "@phosphor-icons/react";
|
||||
import NewSQLConnection from "./NewConnectionModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import SQLAgentImage from "@/media/agents/sql-agent.png";
|
||||
|
||||
export default function AgentSQLConnectorSelection({
|
||||
skill,
|
||||
settings,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
setHasChanges,
|
||||
}) {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [connections, setConnections] = useState(
|
||||
settings?.preferences?.agent_sql_connections || []
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="p-2">
|
||||
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Database size={24} color="white" weight="bold" />
|
||||
<label htmlFor="name" className="text-white text-md font-bold">
|
||||
SQL Agent
|
||||
</label>
|
||||
<label className="border-none relative inline-flex cursor-pointer items-center ml-auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={enabled}
|
||||
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>
|
||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||
</label>
|
||||
</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">
|
||||
Enable your agent to be able to leverage SQL to answer you questions
|
||||
by connecting to various SQL database providers.
|
||||
</p>
|
||||
{enabled && (
|
||||
<>
|
||||
<input
|
||||
name="system::agent_sql_connections"
|
||||
type="hidden"
|
||||
value={JSON.stringify(connections)}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
value={JSON.stringify(
|
||||
connections.filter((conn) => conn.action !== "remove")
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col mt-2 gap-y-2">
|
||||
<p className="text-white font-semibold text-sm">
|
||||
Your database connections
|
||||
</p>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{connections
|
||||
.filter((connection) => connection.action !== "remove")
|
||||
.map((connection) => (
|
||||
<DBConnection
|
||||
key={connection.database_id}
|
||||
connection={connection}
|
||||
onRemove={(databaseId) => {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) =>
|
||||
prev.map((conn) => {
|
||||
if (conn.database_id === databaseId)
|
||||
return { ...conn, action: "remove" };
|
||||
return conn;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NewSQLConnection
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
onSubmit={(newDb) =>
|
||||
setConnections((prev) => [...prev, { action: "add", ...newDb }])
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Before ![]() (image error) Size: 73 KiB After ![]() (image error) Size: 73 KiB ![]() ![]() |
Before ![]() (image error) Size: 18 KiB After ![]() (image error) Size: 18 KiB ![]() ![]() |
Before ![]() (image error) Size: 31 KiB After ![]() (image error) Size: 31 KiB ![]() ![]() |
Before ![]() (image error) Size: 4.4 KiB After ![]() (image error) Size: 4.4 KiB ![]() ![]() |
227
frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx
Normal file
|
@ -0,0 +1,227 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||
import GoogleSearchIcon from "./icons/google.png";
|
||||
import SerperDotDevIcon from "./icons/serper.png";
|
||||
import BingSearchIcon from "./icons/bing.png";
|
||||
import SerplySearchIcon from "./icons/serply.png";
|
||||
import {
|
||||
CaretUpDown,
|
||||
MagnifyingGlass,
|
||||
X,
|
||||
ListMagnifyingGlass,
|
||||
} from "@phosphor-icons/react";
|
||||
import SearchProviderItem from "./SearchProviderItem";
|
||||
import WebSearchImage from "@/media/agents/scrape-websites.png";
|
||||
import {
|
||||
SerperDotDevOptions,
|
||||
GoogleSearchOptions,
|
||||
BingSearchOptions,
|
||||
SerplySearchOptions,
|
||||
} from "./SearchProviderOptions";
|
||||
|
||||
const SEARCH_PROVIDERS = [
|
||||
{
|
||||
name: "Please make a selection",
|
||||
value: "none",
|
||||
logo: AnythingLLMIcon,
|
||||
options: () => <React.Fragment />,
|
||||
description:
|
||||
"Web search will be disabled until a provider and keys are provided.",
|
||||
},
|
||||
{
|
||||
name: "Google Search Engine",
|
||||
value: "google-search-engine",
|
||||
logo: GoogleSearchIcon,
|
||||
options: (settings) => <GoogleSearchOptions settings={settings} />,
|
||||
description:
|
||||
"Web search powered by a custom Google Search Engine. Free for 100 queries per day.",
|
||||
},
|
||||
{
|
||||
name: "Serper.dev",
|
||||
value: "serper-dot-dev",
|
||||
logo: SerperDotDevIcon,
|
||||
options: (settings) => <SerperDotDevOptions settings={settings} />,
|
||||
description:
|
||||
"Serper.dev web-search. Free account with a 2,500 calls, but then paid.",
|
||||
},
|
||||
{
|
||||
name: "Bing Search",
|
||||
value: "bing-search",
|
||||
logo: BingSearchIcon,
|
||||
options: (settings) => <BingSearchOptions settings={settings} />,
|
||||
description:
|
||||
"Web search powered by the Bing Search API. Free for 1000 queries per month.",
|
||||
},
|
||||
{
|
||||
name: "Serply.io",
|
||||
value: "serply-engine",
|
||||
logo: SerplySearchIcon,
|
||||
options: (settings) => <SerplySearchOptions settings={settings} />,
|
||||
description:
|
||||
"Serply.io web-search. Free account with a 100 calls/month forever.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AgentWebSearchSelection({
|
||||
skill,
|
||||
settings,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
setHasChanges,
|
||||
}) {
|
||||
const searchInputRef = useRef(null);
|
||||
const [filteredResults, setFilteredResults] = useState([]);
|
||||
const [selectedProvider, setSelectedProvider] = useState("none");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
|
||||
|
||||
function updateChoice(selection) {
|
||||
setSearchQuery("");
|
||||
setSelectedProvider(selection);
|
||||
setSearchMenuOpen(false);
|
||||
setHasChanges(true);
|
||||
}
|
||||
|
||||
function handleXButton() {
|
||||
if (searchQuery.length > 0) {
|
||||
setSearchQuery("");
|
||||
if (searchInputRef.current) searchInputRef.current.value = "";
|
||||
} else {
|
||||
setSearchMenuOpen(!searchMenuOpen);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = SEARCH_PROVIDERS.filter((provider) =>
|
||||
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredResults(filtered);
|
||||
}, [searchQuery, selectedProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none");
|
||||
}, [settings?.preferences?.agent_search_provider]);
|
||||
|
||||
const selectedSearchProviderObject = SEARCH_PROVIDERS.find(
|
||||
(provider) => provider.value === selectedProvider
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ListMagnifyingGlass size={24} color="white" weight="bold" />
|
||||
<label htmlFor="name" className="text-white text-md font-bold">
|
||||
Live web search and browsing
|
||||
</label>
|
||||
<label className="border-none relative inline-flex cursor-pointer items-center ml-auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={enabled}
|
||||
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>
|
||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||
</label>
|
||||
</div>
|
||||
<img
|
||||
src={WebSearchImage}
|
||||
alt="Web Search"
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
Enable your agent to search the web to answer your questions by
|
||||
connecting to a web-search (SERP) provider. Web search during agent
|
||||
sessions will not work until this is set up.
|
||||
</p>
|
||||
<div hidden={!enabled}>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="hidden"
|
||||
name="system::agent_search_provider"
|
||||
value={selectedProvider}
|
||||
/>
|
||||
{searchMenuOpen && (
|
||||
<div
|
||||
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
|
||||
onClick={() => setSearchMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{searchMenuOpen ? (
|
||||
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
|
||||
<MagnifyingGlass
|
||||
size={20}
|
||||
weight="bold"
|
||||
className="absolute left-4 z-30 text-white -ml-4 my-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="web-provider-search"
|
||||
autoComplete="off"
|
||||
placeholder="Search available web-search providers"
|
||||
className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
ref={searchInputRef}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
<X
|
||||
size={20}
|
||||
weight="bold"
|
||||
className="cursor-pointer text-white hover:text-[#9CA3AF]"
|
||||
onClick={handleXButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
|
||||
{filteredResults.map((provider) => {
|
||||
return (
|
||||
<SearchProviderItem
|
||||
provider={provider}
|
||||
key={provider.name}
|
||||
checked={selectedProvider === provider.value}
|
||||
onClick={() => updateChoice(provider.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
|
||||
type="button"
|
||||
onClick={() => setSearchMenuOpen(true)}
|
||||
>
|
||||
<div className="flex gap-x-4 items-center">
|
||||
<img
|
||||
src={selectedSearchProviderObject.logo}
|
||||
alt={`${selectedSearchProviderObject.name} logo`}
|
||||
className="w-10 h-10 rounded-md"
|
||||
/>
|
||||
<div className="flex flex-col text-left">
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{selectedSearchProviderObject.name}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-[#D2D5DB]">
|
||||
{selectedSearchProviderObject.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CaretUpDown size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{selectedProvider !== "none" && (
|
||||
<div className="mt-4 flex flex-col gap-y-1">
|
||||
{selectedSearchProviderObject.options(settings)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
232
frontend/src/pages/Admin/Agents/index.jsx
Normal file
|
@ -0,0 +1,232 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import Sidebar from "@/components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Admin from "@/models/admin";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { CaretRight, Robot } from "@phosphor-icons/react";
|
||||
import ContextualSaveBar from "@/components/ContextualSaveBar";
|
||||
import { castToType } from "@/utils/types";
|
||||
import { FullScreenLoader } from "@/components/Preloader";
|
||||
import { defaultSkills, configurableSkills } from "./skills";
|
||||
import { DefaultBadge } from "./Badges/default";
|
||||
|
||||
export default function AdminAgents() {
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [settings, setSettings] = useState({});
|
||||
const [selectedSkill, setSelectedSkill] = useState("");
|
||||
const [agentSkills, setAgentSkills] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const formEl = useRef(null);
|
||||
|
||||
// Alert user if they try to leave the page with unsaved changes
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (hasChanges) {
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
}
|
||||
};
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}, [hasChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
const _settings = await System.keys();
|
||||
const _preferences = await Admin.systemPreferences();
|
||||
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const toggleAgentSkill = (skillName) => {
|
||||
setAgentSkills((prev) => {
|
||||
const updatedSkills = prev.includes(skillName)
|
||||
? prev.filter((name) => name !== skillName)
|
||||
: [...prev, skillName];
|
||||
setHasChanges(true);
|
||||
return updatedSkills;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
workspace: {},
|
||||
system: {},
|
||||
env: {},
|
||||
};
|
||||
|
||||
const form = new FormData(formEl.current);
|
||||
for (var [key, value] of form.entries()) {
|
||||
if (key.startsWith("system::")) {
|
||||
const [_, label] = key.split("system::");
|
||||
data.system[label] = String(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.startsWith("env::")) {
|
||||
const [_, label] = key.split("env::");
|
||||
data.env[label] = String(value);
|
||||
continue;
|
||||
}
|
||||
data.workspace[key] = castToType(key, value);
|
||||
}
|
||||
|
||||
const { success } = await Admin.updateSystemPreferences(data.system);
|
||||
await System.updateSystem(data.env);
|
||||
|
||||
if (success) {
|
||||
const _settings = await System.keys();
|
||||
const _preferences = await Admin.systemPreferences();
|
||||
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||
showToast(`Agent preferences saved successfully.`, "success", {
|
||||
clear: true,
|
||||
});
|
||||
} else {
|
||||
showToast(`Agent preferences failed to save.`, "error", { clear: true });
|
||||
}
|
||||
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const SelectedSkillComponent =
|
||||
configurableSkills[selectedSkill]?.component ||
|
||||
defaultSkills[selectedSkill]?.component;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex justify-center items-center"
|
||||
>
|
||||
<FullScreenLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
id="workspace-agent-settings-container"
|
||||
className="w-screen h-screen overflow-hidden bg-sidebar flex"
|
||||
>
|
||||
<Sidebar />
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex"
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
onChange={() => setHasChanges(true)}
|
||||
ref={formEl}
|
||||
className="flex-1 flex gap-x-6 p-4 mt-10"
|
||||
>
|
||||
<input
|
||||
name="system::default_agent_skills"
|
||||
type="hidden"
|
||||
value={agentSkills.join(",")}
|
||||
/>
|
||||
|
||||
{/* Skill settings nav */}
|
||||
<div className="flex flex-col gap-y-[18px]">
|
||||
<div className="text-white flex items-center gap-x-2">
|
||||
<Robot size={24} />
|
||||
<p className="text-lg font-medium">Agent Skills</p>
|
||||
</div>
|
||||
|
||||
{/* Default skills list */}
|
||||
<SkillList
|
||||
isDefault={true}
|
||||
skills={defaultSkills}
|
||||
selectedSkill={selectedSkill}
|
||||
handleClick={setSelectedSkill}
|
||||
/>
|
||||
{/* Configurable skills */}
|
||||
<SkillList
|
||||
skills={configurableSkills}
|
||||
selectedSkill={selectedSkill}
|
||||
handleClick={setSelectedSkill}
|
||||
activeSkills={agentSkills}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected agent skill setting panel */}
|
||||
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
|
||||
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4">
|
||||
{SelectedSkillComponent ? (
|
||||
<SelectedSkillComponent
|
||||
skill={configurableSkills[selectedSkill]?.skill}
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={agentSkills.includes(
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
{...(configurableSkills[selectedSkill] ||
|
||||
defaultSkills[selectedSkill])}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-white/60">
|
||||
<Robot size={40} />
|
||||
<p className="font-medium">Select an agent skill</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ContextualSaveBar
|
||||
showing={hasChanges}
|
||||
onSave={handleSubmit}
|
||||
onCancel={() => setHasChanges(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillList({
|
||||
isDefault = false,
|
||||
skills = [],
|
||||
selectedSkill = null,
|
||||
handleClick = null,
|
||||
activeSkills = [],
|
||||
}) {
|
||||
if (skills.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 text-white min-w-[360px] w-fit rounded-xl">
|
||||
{Object.entries(skills).map(([skill, settings], index) => (
|
||||
<div
|
||||
key={skill}
|
||||
className={`py-3 px-4 flex items-center justify-between ${
|
||||
index === 0 ? "rounded-t-xl" : ""
|
||||
} ${
|
||||
index === Object.keys(skills).length - 1
|
||||
? "rounded-b-xl"
|
||||
: "border-b border-white/10"
|
||||
} cursor-pointer transition-all duration-300 hover:bg-white/5 ${
|
||||
selectedSkill === skill ? "bg-white/10" : ""
|
||||
}`}
|
||||
onClick={() => handleClick?.(skill)}
|
||||
>
|
||||
<div className="text-sm font-light">{settings.title}</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isDefault ? (
|
||||
<DefaultBadge title={skill} />
|
||||
) : (
|
||||
<div className="text-sm text-white/60 font-medium">
|
||||
{activeSkills.includes(skill) ? "On" : "Off"}
|
||||
</div>
|
||||
)}
|
||||
<CaretRight size={14} weight="bold" className="text-white/80" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
73
frontend/src/pages/Admin/Agents/skills.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
import AgentWebSearchSelection from "./WebSearchSelection";
|
||||
import AgentSQLConnectorSelection from "./SQLConnectorSelection";
|
||||
import GenericSkillPanel from "./GenericSkillPanel";
|
||||
import DefaultSkillPanel from "./DefaultSkillPanel";
|
||||
import {
|
||||
Brain,
|
||||
File,
|
||||
Browser,
|
||||
ChartBar,
|
||||
FileMagnifyingGlass,
|
||||
} from "@phosphor-icons/react";
|
||||
import RAGImage from "@/media/agents/rag-memory.png";
|
||||
import SummarizeImage from "@/media/agents/view-summarize.png";
|
||||
import ScrapeWebsitesImage from "@/media/agents/scrape-websites.png";
|
||||
import GenerateChartsImage from "@/media/agents/generate-charts.png";
|
||||
import GenerateSaveImages from "@/media/agents/generate-save-files.png";
|
||||
|
||||
export const defaultSkills = {
|
||||
"rag-memory": {
|
||||
title: "RAG & long-term memory",
|
||||
description:
|
||||
'Allow the agent to leverage your local documents to answer a query or ask the agent to "remember" pieces of content for long-term memory retrieval.',
|
||||
component: DefaultSkillPanel,
|
||||
icon: Brain,
|
||||
image: RAGImage,
|
||||
},
|
||||
"view-summarize": {
|
||||
title: "View & summarize documents",
|
||||
description:
|
||||
"Allow the agent to list and summarize the content of workspace files currently embedded.",
|
||||
component: DefaultSkillPanel,
|
||||
icon: File,
|
||||
image: SummarizeImage,
|
||||
},
|
||||
"scrape-websites": {
|
||||
title: "Scrape websites",
|
||||
description: "Allow the agent to visit and scrape the content of websites.",
|
||||
component: DefaultSkillPanel,
|
||||
icon: Browser,
|
||||
image: ScrapeWebsitesImage,
|
||||
},
|
||||
};
|
||||
|
||||
export const configurableSkills = {
|
||||
"save-file": {
|
||||
title: "Generate & save files to browser",
|
||||
description:
|
||||
"Enable the default agent to generate and write to files that can be saved to your computer.",
|
||||
component: GenericSkillPanel,
|
||||
skill: "save-file-to-browser",
|
||||
icon: FileMagnifyingGlass,
|
||||
image: GenerateSaveImages,
|
||||
},
|
||||
"create-chart": {
|
||||
title: "Generate charts",
|
||||
description:
|
||||
"Enable the default agent to generate various types of charts from data provided or given in chat.",
|
||||
component: GenericSkillPanel,
|
||||
skill: "create-chart",
|
||||
icon: ChartBar,
|
||||
image: GenerateChartsImage,
|
||||
},
|
||||
"web-browsing": {
|
||||
title: "Web Search",
|
||||
component: AgentWebSearchSelection,
|
||||
skill: "web-browsing",
|
||||
},
|
||||
"sql-agent": {
|
||||
title: "SQL Connector",
|
||||
component: AgentSQLConnectorSelection,
|
||||
skill: "sql-agent",
|
||||
},
|
||||
};
|
|
@ -1,111 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import DBConnection from "./DBConnection";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import NewSQLConnection from "./NewConnectionModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
|
||||
export default function AgentSQLConnectorSelection({
|
||||
skill,
|
||||
settings,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
setHasChanges,
|
||||
}) {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [connections, setConnections] = useState(
|
||||
settings?.preferences?.agent_sql_connections || []
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-white/40 pb-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
SQL Agent
|
||||
</label>
|
||||
<label className="border-none relative inline-flex cursor-pointer items-center mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={enabled}
|
||||
onClick={() => 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>
|
||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||
</label>
|
||||
</div>
|
||||
<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
|
||||
by connecting to various SQL database providers.
|
||||
</p>
|
||||
</div>
|
||||
{enabled && (
|
||||
<>
|
||||
<input
|
||||
name="system::agent_sql_connections"
|
||||
type="hidden"
|
||||
value={JSON.stringify(connections)}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
value={JSON.stringify(
|
||||
connections.filter((conn) => conn.action !== "remove")
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col mt-2 gap-y-2">
|
||||
<p className="text-white font-semibold text-sm">
|
||||
Your database connections
|
||||
</p>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{connections
|
||||
.filter((connection) => connection.action !== "remove")
|
||||
.map((connection) => (
|
||||
<DBConnection
|
||||
key={connection.database_id}
|
||||
connection={connection}
|
||||
onRemove={(databaseId) => {
|
||||
setConnections((prev) =>
|
||||
prev.map((conn) => {
|
||||
if (conn.database_id === databaseId)
|
||||
return { ...conn, action: "remove" };
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-left text-slate-100 text-sm">
|
||||
New SQL connection
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<NewSQLConnection
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
onSubmit={(newDb) =>
|
||||
setConnections((prev) => [...prev, { action: "add", ...newDb }])
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||
import GoogleSearchIcon from "./icons/google.png";
|
||||
import SerperDotDevIcon from "./icons/serper.png";
|
||||
import BingSearchIcon from "./icons/bing.png";
|
||||
import SerplySearchIcon from "./icons/serply.png";
|
||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import SearchProviderItem from "./SearchProviderItem";
|
||||
import {
|
||||
SerperDotDevOptions,
|
||||
GoogleSearchOptions,
|
||||
BingSearchOptions,
|
||||
SerplySearchOptions,
|
||||
} from "./SearchProviderOptions";
|
||||
|
||||
const SEARCH_PROVIDERS = [
|
||||
{
|
||||
name: "Please make a selection",
|
||||
value: "none",
|
||||
logo: AnythingLLMIcon,
|
||||
options: () => <React.Fragment />,
|
||||
description:
|
||||
"Web search will be disabled until a provider and keys are provided.",
|
||||
},
|
||||
{
|
||||
name: "Google Search Engine",
|
||||
value: "google-search-engine",
|
||||
logo: GoogleSearchIcon,
|
||||
options: (settings) => <GoogleSearchOptions settings={settings} />,
|
||||
description:
|
||||
"Web search powered by a custom Google Search Engine. Free for 100 queries per day.",
|
||||
},
|
||||
{
|
||||
name: "Serper.dev",
|
||||
value: "serper-dot-dev",
|
||||
logo: SerperDotDevIcon,
|
||||
options: (settings) => <SerperDotDevOptions settings={settings} />,
|
||||
description:
|
||||
"Serper.dev web-search. Free account with a 2,500 calls, but then paid.",
|
||||
},
|
||||
{
|
||||
name: "Bing Search",
|
||||
value: "bing-search",
|
||||
logo: BingSearchIcon,
|
||||
options: (settings) => <BingSearchOptions settings={settings} />,
|
||||
description:
|
||||
"Web search powered by the Bing Search API. Free for 1000 queries per month.",
|
||||
},
|
||||
{
|
||||
name: "Serply.io",
|
||||
value: "serply-engine",
|
||||
logo: SerplySearchIcon,
|
||||
options: (settings) => <SerplySearchOptions settings={settings} />,
|
||||
description:
|
||||
"Serply.io web-search. Free account with a 100 calls/month forever.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AgentWebSearchSelection({
|
||||
skill,
|
||||
settings,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
}) {
|
||||
const searchInputRef = useRef(null);
|
||||
const [filteredResults, setFilteredResults] = useState([]);
|
||||
const [selectedProvider, setSelectedProvider] = useState("none");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
|
||||
|
||||
function updateChoice(selection) {
|
||||
setSearchQuery("");
|
||||
setSelectedProvider(selection);
|
||||
setSearchMenuOpen(false);
|
||||
}
|
||||
|
||||
function handleXButton() {
|
||||
if (searchQuery.length > 0) {
|
||||
setSearchQuery("");
|
||||
if (searchInputRef.current) searchInputRef.current.value = "";
|
||||
} else {
|
||||
setSearchMenuOpen(!searchMenuOpen);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = SEARCH_PROVIDERS.filter((provider) =>
|
||||
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredResults(filtered);
|
||||
}, [searchQuery, selectedProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none");
|
||||
}, [settings?.preferences?.agent_search_provider]);
|
||||
|
||||
const selectedSearchProviderObject = SEARCH_PROVIDERS.find(
|
||||
(provider) => provider.value === selectedProvider
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-b border-white/40 pb-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
Live web search and browsing
|
||||
</label>
|
||||
<label className="border-none relative inline-flex cursor-pointer items-center mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={enabled}
|
||||
onClick={() => 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>
|
||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
Enable your agent to search the web to answer your questions by
|
||||
connecting to a web-search (SERP) provider.
|
||||
<br />
|
||||
Web search during agent sessions will not work until this is set up.
|
||||
</p>
|
||||
</div>
|
||||
<div hidden={!enabled}>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="hidden"
|
||||
name="system::agent_search_provider"
|
||||
value={selectedProvider}
|
||||
/>
|
||||
{searchMenuOpen && (
|
||||
<div
|
||||
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
|
||||
onClick={() => setSearchMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{searchMenuOpen ? (
|
||||
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
|
||||
<MagnifyingGlass
|
||||
size={20}
|
||||
weight="bold"
|
||||
className="absolute left-4 z-30 text-white -ml-4 my-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="web-provider-search"
|
||||
autoComplete="off"
|
||||
placeholder="Search available web-search providers"
|
||||
className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
ref={searchInputRef}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
<X
|
||||
size={20}
|
||||
weight="bold"
|
||||
className="cursor-pointer text-white hover:text-[#9CA3AF]"
|
||||
onClick={handleXButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
|
||||
{filteredResults.map((provider) => {
|
||||
return (
|
||||
<SearchProviderItem
|
||||
provider={provider}
|
||||
key={provider.name}
|
||||
checked={selectedProvider === provider.value}
|
||||
onClick={() => updateChoice(provider.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
|
||||
type="button"
|
||||
onClick={() => setSearchMenuOpen(true)}
|
||||
>
|
||||
<div className="flex gap-x-4 items-center">
|
||||
<img
|
||||
src={selectedSearchProviderObject.logo}
|
||||
alt={`${selectedSearchProviderObject.name} logo`}
|
||||
className="w-10 h-10 rounded-md"
|
||||
/>
|
||||
<div className="flex flex-col text-left">
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{selectedSearchProviderObject.name}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-[#D2D5DB]">
|
||||
{selectedSearchProviderObject.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CaretUpDown size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{selectedProvider !== "none" && (
|
||||
<div className="mt-4 flex flex-col gap-y-1">
|
||||
{selectedSearchProviderObject.options(settings)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,27 +4,25 @@ import showToast from "@/utils/toast";
|
|||
import { castToType } from "@/utils/types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import AgentLLMSelection from "./AgentLLMSelection";
|
||||
import AgentWebSearchSelection from "./WebSearchSelection";
|
||||
import AgentSQLConnectorSelection from "./SQLConnectorSelection";
|
||||
import GenericSkill from "./GenericSkill";
|
||||
import Admin from "@/models/admin";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import paths from "@/utils/paths";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function WorkspaceAgentConfiguration({ workspace }) {
|
||||
const [settings, setSettings] = useState({});
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [agentSkills, setAgentSkills] = useState([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const formEl = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
const _settings = await System.keys();
|
||||
const _preferences = await Admin.systemPreferences();
|
||||
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchSettings();
|
||||
|
@ -73,14 +71,6 @@ export default function WorkspaceAgentConfiguration({ workspace }) {
|
|||
setHasChanges(false);
|
||||
};
|
||||
|
||||
function toggleAgentSkill(skillName = "") {
|
||||
setAgentSkills((prev) => {
|
||||
return prev.includes(skillName)
|
||||
? prev.filter((name) => name !== skillName)
|
||||
: [...prev, skillName];
|
||||
});
|
||||
}
|
||||
|
||||
if (!workspace || loading) return <LoadingSkeleton />;
|
||||
return (
|
||||
<div id="workspace-agent-settings-container">
|
||||
|
@ -96,12 +86,23 @@ export default function WorkspaceAgentConfiguration({ workspace }) {
|
|||
workspace={workspace}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
<AvailableAgentSkills
|
||||
skills={agentSkills}
|
||||
toggleAgentSkill={toggleAgentSkill}
|
||||
settings={settings}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
{!hasChanges && (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<button onClick={() => navigate(paths.settings.agentSkills())}>
|
||||
<div
|
||||
type="button"
|
||||
className="w-fit transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||
>
|
||||
Configure Agent Skills
|
||||
</div>
|
||||
</button>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium">
|
||||
Customize and enhance the default agent's capabilities by enabling
|
||||
or disabling specific skills. These settings will be applied
|
||||
across all workspaces.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<button
|
||||
type="submit"
|
||||
|
@ -143,83 +144,3 @@ function LoadingSkeleton() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AvailableAgentSkills({
|
||||
skills,
|
||||
settings,
|
||||
toggleAgentSkill,
|
||||
setHasChanges,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col mb-8">
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<label htmlFor="name" className="text-white text-md font-semibold">
|
||||
Default agent skills
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
Improve the natural abilities of the default agent with these
|
||||
pre-built skills. This set up applies to all workspaces.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
name="system::default_agent_skills"
|
||||
type="hidden"
|
||||
value={skills.join(",")}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<GenericSkill
|
||||
title="RAG & long-term memory"
|
||||
description='Allow the agent to leverage your local documents to answer a query or ask the agent to "remember" pieces of content for long-term memory retrieval.'
|
||||
settings={settings}
|
||||
enabled={true}
|
||||
disabled={true}
|
||||
/>
|
||||
<GenericSkill
|
||||
title="View & summarize documents"
|
||||
description="Allow the agent to list and summarize the content of workspace files currently embedded."
|
||||
settings={settings}
|
||||
enabled={true}
|
||||
disabled={true}
|
||||
/>
|
||||
<GenericSkill
|
||||
title="Scrape websites"
|
||||
description="Allow the agent to visit and scrape the content of websites."
|
||||
settings={settings}
|
||||
enabled={true}
|
||||
disabled={true}
|
||||
/>
|
||||
<GenericSkill
|
||||
title="Generate charts"
|
||||
description="Enable the default agent to generate various types of charts from data provided or given in chat."
|
||||
skill="create-chart"
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={skills.includes("create-chart")}
|
||||
/>
|
||||
<GenericSkill
|
||||
title="Generate & save files to browser"
|
||||
description="Enable the default agent to generate and write to files that save and can be downloaded in your browser."
|
||||
skill="save-file-to-browser"
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={skills.includes("save-file-to-browser")}
|
||||
/>
|
||||
<AgentWebSearchSelection
|
||||
skill="web-browsing"
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={skills.includes("web-browsing")}
|
||||
/>
|
||||
<AgentSQLConnectorSelection
|
||||
skill="sql-agent"
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={skills.includes("sql-agent")}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -117,6 +117,9 @@ export default {
|
|||
appearance: () => {
|
||||
return "/settings/appearance";
|
||||
},
|
||||
agentSkills: () => {
|
||||
return "/settings/agents";
|
||||
},
|
||||
apiKeys: () => {
|
||||
return "/settings/api-keys";
|
||||
},
|
||||
|
|
|
@ -317,6 +317,7 @@ function adminEndpoints(app) {
|
|||
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||
async (_, response) => {
|
||||
try {
|
||||
const embedder = getEmbeddingEngineSelection();
|
||||
const settings = {
|
||||
users_can_delete_workspaces:
|
||||
(await SystemSettings.get({ label: "users_can_delete_workspaces" }))
|
||||
|
@ -337,13 +338,12 @@ function adminEndpoints(app) {
|
|||
text_splitter_chunk_size:
|
||||
(await SystemSettings.get({ label: "text_splitter_chunk_size" }))
|
||||
?.value ||
|
||||
getEmbeddingEngineSelection()?.embeddingMaxChunkLength ||
|
||||
embedder?.embeddingMaxChunkLength ||
|
||||
null,
|
||||
text_splitter_chunk_overlap:
|
||||
(await SystemSettings.get({ label: "text_splitter_chunk_overlap" }))
|
||||
?.value || null,
|
||||
max_embed_chunk_size:
|
||||
getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1000,
|
||||
max_embed_chunk_size: embedder?.embeddingMaxChunkLength || 1000,
|
||||
agent_search_provider:
|
||||
(await SystemSettings.get({ label: "agent_search_provider" }))
|
||||
?.value || null,
|
||||
|
|