mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 20:00:15 +01:00
🚧 chore: merge latest dev build to main repo (#3844)
* agents - phase 1 (#30) * chore: copy assistant files * feat: frontend and data-provider * feat: backend get endpoint test * fix(MessageEndpointIcon): switched to AgentName and AgentAvatar * fix: small fixes * fix: agent endpoint config * fix: show Agent Builder * chore: install agentus * chore: initial scaffolding for agents * fix: updated Assistant logic to Agent Logic for some Agent components * WIP first pass, demo of agent package * WIP: initial backend infra for agents * fix: agent list error * wip: agents routing * chore: Refactor useSSE hook to handle different data events * wip: correctly emit events * chore: Update @librechat/agentus npm dependency to version 1.0.9 * remove comment * first pass: streaming agent text * chore: Remove @librechat/agentus root-level workspace npm dependency * feat: Agent Schema and Model * fix: content handling fixes * fix: content message save * WIP: new content data * fix: run step issue with tool calls * chore: Update @librechat/agentus npm dependency to version 1.1.5 * feat: update controller and agent routes * wip: initial backend tool and tool error handling support * wip: tool chunks * chore: Update @librechat/agentus npm dependency to version 1.1.7 * chore: update tool_call typing, add test conditions and logs * fix: create agent * fix: create agent * first pass: render completed content parts * fix: remove logging, fix step handler typing * chore: Update @librechat/agentus npm dependency to version 1.1.9 * refactor: cleanup maps on unmount * chore: Update BaseClient.js to safely count tokens for string, number, and boolean values * fix: support subsequent messages with tool_calls * chore: export order * fix: select agent * fix: tool call types and handling * chore: switch to anthropic for testing * fix: AgentSelect * refactor: experimental: OpenAIClient to use array for intermediateReply * fix(useSSE): revert old condition for streaming legacy client tokens * fix: lint * revert `agent_id` to `id` * chore: update localization keys for agent-related components * feat: zod schema handling for actions * refactor(actions): if no params, no zodSchema * chore: Update @librechat/agentus npm dependency to version 1.2.1 * feat: first pass, actions * refactor: empty schema for actions without params * feat: Update createRun function to accept additional options * fix: message payload formatting; feat: add more client options * fix: ToolCall component rendering when action has no args but has output * refactor(ToolCall): allow non-stringy args * WIP: first pass, correctly formatted tool_calls between providers * refactor: Remove duplicate import of 'roles' module * refactor: Exclude 'vite.config.ts' from TypeScript compilation * refactor: fix agent related types > - no need to use endpoint/model fields for identifying agent metadata > - add `provider` distinction for agent-configured 'endpoint' - no need for agent-endpoint map - reduce complexity of tools as functions into tools as string[] - fix types related to above changes - reduce unnecessary variables for queries/mutations and corresponding react-query keys * refactor: Add tools and tool_kwargs fields to agent schema * refactor: Remove unused code and update dependencies * refactor: Update updateAgentHandler to use req.body directly * refactor: Update AgentSelect component to use localized hooks * refactor: Update agent schema to include tools and provider fields * refactor(AgentPanel): add scrollbar gutter, add provider field to form, fix agent schema required values * refactor: Update AgentSwitcher component to use selectedAgentId instead of selectedAgent * refactor: Update AgentPanel component to include alternateName import and defaultAgentFormValues * refactor(SelectDropDown): allow setting value as option while still supporting legacy usage (string values only) * refactor: SelectDropdown changes - Only necessary when the available values are objects with label/value fields and the selected value is expected to be a string. * refactor: TypeError issues and handle provider as option * feat: Add placeholder for provider selection in AgentPanel component * refactor: Update agent schema to include author and provider fields * fix: show expected 'create agent' placeholder when creating agent * chore: fix localization strings, hide capabilities form for now * chore: typing * refactor: import order and use compact agents schema for now * chore: typing * refactor: Update AgentForm type to use AgentCapabilities * fix agent form agent selection issues * feat: responsive agent selection * fix: Handle cancelled fetch in useSelectAgent hook * fix: reset agent form on accordion close/open * feat: Add agent_id to default conversation for agents endpoint * feat: agents endpoint request handling * refactor: reset conversation model on agent select * refactor: add `additional_instructions` to conversation schema, organize other fields * chore: casing * chore: types * refactor(loadAgentTools): explicitly pass agent_id, do not pass `model` to loadAgentTools for now, load action sets by agent_id * WIP: initial draft of real agent client initialization * WIP: first pass, anthropic agent requests * feat: remember last selected agent * feat: openai and azure connected * fix: prioritize agent model for runs unless an explicit override model is passed from client * feat: Agent Actions * fix: save agent id to convo * feat: model panel (#29) * feat: model panel * bring back comments * fix: method still null * fix: AgentPanel FormContext * feat: add more parameters * fix: style issues; refactor: Agent Controller * fix: cherry-pick * fix: Update AgentAvatar component to use AssistantIcon instead of BrainCircuit * feat: OGDialog for delete agent; feat(assistant): update Agent types, introduced `model_parameters` * feat: icon and general `model_parameters` update * feat: use react-hook-form better * fix: agent builder form reset issue when switching panels * refactor: modularize agent builder form --------- Co-authored-by: Danny Avila <danny@librechat.ai> * fix: AgentPanel and ModelPanel type issues and use `useFormContext` and `watch` instead of `methods` directly and `useWatch`. * fix: tool call issues due to invalid input (anthropic) of empty string * fix: handle empty text in Part component --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> * refactor: remove form ModelPanel and fixed nested ternary expressions in AgentConfig * fix: Model Parameters not saved correctly * refactor: remove console log * feat: avatar upload and get for Agents (#36) Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> * chore: update to public package * fix: typing, optional chaining * fix: cursor not showing for content parts * chore: conditionally enable agents * ci: fix azure test * ci: fix frontend tests, fix eslint api * refactor: Remove unused errorContentPart variable * continue of the agent message PR (#40) * last fixes * fix: agentMap * pr merge test (#41) * fix: model icon not fetching correctly * remove console logs * feat: agent name * refactor: pass documentsMap as a prop to allow re-render of assistant form * refactor: pass documentsMap as a prop to allow re-render of assistant form * chore: Bump version to 0.7.419 * fix: TypeError: Cannot read properties of undefined (reading 'id') * refactor: update AgentSwitcher component to use ControlCombobox instead of Combobox --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
parent
618be4bf2b
commit
a0291ed155
141 changed files with 14473 additions and 5714 deletions
87
client/src/components/SidePanel/AgentSwitcher.tsx
Normal file
87
client/src/components/SidePanel/AgentSwitcher.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { EModelEndpoint, isAgentsEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
import type { SwitcherProps, OptionWithIcon } from '~/common';
|
||||
import { useSetIndexOptions, useSelectAgent, useLocalize } from '~/hooks';
|
||||
import { useChatContext, useAgentsMapContext } from '~/Providers';
|
||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||
import Icon from '~/components/Endpoints/Icon';
|
||||
|
||||
export default function AgentSwitcher({ isCollapsed }: SwitcherProps) {
|
||||
const localize = useLocalize();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
const { index, conversation } = useChatContext();
|
||||
const { agent_id: selectedAgentId = null, endpoint } = conversation ?? {};
|
||||
|
||||
const agentsMapResult = useAgentsMapContext();
|
||||
|
||||
const agentsMap = useMemo(() => {
|
||||
return agentsMapResult ?? {};
|
||||
}, [agentsMapResult]);
|
||||
|
||||
const { onSelect } = useSelectAgent();
|
||||
|
||||
const agents: Agent[] = useMemo(() => {
|
||||
return Object.values(agentsMap) as Agent[];
|
||||
}, [agentsMap]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAgentId == null && agents.length > 0) {
|
||||
let agent_id = localStorage.getItem(`${LocalStorageKeys.AGENT_ID_PREFIX}${index}`);
|
||||
if (agent_id == null) {
|
||||
agent_id = agents[0].id;
|
||||
}
|
||||
const agent = agentsMap[agent_id];
|
||||
|
||||
if (agent !== undefined && isAgentsEndpoint(endpoint as string) === true) {
|
||||
setOption('model')('');
|
||||
setOption('agent_id')(agent_id);
|
||||
}
|
||||
}
|
||||
}, [index, agents, selectedAgentId, agentsMap, endpoint, setOption]);
|
||||
|
||||
const currentAgent = agentsMap[selectedAgentId ?? ''];
|
||||
|
||||
const agentOptions: OptionWithIcon[] = useMemo(
|
||||
() =>
|
||||
agents.map((agent: Agent) => {
|
||||
return {
|
||||
label: agent.name ?? '',
|
||||
value: agent.id,
|
||||
icon: (
|
||||
<Icon
|
||||
isCreatedByUser={false}
|
||||
endpoint={EModelEndpoint.agents}
|
||||
agentName={agent.name ?? ''}
|
||||
iconURL={agent.avatar?.filepath}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
[agents],
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlCombobox
|
||||
selectedValue={currentAgent?.id ?? ''}
|
||||
displayValue={
|
||||
agents.find((agent: Agent) => agent.id === selectedAgentId)?.name ??
|
||||
localize('com_sidepanel_select_agent')
|
||||
}
|
||||
selectPlaceholder={localize('com_sidepanel_select_agent')}
|
||||
searchPlaceholder={localize('com_agents_search_name')}
|
||||
isCollapsed={isCollapsed}
|
||||
ariaLabel={'agent'}
|
||||
setValue={onSelect}
|
||||
items={agentOptions}
|
||||
SelectIcon={
|
||||
<Icon
|
||||
isCreatedByUser={false}
|
||||
endpoint={endpoint}
|
||||
agentName={currentAgent?.name ?? ''}
|
||||
iconURL={currentAgent?.avatar?.filepath ?? ''}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
296
client/src/components/SidePanel/Agents/ActionsAuth.tsx
Normal file
296
client/src/components/SidePanel/Agents/ActionsAuth.tsx
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import { DialogContent } from '~/components/ui/';
|
||||
|
||||
export default function ActionsAuth({
|
||||
setOpenAuthDialog,
|
||||
}: {
|
||||
setOpenAuthDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { watch, setValue, trigger } = useFormContext();
|
||||
const type = watch('type');
|
||||
return (
|
||||
<DialogContent
|
||||
role="dialog"
|
||||
id="radix-:rf5:"
|
||||
aria-describedby="radix-:rf7:"
|
||||
aria-labelledby="radix-:rf6:"
|
||||
data-state="open"
|
||||
className="left-1/2 col-auto col-start-2 row-auto row-start-2 w-full max-w-md -translate-x-1/2 rounded-xl bg-white pb-0 text-left shadow-xl transition-all dark:bg-gray-700 dark:text-gray-100"
|
||||
tabIndex={-1}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
|
||||
<div className="flex">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow flex-col gap-1">
|
||||
<h2
|
||||
id="radix-:rf6:"
|
||||
className="text-token-text-primary text-lg font-medium leading-6"
|
||||
>
|
||||
Authentication
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 sm:p-6 sm:pt-0">
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-sm font-medium">Authentication Type</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthTypeEnum.None}
|
||||
onValueChange={(value) => setValue('type', value)}
|
||||
value={type}
|
||||
role="radiogroup"
|
||||
aria-required="false"
|
||||
dir="ltr"
|
||||
className="flex gap-4"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rf8:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.None}
|
||||
id=":rf8:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rfa:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.ServiceHttp}
|
||||
id=":rfa:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
API Key
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<label htmlFor=":rfc:" className="flex cursor-not-allowed items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
disabled={true}
|
||||
value={AuthTypeEnum.OAuth}
|
||||
id=":rfc:"
|
||||
className="mr-1 flex h-5 w-5 cursor-not-allowed items-center justify-center rounded-full border border-gray-500 bg-gray-300 dark:border-gray-600 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
OAuth
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
{type === 'none' ? null : type === 'service_http' ? <ApiKey /> : <OAuth />}
|
||||
{/* Cancel/Save */}
|
||||
<div className="mt-5 flex flex-col gap-3 sm:mt-4 sm:flex-row-reverse">
|
||||
<button
|
||||
className="btn relative bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
|
||||
onClick={async () => {
|
||||
const result = await trigger(undefined, { shouldFocus: true });
|
||||
setValue('saved_auth_fields', result);
|
||||
setOpenAuthDialog(!result);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">Save</div>
|
||||
</button>
|
||||
<DialogPrimitive.Close className="btn btn-neutral relative">
|
||||
<div className="flex w-full items-center justify-center gap-2">Cancel</div>
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const ApiKey = () => {
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const authorization_type = watch('authorization_type');
|
||||
const type = watch('type');
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">API Key</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Auth Type</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
onValueChange={(value) => setValue('authorization_type', value)}
|
||||
value={authorization_type}
|
||||
role="radiogroup"
|
||||
aria-required="true"
|
||||
dir="ltr"
|
||||
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rfu:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Basic}
|
||||
id=":rfu:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Basic
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rg0:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Bearer}
|
||||
id=":rg0:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Bearer
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rg2:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Custom}
|
||||
id=":rg2:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Custom
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
{authorization_type === AuthorizationTypeEnum.Custom && (
|
||||
<div className="mt-2">
|
||||
<label className="mb-1 block text-sm font-medium">Custom Header Name</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
placeholder="X-Api-Key"
|
||||
{...register('custom_auth_header', {
|
||||
required: authorization_type === AuthorizationTypeEnum.Custom,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OAuth = () => {
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const token_exchange_method = watch('token_exchange_method');
|
||||
const type = watch('type');
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">Client ID</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('oauth_client_id', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Client Secret</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('oauth_client_secret', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Authorization URL</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('authorization_url', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Token URL</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('client_url', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Scope</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('scope', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Token Exchange Method</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
onValueChange={(value) => setValue('token_exchange_method', value)}
|
||||
value={token_exchange_method}
|
||||
role="radiogroup"
|
||||
aria-required="true"
|
||||
dir="ltr"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rj1:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={TokenExchangeMethodEnum.DefaultPost}
|
||||
id=":rj1:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Default (POST request)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rj3:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={TokenExchangeMethodEnum.BasicAuthHeader}
|
||||
id=":rj3:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Basic authorization header
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
285
client/src/components/SidePanel/Agents/ActionsInput.tsx
Normal file
285
client/src/components/SidePanel/Agents/ActionsInput.tsx
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
validateAndParseOpenAPISpec,
|
||||
openapiToFunction,
|
||||
AuthTypeEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
Action,
|
||||
FunctionTool,
|
||||
ActionMetadata,
|
||||
ValidationResult,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ActionAuthForm } from '~/common';
|
||||
import type { Spec } from './ActionsTable';
|
||||
import { ActionsTable, columns } from './ActionsTable';
|
||||
import { useUpdateAgentAction } from '~/data-provider';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { Spinner } from '~/components/svg';
|
||||
|
||||
const debouncedValidation = debounce(
|
||||
(input: string, callback: (result: ValidationResult) => void) => {
|
||||
const result = validateAndParseOpenAPISpec(input);
|
||||
callback(result);
|
||||
},
|
||||
800,
|
||||
);
|
||||
|
||||
export default function ActionsInput({
|
||||
action,
|
||||
agent_id,
|
||||
setAction,
|
||||
}: {
|
||||
action?: Action;
|
||||
agent_id?: string;
|
||||
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
|
||||
}) {
|
||||
const handleResult = (result: ValidationResult) => {
|
||||
if (!result.status) {
|
||||
setData(null);
|
||||
setFunctions(null);
|
||||
}
|
||||
setValidationResult(result);
|
||||
};
|
||||
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { handleSubmit, reset } = useFormContext<ActionAuthForm>();
|
||||
const [validationResult, setValidationResult] = useState<null | ValidationResult>(null);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const [data, setData] = useState<Spec[] | null>(null);
|
||||
const [functions, setFunctions] = useState<FunctionTool[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!action?.metadata?.raw_spec) {
|
||||
return;
|
||||
}
|
||||
setInputValue(action.metadata.raw_spec);
|
||||
debouncedValidation(action.metadata.raw_spec, handleResult);
|
||||
}, [action?.metadata?.raw_spec]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!validationResult || !validationResult.status || !validationResult.spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { functionSignatures, requestBuilders } = openapiToFunction(validationResult.spec);
|
||||
const specs = Object.entries(requestBuilders).map(([name, props]) => {
|
||||
return {
|
||||
name,
|
||||
method: props.method,
|
||||
path: props.path,
|
||||
domain: props.domain,
|
||||
};
|
||||
});
|
||||
|
||||
setData(specs);
|
||||
setValidationResult(null);
|
||||
setFunctions(functionSignatures.map((f) => f.toObjectTool()));
|
||||
}, [validationResult]);
|
||||
|
||||
const updateAgentAction = useUpdateAgentAction({
|
||||
onSuccess(data) {
|
||||
showToast({
|
||||
message: localize('com_assistants_update_actions_success'),
|
||||
status: 'success',
|
||||
});
|
||||
reset();
|
||||
setAction(data[1]);
|
||||
},
|
||||
onError(error) {
|
||||
showToast({
|
||||
message: (error as Error)?.message ?? localize('com_assistants_update_actions_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const saveAction = handleSubmit((authFormData) => {
|
||||
console.log('authFormData', authFormData);
|
||||
if (!agent_id) {
|
||||
// alert user?
|
||||
return;
|
||||
}
|
||||
|
||||
if (!functions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { metadata = {} } = action ?? {};
|
||||
const action_id = action?.action_id;
|
||||
metadata.raw_spec = inputValue;
|
||||
const parsedUrl = new URL(data[0].domain);
|
||||
const domain = parsedUrl.hostname;
|
||||
if (!domain) {
|
||||
// alert user?
|
||||
return;
|
||||
}
|
||||
metadata.domain = domain;
|
||||
|
||||
const { type, saved_auth_fields } = authFormData;
|
||||
|
||||
const removeSensitiveFields = (obj: ActionMetadata) => {
|
||||
delete obj.auth;
|
||||
delete obj.api_key;
|
||||
delete obj.oauth_client_id;
|
||||
delete obj.oauth_client_secret;
|
||||
};
|
||||
|
||||
if (saved_auth_fields && type === AuthTypeEnum.ServiceHttp) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
api_key: authFormData.api_key,
|
||||
auth: {
|
||||
type,
|
||||
authorization_type: authFormData.authorization_type,
|
||||
custom_auth_header: authFormData.custom_auth_header,
|
||||
},
|
||||
};
|
||||
} else if (saved_auth_fields && type === AuthTypeEnum.OAuth) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
auth: {
|
||||
type,
|
||||
authorization_url: authFormData.authorization_url,
|
||||
client_url: authFormData.client_url,
|
||||
scope: authFormData.scope,
|
||||
token_exchange_method: authFormData.token_exchange_method,
|
||||
},
|
||||
oauth_client_id: authFormData.oauth_client_id,
|
||||
oauth_client_secret: authFormData.oauth_client_secret,
|
||||
};
|
||||
} else if (saved_auth_fields) {
|
||||
removeSensitiveFields(metadata);
|
||||
metadata.auth = {
|
||||
type,
|
||||
};
|
||||
} else {
|
||||
removeSensitiveFields(metadata);
|
||||
}
|
||||
|
||||
updateAgentAction.mutate({
|
||||
action_id,
|
||||
metadata,
|
||||
functions,
|
||||
agent_id,
|
||||
});
|
||||
});
|
||||
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLTextAreaElement> = (event) => {
|
||||
const newValue = event.target.value;
|
||||
setInputValue(newValue);
|
||||
if (!newValue) {
|
||||
setData(null);
|
||||
setFunctions(null);
|
||||
return setValidationResult(null);
|
||||
}
|
||||
debouncedValidation(newValue, handleResult);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
<div className="mb-1 flex flex-wrap items-center justify-between gap-4">
|
||||
<label className="text-token-text-primary whitespace-nowrap font-medium">Schema</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <button className="btn btn-neutral border-token-border-light relative h-8 min-w-[100px] rounded-lg font-medium">
|
||||
<div className="flex w-full items-center justify-center text-xs">Import from URL</div>
|
||||
</button> */}
|
||||
<select
|
||||
onChange={(e) => console.log(e.target.value)}
|
||||
className="border-token-border-medium h-8 min-w-[100px] rounded-lg border bg-transparent px-2 py-0 text-sm"
|
||||
>
|
||||
<option value="label">{localize('com_ui_examples')}</option>
|
||||
{/* TODO: make these appear and function correctly */}
|
||||
<option value="0">Weather (JSON)</option>
|
||||
<option value="1">Pet Store (YAML)</option>
|
||||
<option value="2">Blank Template</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-token-border-light mb-4 overflow-hidden rounded-lg border">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
spellCheck="false"
|
||||
placeholder="Enter your OpenAPI schema here"
|
||||
className={cn(
|
||||
'text-token-text-primary block h-96 w-full border-none bg-transparent p-2 font-mono text-xs',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
{/* TODO: format input button */}
|
||||
</div>
|
||||
{validationResult && validationResult.message !== 'OpenAPI spec is valid.' && (
|
||||
<div className="border-token-border-light border-t p-2 text-red-500">
|
||||
{validationResult.message.split('\n').map((line: string, i: number) => (
|
||||
<div key={i}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!data && (
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_available_actions')}
|
||||
</label>
|
||||
</div>
|
||||
<ActionsTable columns={columns} data={data} />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<span className="" data-state="closed">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_privacy_policy')}
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-md border border-gray-300 px-3 py-2 shadow-none focus-within:border-gray-800 focus-within:ring-1 focus-within:ring-gray-800 dark:border-gray-700 dark:bg-gray-700 dark:focus-within:border-gray-500 dark:focus-within:ring-gray-500">
|
||||
<label
|
||||
htmlFor="privacyPolicyUrl"
|
||||
className="block text-xs font-medium text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
name="privacyPolicyUrl"
|
||||
id="privacyPolicyUrl"
|
||||
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 shadow-none outline-none focus-within:shadow-none focus-within:outline-none focus-within:ring-0 focus:border-none focus:ring-0 dark:bg-gray-700 dark:text-gray-100 sm:text-sm"
|
||||
placeholder="https://api.example-weather-app.com/privacy"
|
||||
// value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
disabled={!functions || !functions.length}
|
||||
onClick={saveAction}
|
||||
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400"
|
||||
type="button"
|
||||
>
|
||||
{updateAgentAction.isLoading ? (
|
||||
<Spinner className="icon-md" />
|
||||
) : action?.action_id ? (
|
||||
localize('com_ui_update')
|
||||
) : (
|
||||
localize('com_ui_create')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
198
client/src/components/SidePanel/Agents/ActionsPanel.tsx
Normal file
198
client/src/components/SidePanel/Agents/ActionsPanel.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import type { AgentPanelProps, ActionAuthForm } from '~/common';
|
||||
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useDeleteAgentAction } from '~/data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import ActionsInput from './ActionsInput';
|
||||
import ActionsAuth from './ActionsAuth';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function ActionsPanel({
|
||||
// activePanel,
|
||||
action,
|
||||
setAction,
|
||||
agent_id,
|
||||
setActivePanel,
|
||||
}: AgentPanelProps) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const [openAuthDialog, setOpenAuthDialog] = useState(false);
|
||||
const deleteAgentAction = useDeleteAgentAction({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: localize('com_assistants_delete_actions_success'),
|
||||
status: 'success',
|
||||
});
|
||||
setActivePanel(Panel.builder);
|
||||
setAction(undefined);
|
||||
},
|
||||
onError(error) {
|
||||
showToast({
|
||||
message: (error as Error)?.message ?? localize('com_assistants_delete_actions_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const methods = useForm<ActionAuthForm>({
|
||||
defaultValues: {
|
||||
/* General */
|
||||
type: AuthTypeEnum.None,
|
||||
saved_auth_fields: false,
|
||||
/* API key */
|
||||
api_key: '',
|
||||
authorization_type: AuthorizationTypeEnum.Basic,
|
||||
custom_auth_header: '',
|
||||
/* OAuth */
|
||||
oauth_client_id: '',
|
||||
oauth_client_secret: '',
|
||||
authorization_url: '',
|
||||
client_url: '',
|
||||
scope: '',
|
||||
token_exchange_method: TokenExchangeMethodEnum.DefaultPost,
|
||||
},
|
||||
});
|
||||
|
||||
const { reset, watch } = methods;
|
||||
const type = watch('type');
|
||||
|
||||
useEffect(() => {
|
||||
if (action?.metadata?.auth) {
|
||||
reset({
|
||||
type: action.metadata.auth.type || AuthTypeEnum.None,
|
||||
saved_auth_fields: false,
|
||||
api_key: action.metadata.api_key ?? '',
|
||||
authorization_type: action.metadata.auth.authorization_type || AuthorizationTypeEnum.Basic,
|
||||
oauth_client_id: action.metadata.oauth_client_id ?? '',
|
||||
oauth_client_secret: action.metadata.oauth_client_secret ?? '',
|
||||
authorization_url: action.metadata.auth.authorization_url ?? '',
|
||||
client_url: action.metadata.auth.client_url ?? '',
|
||||
scope: action.metadata.auth.scope ?? '',
|
||||
token_exchange_method:
|
||||
action.metadata.auth.token_exchange_method ?? TokenExchangeMethodEnum.DefaultPost,
|
||||
});
|
||||
}
|
||||
}, [action, reset]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className="h-full grow overflow-hidden">
|
||||
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
||||
<div className="relative flex flex-col items-center px-16 py-6 text-center">
|
||||
<div className="absolute left-0 top-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-neutral relative"
|
||||
onClick={() => {
|
||||
setActivePanel(Panel.builder);
|
||||
setAction(undefined);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<ChevronLeft />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!!action && (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<div className="absolute right-0 top-6">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id || !action.action_id}
|
||||
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
||||
>
|
||||
<TrashIcon className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_action')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_action_confirm')}
|
||||
</Label>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: () => {
|
||||
if (!agent_id) {
|
||||
return showToast({
|
||||
message: 'No agent_id found, is the agent created?',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
deleteAgentAction.mutate({
|
||||
action_id: action.action_id,
|
||||
agent_id,
|
||||
});
|
||||
},
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
)}
|
||||
|
||||
<div className="text-xl font-medium">{(action ? 'Edit' : 'Add') + ' ' + 'actions'}</div>
|
||||
<div className="text-token-text-tertiary text-sm">
|
||||
{localize('com_assistants_actions_info')}
|
||||
</div>
|
||||
{/* <div className="text-sm text-token-text-tertiary">
|
||||
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
|
||||
</div> */}
|
||||
</div>
|
||||
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<div className="relative mb-6">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_authentication')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="border-token-border-medium flex rounded-lg border text-sm hover:cursor-pointer">
|
||||
<div className="h-9 grow px-3 py-2">{type}</div>
|
||||
<div className="bg-token-border-medium w-px"></div>
|
||||
<button type="button" color="neutral" className="flex items-center gap-2 px-3">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-sm"
|
||||
>
|
||||
<path
|
||||
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
|
||||
</Dialog>
|
||||
<ActionsInput action={action} agent_id={agent_id} setAction={setAction} />
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
export type Spec = {
|
||||
name: string;
|
||||
method: string;
|
||||
path: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export const fakeData: Spec[] = [
|
||||
{
|
||||
name: 'listPets',
|
||||
method: 'get',
|
||||
path: '/pets',
|
||||
domain: 'petstore.swagger.io',
|
||||
},
|
||||
{
|
||||
name: 'createPets',
|
||||
method: 'post',
|
||||
path: '/pets',
|
||||
domain: 'petstore.swagger.io',
|
||||
},
|
||||
{
|
||||
name: 'showPetById',
|
||||
method: 'get',
|
||||
path: '/pets/{petId}',
|
||||
domain: 'petstore.swagger.io',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: ColumnDef<Spec>[] = [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Method',
|
||||
accessorKey: 'method',
|
||||
},
|
||||
{
|
||||
header: 'Path',
|
||||
accessorKey: 'path',
|
||||
},
|
||||
// {
|
||||
// header: '',
|
||||
// accessorKey: 'action',
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// cell: ({ row: _row }) => (
|
||||
// <button className="btn relative btn-neutral h-8 rounded-lg border-token-border-light font-medium">
|
||||
// <div className="flex w-full gap-2 items-center justify-center">Test</div>
|
||||
// </button>
|
||||
// ),
|
||||
// },
|
||||
];
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { useReactTable, flexRender, getCoreRowModel } from '@tanstack/react-table';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className="border-token-border-light text-token-text-tertiary border-b text-left text-xs"
|
||||
>
|
||||
{headerGroup.headers.map((header, j) => (
|
||||
<th key={j} className="py-1 font-normal">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row, i) => (
|
||||
<tr key={i} className="border-token-border-light border-b">
|
||||
{row.getVisibleCells().map((cell, j) => (
|
||||
<td key={j} className="py-2">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ActionsTable } from './Table';
|
||||
export * from './Columns';
|
||||
196
client/src/components/SidePanel/Agents/AgentAvatar.tsx
Normal file
196
client/src/components/SidePanel/Agents/AgentAvatar.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
fileConfig as defaultFileConfig,
|
||||
QueryKeys,
|
||||
defaultOrderQuery,
|
||||
mergeFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type {
|
||||
Agent,
|
||||
AgentAvatar,
|
||||
AgentCreateParams,
|
||||
AgentListResponse,
|
||||
} from 'librechat-data-provider';
|
||||
import { useUploadAgentAvatarMutation, useGetFileConfig } from '~/data-provider';
|
||||
import { AgentAvatarRender, NoImage, AvatarMenu } from './Images';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { formatBytes } from '~/utils';
|
||||
|
||||
function Avatar({
|
||||
agent_id,
|
||||
avatar,
|
||||
createMutation,
|
||||
}: {
|
||||
agent_id: string | null;
|
||||
avatar: null | AgentAvatar;
|
||||
createMutation: UseMutationResult<Agent, Error, AgentCreateParams>;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [progress, setProgress] = useState<number>(1);
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const lastSeenCreatedId = useRef<string | null>(null);
|
||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const { mutate: uploadAvatar } = useUploadAgentAvatarMutation({
|
||||
onMutate: () => {
|
||||
setProgress(0.4);
|
||||
},
|
||||
onSuccess: (data, vars) => {
|
||||
if (vars.postCreation === false) {
|
||||
showToast({ message: localize('com_ui_upload_success') });
|
||||
} else if (lastSeenCreatedId.current !== createMutation.data?.id) {
|
||||
lastSeenCreatedId.current = createMutation.data?.id ?? '';
|
||||
}
|
||||
|
||||
setInput(null);
|
||||
setPreviewUrl(data.avatar?.filepath as string | null);
|
||||
|
||||
const res = queryClient.getQueryData<AgentListResponse>([
|
||||
QueryKeys.agents,
|
||||
defaultOrderQuery,
|
||||
]);
|
||||
|
||||
if (!res?.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agents =
|
||||
res.data.map((agent) => {
|
||||
if (agent.id === agent_id) {
|
||||
return {
|
||||
...agent,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
return agent;
|
||||
}) ?? [];
|
||||
|
||||
queryClient.setQueryData<AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
|
||||
...res,
|
||||
data: agents,
|
||||
});
|
||||
|
||||
setProgress(1);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error:', error);
|
||||
setInput(null);
|
||||
setPreviewUrl(null);
|
||||
showToast({ message: localize('com_ui_upload_error'), status: 'error' });
|
||||
setProgress(1);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (input) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPreviewUrl(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(input);
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatar) {
|
||||
setPreviewUrl((avatar.filepath as string | undefined) ?? null);
|
||||
}
|
||||
}, [avatar]);
|
||||
|
||||
useEffect(() => {
|
||||
/** Experimental: Condition to prime avatar upload before Agent Creation
|
||||
* - If the createMutation state Id was last seen (current) and the createMutation is successful
|
||||
* we can assume that the avatar upload has already been initiated and we can skip the upload
|
||||
*
|
||||
* The mutation state is not reset until the user deliberately selects a new agent or an agent is deleted
|
||||
*
|
||||
* This prevents the avatar from being uploaded multiple times before the user selects a new agent
|
||||
* while allowing the user to upload to prime the avatar and other values before the agent is created.
|
||||
*/
|
||||
const sharedUploadCondition = !!(
|
||||
createMutation.isSuccess &&
|
||||
input &&
|
||||
previewUrl &&
|
||||
previewUrl.includes('base64')
|
||||
);
|
||||
if (sharedUploadCondition && lastSeenCreatedId.current === createMutation.data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sharedUploadCondition && createMutation.data.id) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', input, input.name);
|
||||
formData.append('agent_id', createMutation.data.id);
|
||||
|
||||
if (typeof createMutation.data.avatar === 'object') {
|
||||
formData.append('avatar', JSON.stringify(createMutation.data.avatar));
|
||||
}
|
||||
|
||||
uploadAvatar({
|
||||
agent_id: createMutation.data.id,
|
||||
postCreation: true,
|
||||
formData,
|
||||
});
|
||||
}
|
||||
}, [createMutation.data, createMutation.isSuccess, input, previewUrl, uploadAvatar]);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
if (fileConfig.avatarSizeLimit && file && file.size <= fileConfig.avatarSizeLimit) {
|
||||
setInput(file);
|
||||
setMenuOpen(false);
|
||||
|
||||
if (!agent_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
formData.append('agent_id', agent_id);
|
||||
|
||||
if (typeof avatar === 'object') {
|
||||
formData.append('avatar', JSON.stringify(avatar));
|
||||
}
|
||||
|
||||
uploadAvatar({
|
||||
agent_id,
|
||||
formData,
|
||||
});
|
||||
} else {
|
||||
const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
||||
showToast({
|
||||
message: localize('com_ui_upload_invalid_var', megabytes + ''),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
setMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<div className="flex w-full items-center justify-center gap-4">
|
||||
<Popover.Trigger asChild>
|
||||
<button type="button" className="h-20 w-20">
|
||||
{previewUrl ? <AgentAvatarRender url={previewUrl} progress={progress} /> : <NoImage />}
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
</div>
|
||||
{<AvatarMenu handleFileChange={handleFileChange} />}
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
366
client/src/components/SidePanel/Agents/AgentConfig.tsx
Normal file
366
client/src/components/SidePanel/Agents/AgentConfig.tsx
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Controller, useWatch, useFormContext } from 'react-hook-form';
|
||||
import { QueryKeys, Capabilities, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TConfig, TPlugin } from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentPanelProps } from '~/common';
|
||||
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
|
||||
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
|
||||
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||
import Action from '~/components/SidePanel/Builder/Action';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ToolSelectDialog } from '~/components/Tools';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import ContextButton from './ContextButton';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import AgentTool from './AgentTool';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||
const inputClass = cn(
|
||||
defaultTextProps,
|
||||
'flex w-full px-3 py-2 border-border-light bg-surface-secondary focus-visible:ring-2 focus-visible:ring-ring-primary',
|
||||
removeFocusOutlines,
|
||||
);
|
||||
|
||||
export default function AgentConfig({
|
||||
setAction,
|
||||
actions = [],
|
||||
agentsConfig,
|
||||
endpointsConfig,
|
||||
setActivePanel,
|
||||
setCurrentAgentId,
|
||||
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||
|
||||
const methods = useFormContext<AgentForm>();
|
||||
|
||||
const { control } = methods;
|
||||
const provider = useWatch({ control, name: 'provider' });
|
||||
const model = useWatch({ control, name: 'model' });
|
||||
const agent = useWatch({ control, name: 'agent' });
|
||||
const tools = useWatch({ control, name: 'tools' });
|
||||
const agent_id = useWatch({ control, name: 'id' });
|
||||
|
||||
const toolsEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(Capabilities.tools),
|
||||
[agentsConfig],
|
||||
);
|
||||
const actionsEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(Capabilities.actions),
|
||||
[agentsConfig],
|
||||
);
|
||||
const retrievalEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(Capabilities.retrieval),
|
||||
[agentsConfig],
|
||||
);
|
||||
const codeEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(Capabilities.code_interpreter),
|
||||
[agentsConfig],
|
||||
);
|
||||
|
||||
/* Mutations */
|
||||
const update = useUpdateAgentMutation({
|
||||
onSuccess: (data) => {
|
||||
showToast({
|
||||
message: `${localize('com_assistants_update_success')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error;
|
||||
showToast({
|
||||
message: `${localize('com_agents_update_error')}${
|
||||
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||
}`,
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const create = useCreateAgentMutation({
|
||||
onSuccess: (data) => {
|
||||
setCurrentAgentId(data.id);
|
||||
showToast({
|
||||
message: `${localize('com_assistants_create_success ')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error;
|
||||
showToast({
|
||||
message: `${localize('com_agents_create_error')}${
|
||||
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||
}`,
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddActions = useCallback(() => {
|
||||
if (!agent_id) {
|
||||
showToast({
|
||||
message: localize('com_assistants_actions_disabled'),
|
||||
status: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
setActivePanel(Panel.actions);
|
||||
}, [agent_id, setActivePanel, showToast, localize]);
|
||||
|
||||
// Provider Icon logic
|
||||
|
||||
const providerValue = typeof provider === 'string' ? provider : provider?.value;
|
||||
let endpointType: EModelEndpoint | undefined;
|
||||
let endpointIconURL: string | undefined;
|
||||
let iconKey: string | undefined;
|
||||
let Icon:
|
||||
| React.ComponentType<
|
||||
React.SVGProps<SVGSVGElement> & {
|
||||
endpoint: string;
|
||||
endpointType: EModelEndpoint | undefined;
|
||||
iconURL: string | undefined;
|
||||
}
|
||||
>
|
||||
| undefined;
|
||||
|
||||
if (providerValue !== undefined) {
|
||||
endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type');
|
||||
endpointIconURL = getEndpointField(endpointsConfig, providerValue as string, 'iconURL');
|
||||
iconKey = getIconKey({
|
||||
endpoint: providerValue as string,
|
||||
endpointsConfig,
|
||||
endpointType,
|
||||
endpointIconURL,
|
||||
});
|
||||
Icon = icons[iconKey];
|
||||
}
|
||||
|
||||
const renderSaveButton = () => {
|
||||
if (create.isLoading || update.isLoading) {
|
||||
return <Spinner className="icon-md" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
if (agent_id) {
|
||||
return localize('com_ui_save');
|
||||
}
|
||||
|
||||
return localize('com_ui_create');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
|
||||
{/* Avatar & Name */}
|
||||
<div className="mb-4">
|
||||
<AgentAvatar
|
||||
createMutation={create}
|
||||
agent_id={agent_id}
|
||||
avatar={agent?.['avatar'] ?? null}
|
||||
/>
|
||||
<label className={labelClass} htmlFor="name">
|
||||
{localize('com_ui_name')}
|
||||
</label>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
maxLength={256}
|
||||
className={inputClass}
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder={localize('com_agents_name_placeholder')}
|
||||
aria-label="Agent name"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="id"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<p className="h-3 text-xs italic text-gray-600" aria-live="polite">
|
||||
{field.value}
|
||||
</p>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Description */}
|
||||
<div className="mb-4">
|
||||
<label className={labelClass} htmlFor="description">
|
||||
{localize('com_ui_description')}
|
||||
</label>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
maxLength={512}
|
||||
className={inputClass}
|
||||
id="description"
|
||||
type="text"
|
||||
placeholder={localize('com_agents_description_placeholder')}
|
||||
aria-label="Agent description"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Instructions */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass} htmlFor="instructions">
|
||||
{localize('com_ui_instructions')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Controller
|
||||
name="instructions"
|
||||
control={control}
|
||||
rules={{ required: true, minLength: 1 }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<>
|
||||
<textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
maxLength={32768}
|
||||
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
||||
id="instructions"
|
||||
placeholder={localize('com_agents_instructions_placeholder')}
|
||||
rows={3}
|
||||
aria-label="Agent instructions"
|
||||
aria-required="true"
|
||||
aria-invalid={error ? 'true' : 'false'}
|
||||
/>
|
||||
{error && (
|
||||
<span
|
||||
className="text-sm text-red-500 transition duration-300 ease-in-out"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_ui_field_required')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Model and Provider */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass} htmlFor="provider">
|
||||
{localize('com_ui_model')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActivePanel(Panel.model)}
|
||||
className="btn btn-neutral border-token-border-light relative h-10 w-full rounded-lg font-medium"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{Icon && (
|
||||
<div className="shadow-stroke relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
||||
<Icon
|
||||
className="h-2/3 w-2/3"
|
||||
endpoint={provider as string}
|
||||
endpointType={endpointType}
|
||||
iconURL={endpointIconURL}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span>{model ? model : localize('com_ui_select_model')}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/* Agent Tools & Actions */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass}>
|
||||
{`${toolsEnabled ? localize('com_assistants_tools') : ''}
|
||||
${toolsEnabled && actionsEnabled ? ' + ' : ''}
|
||||
${actionsEnabled ? localize('com_assistants_actions') : ''}`}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{tools?.map((func, i) => (
|
||||
<AgentTool
|
||||
key={`${func}-${i}-${agent_id}`}
|
||||
tool={func}
|
||||
allTools={allTools}
|
||||
agent_id={agent_id}
|
||||
/>
|
||||
))}
|
||||
{actions
|
||||
.filter((action) => action.agent_id === agent_id)
|
||||
.map((action, i) => (
|
||||
<Action
|
||||
key={i}
|
||||
action={action}
|
||||
onClick={() => {
|
||||
setAction(action);
|
||||
setActivePanel(Panel.actions);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<div className="flex space-x-2">
|
||||
{(toolsEnabled ?? false) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToolDialog(true)}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_assistants_add_tools')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{(actionsEnabled ?? false) && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id}
|
||||
onClick={handleAddActions}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_assistants_add_actions')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Context Button */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<ContextButton
|
||||
agent_id={agent_id}
|
||||
setCurrentAgentId={setCurrentAgentId}
|
||||
createMutation={create}
|
||||
/>
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||
type="submit"
|
||||
disabled={create.isLoading || update.isLoading}
|
||||
aria-busy={create.isLoading || update.isLoading}
|
||||
>
|
||||
{renderSaveButton()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ToolSelectDialog
|
||||
isOpen={showToolDialog}
|
||||
setIsOpen={setShowToolDialog}
|
||||
toolsFormKey="tools"
|
||||
endpoint={EModelEndpoint.agents}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
207
client/src/components/SidePanel/Agents/AgentPanel.tsx
Normal file
207
client/src/components/SidePanel/Agents/AgentPanel.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
Tools,
|
||||
EModelEndpoint,
|
||||
isAssistantsEndpoint,
|
||||
defaultAgentFormValues,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentPanelProps, Option } from '~/common';
|
||||
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
|
||||
import { useSelectAgent, useLocalize } from '~/hooks';
|
||||
// import CapabilitiesForm from './CapabilitiesForm';
|
||||
import { createProviderOption } from '~/utils';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import AgentConfig from './AgentConfig';
|
||||
import AgentSelect from './AgentSelect';
|
||||
import ModelPanel from './ModelPanel';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function AgentPanel({
|
||||
setAction,
|
||||
activePanel,
|
||||
actions = [],
|
||||
setActivePanel,
|
||||
agent_id: current_agent_id,
|
||||
setCurrentAgentId,
|
||||
agentsConfig,
|
||||
endpointsConfig,
|
||||
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
|
||||
const { onSelect: onSelectAgent } = useSelectAgent();
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: defaultAgentFormValues,
|
||||
});
|
||||
|
||||
const { control, handleSubmit, reset } = methods;
|
||||
const agent_id = useWatch({ control, name: 'id' });
|
||||
|
||||
const providers = useMemo(
|
||||
() =>
|
||||
Object.keys(endpointsConfig ?? {})
|
||||
.filter(
|
||||
(key) =>
|
||||
!isAssistantsEndpoint(key) &&
|
||||
key !== EModelEndpoint.agents &&
|
||||
key !== EModelEndpoint.chatGPTBrowser &&
|
||||
key !== EModelEndpoint.gptPlugins &&
|
||||
key !== EModelEndpoint.bingAI,
|
||||
)
|
||||
.map((provider) => createProviderOption(provider)),
|
||||
[endpointsConfig],
|
||||
);
|
||||
|
||||
/* Mutations */
|
||||
const update = useUpdateAgentMutation({
|
||||
onSuccess: (data) => {
|
||||
showToast({
|
||||
message: `${localize('com_assistants_update_success')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error;
|
||||
showToast({
|
||||
message: `${localize('com_agents_update_error')}${
|
||||
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||
}`,
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const create = useCreateAgentMutation({
|
||||
onSuccess: (data) => {
|
||||
setCurrentAgentId(data.id);
|
||||
showToast({
|
||||
message: `${localize('com_assistants_create_success ')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error;
|
||||
showToast({
|
||||
message: `${localize('com_agents_create_error')}${
|
||||
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||
}`,
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: AgentForm) => {
|
||||
const tools = data.tools ?? [];
|
||||
|
||||
if (data.code_interpreter) {
|
||||
tools.push(Tools.code_interpreter);
|
||||
}
|
||||
if (data.retrieval) {
|
||||
tools.push(Tools.file_search);
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
model,
|
||||
model_parameters,
|
||||
provider: _provider,
|
||||
description,
|
||||
instructions,
|
||||
} = data;
|
||||
|
||||
const provider = typeof _provider === 'string' ? _provider : (_provider as Option).value;
|
||||
|
||||
if (agent_id) {
|
||||
update.mutate({
|
||||
agent_id,
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
model,
|
||||
tools,
|
||||
provider,
|
||||
model_parameters,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
create.mutate({
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
model,
|
||||
tools,
|
||||
provider,
|
||||
model_parameters,
|
||||
});
|
||||
},
|
||||
[agent_id, create, update],
|
||||
);
|
||||
|
||||
const handleSelectAgent = useCallback(() => {
|
||||
if (agent_id) {
|
||||
onSelectAgent(agent_id);
|
||||
}
|
||||
}, [agent_id, onSelectAgent]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
|
||||
aria-label="Agent configuration form"
|
||||
>
|
||||
<div className="flex w-full flex-wrap">
|
||||
<Controller
|
||||
name="agent"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<AgentSelect
|
||||
reset={reset}
|
||||
value={field.value}
|
||||
setCurrentAgentId={setCurrentAgentId}
|
||||
selectedAgentId={current_agent_id ?? null}
|
||||
createMutation={create}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{/* Select Button */}
|
||||
{agent_id && (
|
||||
<button
|
||||
className="btn btn-primary focus:shadow-outline mx-2 mt-1 h-[40px] rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
type="button"
|
||||
disabled={!agent_id}
|
||||
onClick={handleSelectAgent}
|
||||
aria-label="Select agent"
|
||||
>
|
||||
{localize('com_ui_select')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{activePanel === Panel.model ? (
|
||||
<ModelPanel setActivePanel={setActivePanel} providers={providers} models={models} />
|
||||
) : null}
|
||||
{activePanel === Panel.builder ? (
|
||||
<AgentConfig
|
||||
actions={actions}
|
||||
setAction={setAction}
|
||||
agentsConfig={agentsConfig}
|
||||
setActivePanel={setActivePanel}
|
||||
endpointsConfig={endpointsConfig}
|
||||
setCurrentAgentId={setCurrentAgentId}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
59
client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx
Normal file
59
client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Capabilities } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { ActionsEndpoint } from '~/common';
|
||||
import type { Action, TConfig, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { useGetActionsQuery } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import ActionsPanel from './ActionsPanel';
|
||||
import AgentPanel from './AgentPanel';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function AgentPanelSwitch() {
|
||||
const { conversation, index } = useChatContext();
|
||||
const [activePanel, setActivePanel] = useState(Panel.builder);
|
||||
const [action, setAction] = useState<Action | undefined>(undefined);
|
||||
const [currentAgentId, setCurrentAgentId] = useState<string | undefined>(conversation?.agent_id);
|
||||
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as ActionsEndpoint);
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const agentsConfig = useMemo(
|
||||
() =>
|
||||
// endpointsConfig?.[EModelEndpoint.agents] ??
|
||||
({
|
||||
// for testing purposes
|
||||
capabilities: [Capabilities.tools, Capabilities.actions],
|
||||
} as TConfig),
|
||||
// [endpointsConfig]);
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation?.agent_id) {
|
||||
setCurrentAgentId(conversation?.agent_id);
|
||||
}
|
||||
}, [conversation?.agent_id]);
|
||||
|
||||
if (!conversation?.endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonProps = {
|
||||
index,
|
||||
action,
|
||||
actions,
|
||||
setAction,
|
||||
activePanel,
|
||||
setActivePanel,
|
||||
setCurrentAgentId,
|
||||
agent_id: currentAgentId,
|
||||
};
|
||||
|
||||
if (activePanel === Panel.actions) {
|
||||
return <ActionsPanel {...commonProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AgentPanel {...commonProps} agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />
|
||||
);
|
||||
}
|
||||
182
client/src/components/SidePanel/Agents/AgentSelect.tsx
Normal file
182
client/src/components/SidePanel/Agents/AgentSelect.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { Plus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
|
||||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { UseFormReset } from 'react-hook-form';
|
||||
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
|
||||
import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider';
|
||||
import SelectDropDown from '~/components/ui/SelectDropDown';
|
||||
// import { useFileMapContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const keys = new Set(Object.keys(defaultAgentFormValues));
|
||||
|
||||
export default function AgentSelect({
|
||||
reset,
|
||||
value: currentAgentValue,
|
||||
selectedAgentId,
|
||||
setCurrentAgentId,
|
||||
createMutation,
|
||||
}: {
|
||||
reset: UseFormReset<AgentForm>;
|
||||
value?: TAgentOption;
|
||||
selectedAgentId: string | null;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
createMutation: UseMutationResult<Agent, Error, AgentCreateParams>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
// TODO: file handling for agents
|
||||
// const fileMap = useFileMapContext();
|
||||
const lastSelectedAgent = useRef<string | null>(null);
|
||||
|
||||
const { data: agents = [] } = useListAgentsQuery(undefined, {
|
||||
select: (res) => res.data.map((agent) => processAgentOption(agent /*, fileMap */)),
|
||||
});
|
||||
|
||||
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
|
||||
enabled: !!selectedAgentId,
|
||||
});
|
||||
|
||||
const resetAgentForm = useCallback(
|
||||
(fullAgent: Agent) => {
|
||||
const update = {
|
||||
...fullAgent,
|
||||
provider: createProviderOption(fullAgent.provider),
|
||||
label: fullAgent.name ?? '',
|
||||
value: fullAgent.id ?? '',
|
||||
};
|
||||
|
||||
const actions: AgentCapabilities = {
|
||||
[Capabilities.code_interpreter]: false,
|
||||
[Capabilities.image_vision]: false,
|
||||
[Capabilities.retrieval]: false,
|
||||
};
|
||||
|
||||
const formValues: Partial<AgentForm & AgentCapabilities> = {
|
||||
...actions,
|
||||
agent: update,
|
||||
model: update.model,
|
||||
tools: update.tools ?? [],
|
||||
};
|
||||
|
||||
Object.entries(fullAgent).forEach(([name, value]) => {
|
||||
if (name === 'model_parameters') {
|
||||
formValues[name] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keys.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'number' && typeof value !== 'object') {
|
||||
formValues[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
reset(formValues);
|
||||
},
|
||||
[reset],
|
||||
);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(selectedId: string) => {
|
||||
const agentExists = !!(selectedId
|
||||
? agents.find((agent) => agent.id === selectedId)
|
||||
: undefined);
|
||||
|
||||
createMutation.reset();
|
||||
if (!agentExists) {
|
||||
setCurrentAgentId(undefined);
|
||||
return reset({
|
||||
...defaultAgentFormValues,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentAgentId(selectedId);
|
||||
const agent = agentQuery.data;
|
||||
if (!agent) {
|
||||
console.warn('Agent not found');
|
||||
return;
|
||||
}
|
||||
|
||||
resetAgentForm(agent);
|
||||
},
|
||||
[agents, createMutation, setCurrentAgentId, agentQuery.data, resetAgentForm, reset],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (agentQuery.data && agentQuery.isSuccess) {
|
||||
resetAgentForm(agentQuery.data);
|
||||
}
|
||||
}, [agentQuery.data, agentQuery.isSuccess, resetAgentForm]);
|
||||
|
||||
useEffect(() => {
|
||||
let timerId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (selectedAgentId === lastSelectedAgent.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAgentId && agents) {
|
||||
timerId = setTimeout(() => {
|
||||
lastSelectedAgent.current = selectedAgentId;
|
||||
onSelect(selectedAgentId);
|
||||
}, 5);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
};
|
||||
}, [selectedAgentId, agents, onSelect]);
|
||||
|
||||
const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent');
|
||||
const hasAgentValue = !!(typeof currentAgentValue === 'object'
|
||||
? currentAgentValue.value
|
||||
: currentAgentValue);
|
||||
return (
|
||||
<SelectDropDown
|
||||
value={!hasAgentValue ? createAgent : (currentAgentValue as TAgentOption)}
|
||||
setValue={createDropdownSetter(onSelect)}
|
||||
availableValues={
|
||||
agents ?? [
|
||||
{
|
||||
label: 'Loading...',
|
||||
value: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
iconSide="left"
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
emptyTitle={true}
|
||||
containerClassName="flex-grow"
|
||||
searchClassName="dark:from-gray-850"
|
||||
searchPlaceholder={localize('com_agents_search_name')}
|
||||
optionsClass="hover:bg-gray-20/50 dark:border-gray-700"
|
||||
optionsListClass="rounded-lg shadow-lg dark:bg-gray-850 dark:border-gray-700 dark:last:border"
|
||||
currentValueClass={cn(
|
||||
'text-md font-semibold text-gray-900 dark:text-white',
|
||||
hasAgentValue ? 'text-gray-500' : '',
|
||||
)}
|
||||
className={cn(
|
||||
'mt-1 rounded-md dark:border-gray-700 dark:bg-gray-850',
|
||||
'z-50 flex h-[40px] w-full flex-none items-center justify-center px-4 hover:cursor-pointer hover:border-green-500 focus:border-gray-400',
|
||||
)}
|
||||
renderOption={() => (
|
||||
<span className="flex items-center gap-1.5 truncate">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2 text-gray-800 dark:text-gray-100">
|
||||
<Plus className="w-[16px]" />
|
||||
</span>
|
||||
<span className={cn('ml-4 flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100')}>
|
||||
{createAgent}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
104
client/src/components/SidePanel/Agents/AgentTool.tsx
Normal file
104
client/src/components/SidePanel/Agents/AgentTool.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function AgentTool({
|
||||
tool,
|
||||
allTools,
|
||||
agent_id,
|
||||
}: {
|
||||
tool: string;
|
||||
allTools: TPlugin[];
|
||||
agent_id?: string;
|
||||
}) {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||
const { getValues, setValue } = useFormContext();
|
||||
const currentTool = allTools.find((t) => t.pluginKey === tool);
|
||||
|
||||
const removeTool = (tool: string) => {
|
||||
if (tool) {
|
||||
updateUserPlugins.mutate(
|
||||
{ pluginKey: tool, action: 'uninstall', auth: null, isAgentTool: true },
|
||||
{
|
||||
onError: (error: unknown) => {
|
||||
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
|
||||
},
|
||||
onSuccess: () => {
|
||||
const tools = getValues('tools').filter((fn: string) => fn !== tool);
|
||||
setValue('tools', tools);
|
||||
showToast({ message: 'Tool deleted successfully', status: 'success' });
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentTool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OGDialog>
|
||||
<div
|
||||
className={cn('flex w-full items-center rounded-lg text-sm', !agent_id ? 'opacity-40' : '')}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<div className="flex grow items-center">
|
||||
{currentTool.icon && (
|
||||
<div className="flex h-9 w-9 items-center justify-center overflow-hidden rounded-full">
|
||||
<div
|
||||
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
|
||||
style={{ backgroundImage: `url(${currentTool.icon})`, backgroundSize: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="h-9 grow px-3 py-2"
|
||||
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
|
||||
>
|
||||
{currentTool.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isHovering && (
|
||||
<OGDialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="transition-color flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</OGDialogTrigger>
|
||||
)}
|
||||
</div>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_tool')}
|
||||
mainClassName="px-0"
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_tool_confirm')}
|
||||
</Label>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: () => removeTool(currentTool.pluginKey),
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
60
client/src/components/SidePanel/Agents/CapabilitiesForm.tsx
Normal file
60
client/src/components/SidePanel/Agents/CapabilitiesForm.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useMemo } from 'react';
|
||||
// import { Capabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import type { TConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
// import ImageVision from './ImageVision';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Retrieval from './Retrieval';
|
||||
import CodeFiles from './CodeFiles';
|
||||
import Code from './Code';
|
||||
|
||||
export default function CapabilitiesForm({
|
||||
codeEnabled,
|
||||
retrievalEnabled,
|
||||
agentsConfig,
|
||||
}: {
|
||||
codeEnabled?: boolean;
|
||||
retrievalEnabled?: boolean;
|
||||
agentsConfig?: TConfig | null;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control } = methods;
|
||||
const agent = useWatch({ control, name: 'agent' });
|
||||
const agent_id = useWatch({ control, name: 'id' });
|
||||
const files = useMemo(() => {
|
||||
if (typeof agent === 'string') {
|
||||
return [];
|
||||
}
|
||||
return agent?.code_files;
|
||||
}, [agent]);
|
||||
|
||||
const retrievalModels = useMemo(
|
||||
() => new Set(agentsConfig?.retrievalModels ?? []),
|
||||
[agentsConfig],
|
||||
);
|
||||
// const imageVisionEnabled = useMemo(
|
||||
// () => agentsConfig?.capabilities?.includes(Capabilities.image_vision),
|
||||
// [agentsConfig],
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<span>
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_capabilities')}
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{codeEnabled && <Code />}
|
||||
{retrievalEnabled && <Retrieval retrievalModels={retrievalModels} />}
|
||||
{/* {imageVisionEnabled && version == 1 && <ImageVision />} */}
|
||||
{codeEnabled && <CodeFiles agent_id={agent_id} files={files} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
client/src/components/SidePanel/Agents/Code.tsx
Normal file
66
client/src/components/SidePanel/Agents/Code.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Capabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import {
|
||||
Checkbox,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardPortal,
|
||||
HoverCardTrigger,
|
||||
} from '~/components/ui';
|
||||
import { CircleHelpIcon } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
export default function Code() {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={50}>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.code_interpreter}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={Capabilities.code_interpreter}
|
||||
onClick={() =>
|
||||
setValue(Capabilities.code_interpreter, !getValues(Capabilities.code_interpreter), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{localize('com_assistants_code_interpreter')}
|
||||
</label>
|
||||
<HoverCardTrigger>
|
||||
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
||||
</HoverCardTrigger>
|
||||
</div>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={ESide.Top} className="w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{/* // TODO: add a Code Interpreter description */}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
95
client/src/components/SidePanel/Agents/CodeFiles.tsx
Normal file
95
client/src/components/SidePanel/Agents/CodeFiles.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { useFileHandling } from '~/hooks/Files';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
const tool_resource = EToolResources.code_interpreter;
|
||||
|
||||
export default function CodeFiles({
|
||||
agent_id,
|
||||
files: _files,
|
||||
}: {
|
||||
agent_id: string;
|
||||
files?: [string, ExtendedFile][];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
const { handleFileChange } = useFileHandling({
|
||||
overrideEndpoint: EModelEndpoint.agents,
|
||||
additionalMetadata: { agent_id, tool_resource },
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (_files) {
|
||||
setFiles(new Map(_files));
|
||||
}
|
||||
}, [_files]);
|
||||
|
||||
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
|
||||
|
||||
if (endpointFileConfig?.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleButtonClick = () => {
|
||||
// necessary to reset the input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2 w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-token-text-tertiary rounded-lg text-xs">
|
||||
{localize('com_assistants_code_interpreter_files')}
|
||||
</div>
|
||||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
agent_id={agent_id}
|
||||
tool_resource={tool_resource}
|
||||
setFilesLoading={setFilesLoading}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<input
|
||||
multiple={true}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
tabIndex={-1}
|
||||
ref={fileInputRef}
|
||||
disabled={!agent_id}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{localize('com_ui_upload_files')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
client/src/components/SidePanel/Agents/ContextButton.tsx
Normal file
110
client/src/components/SidePanel/Agents/ContextButton.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useLocalize, useSetIndexOptions } from '~/hooks';
|
||||
import { useDeleteAgentMutation } from '~/data-provider';
|
||||
import { cn, removeFocusOutlines } from '~/utils/';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
|
||||
export default function ContextButton({
|
||||
agent_id,
|
||||
setCurrentAgentId,
|
||||
createMutation,
|
||||
}: {
|
||||
agent_id: string;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
createMutation: UseMutationResult<Agent, Error, AgentCreateParams>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { conversation } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
|
||||
const deleteAgent = useDeleteAgentMutation({
|
||||
onSuccess: (_, vars, context) => {
|
||||
const updatedList = context as Agent[] | undefined;
|
||||
if (!updatedList) {
|
||||
return;
|
||||
}
|
||||
|
||||
showToast({
|
||||
message: localize('com_ui_agent_deleted'),
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
if (createMutation.data?.id) {
|
||||
console.log('[deleteAgent] resetting createMutation');
|
||||
createMutation.reset();
|
||||
}
|
||||
|
||||
const firstAgent = updatedList[0] as Agent | undefined;
|
||||
if (!firstAgent) {
|
||||
return setOption('agent_id')('');
|
||||
}
|
||||
|
||||
if (vars.agent_id === conversation?.agent_id) {
|
||||
setOption('model')('');
|
||||
return setOption('agent_id')(firstAgent.id);
|
||||
}
|
||||
|
||||
const currentAgent = updatedList?.find((agent) => agent.id === conversation?.agent_id);
|
||||
|
||||
if (currentAgent) {
|
||||
setCurrentAgentId(currentAgent.id);
|
||||
}
|
||||
|
||||
setCurrentAgentId(firstAgent.id);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
showToast({
|
||||
message: localize('com_ui_agent_delete_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!agent_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-red-500">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
</button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_ui_delete') + ' ' + localize('com_ui_agent')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<>
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="delete-agent" className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_agent_confirm')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: () => deleteAgent.mutate({ agent_id }),
|
||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
40
client/src/components/SidePanel/Agents/ImageVision.tsx
Normal file
40
client/src/components/SidePanel/Agents/ImageVision.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { Capabilities } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
import { Checkbox } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function ImageVision() {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.image_vision}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={Capabilities.image_vision}
|
||||
onClick={() =>
|
||||
setValue(Capabilities.image_vision, !getValues(Capabilities.image_vision), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">{localize('com_assistants_image_vision')}</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
client/src/components/SidePanel/Agents/Images.tsx
Normal file
135
client/src/components/SidePanel/Agents/Images.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { useRef } from 'react';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export function NoImage() {
|
||||
return (
|
||||
<div className="border-token-border-medium flex h-full w-full items-center justify-center rounded-full border-2 border-dashed border-black">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-4xl"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const AgentAvatarRender = ({
|
||||
url,
|
||||
progress = 1,
|
||||
}: {
|
||||
url?: string;
|
||||
progress: number; // between 0 and 1
|
||||
}) => {
|
||||
const radius = 55; // Radius of the SVG circle
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
// Calculate the offset based on the loading progress
|
||||
const offset = circumference - progress * circumference;
|
||||
const circleCSSProperties = {
|
||||
transition: 'stroke-dashoffset 0.3s linear',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative h-20 w-20 overflow-hidden rounded-full">
|
||||
<img
|
||||
src={url}
|
||||
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
|
||||
alt="GPT"
|
||||
width="80"
|
||||
height="80"
|
||||
style={{ opacity: progress < 1 ? 0.4 : 1 }}
|
||||
/>
|
||||
{progress < 1 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/5 text-white">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" className="h-6 w-6">
|
||||
<circle
|
||||
className="origin-[50%_50%] -rotate-90 stroke-gray-400"
|
||||
strokeWidth="10"
|
||||
fill="transparent"
|
||||
r="55"
|
||||
cx="60"
|
||||
cy="60"
|
||||
/>
|
||||
<circle
|
||||
className="origin-[50%_50%] -rotate-90 transition-[stroke-dashoffset]"
|
||||
stroke="currentColor"
|
||||
strokeWidth="10"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={offset}
|
||||
fill="transparent"
|
||||
r="55"
|
||||
cx="60"
|
||||
cy="60"
|
||||
style={circleCSSProperties}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function AvatarMenu({
|
||||
handleFileChange,
|
||||
}: {
|
||||
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onItemClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="flex min-w-[100px] max-w-xs flex-col rounded-xl border border-gray-400 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-850 dark:text-white"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div
|
||||
role="menuitem"
|
||||
className="group m-1.5 flex cursor-pointer gap-2 rounded p-2.5 text-sm hover:bg-gray-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-800 dark:hover:bg-white/5"
|
||||
tabIndex={-1}
|
||||
data-orientation="vertical"
|
||||
onClick={onItemClick}
|
||||
>
|
||||
{localize('com_ui_upload_image')}
|
||||
</div>
|
||||
{/* <Popover.Close
|
||||
role="menuitem"
|
||||
className="group m-1.5 flex cursor-pointer gap-2 rounded p-2.5 text-sm hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
||||
tabIndex={-1}
|
||||
data-orientation="vertical"
|
||||
>
|
||||
Use DALL·E
|
||||
</Popover.Close> */}
|
||||
<input
|
||||
accept="image/png,.png,image/jpeg,.jpg,.jpeg,image/gif,.gif,image/webp,.webp"
|
||||
multiple={false}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
ref={fileInputRef}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
);
|
||||
}
|
||||
283
client/src/components/SidePanel/Agents/ModelPanel.tsx
Normal file
283
client/src/components/SidePanel/Agents/ModelPanel.tsx
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { AgentForm, AgentModelPanelProps } from '~/common';
|
||||
import { SelectDropDown, ModelParameters } from '~/components/ui';
|
||||
import { cn, cardStyle } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function ModelPanel({
|
||||
setActivePanel,
|
||||
providers,
|
||||
models: modelsData,
|
||||
}: AgentModelPanelProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const { control, setValue, watch } = useFormContext<AgentForm>();
|
||||
const model = watch('model');
|
||||
const providerOption = watch('provider');
|
||||
|
||||
const provider = useMemo(() => {
|
||||
if (!providerOption) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return typeof providerOption === 'string' ? providerOption : providerOption.value;
|
||||
}, [providerOption]);
|
||||
const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && model) {
|
||||
const modelExists = models.includes(model);
|
||||
if (!modelExists) {
|
||||
const newModels = modelsData[provider];
|
||||
setValue('model', newModels[0] ?? '');
|
||||
}
|
||||
}
|
||||
}, [provider, models, modelsData, setValue, model]);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
||||
<div className="model-panel relative flex flex-col items-center px-16 py-6 text-center">
|
||||
<div className="absolute left-0 top-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-neutral relative"
|
||||
onClick={() => {
|
||||
setActivePanel(Panel.builder);
|
||||
}}
|
||||
>
|
||||
<div className="model-panel-content flex w-full items-center justify-center gap-2">
|
||||
<ChevronLeft />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_model_parameters')}</div>
|
||||
</div>
|
||||
{/* Endpoint aka Provider for Agents */}
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="text-token-text-primary model-panel-label mb-2 block font-medium"
|
||||
htmlFor="provider"
|
||||
>
|
||||
{localize('com_ui_provider')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Controller
|
||||
name="provider"
|
||||
control={control}
|
||||
rules={{ required: true, minLength: 1 }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<>
|
||||
<SelectDropDown
|
||||
emptyTitle={true}
|
||||
value={field.value ?? ''}
|
||||
placeholder={localize('com_ui_select_provider')}
|
||||
setValue={field.onChange}
|
||||
availableValues={providers}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
|
||||
!field.value && 'border-2 border-yellow-400',
|
||||
)}
|
||||
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
|
||||
/>
|
||||
{error && (
|
||||
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
|
||||
{localize('com_ui_field_required')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Model */}
|
||||
<div className="model-panel-section mb-6">
|
||||
<label
|
||||
className={cn(
|
||||
'text-token-text-primary model-panel-label mb-2 block font-medium',
|
||||
!provider && 'text-gray-500 dark:text-gray-400',
|
||||
)}
|
||||
htmlFor="model"
|
||||
>
|
||||
{localize('com_ui_model')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Controller
|
||||
name="model"
|
||||
control={control}
|
||||
rules={{ required: true, minLength: 1 }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<>
|
||||
<SelectDropDown
|
||||
emptyTitle={true}
|
||||
placeholder={
|
||||
provider
|
||||
? localize('com_ui_select_model')
|
||||
: localize('com_ui_select_provider_first')
|
||||
}
|
||||
value={field.value}
|
||||
setValue={field.onChange}
|
||||
availableValues={models}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
disabled={!provider}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4',
|
||||
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer',
|
||||
)}
|
||||
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
|
||||
/>
|
||||
{provider && error && (
|
||||
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
|
||||
{localize('com_ui_field_required')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="model_parameters.temperature"
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<ModelParameters
|
||||
label="com_endpoint_temperature"
|
||||
ariaLabel="Temperature"
|
||||
min={-2}
|
||||
max={2}
|
||||
step={0.01}
|
||||
stepClick={0.01}
|
||||
initialValue={field.value ?? 1}
|
||||
onChange={field.onChange}
|
||||
showButtons={true}
|
||||
disabled={!provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="model_parameters.max_context_tokens"
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<ModelParameters
|
||||
label="com_endpoint_max_output_tokens"
|
||||
ariaLabel="Max Context Tokens"
|
||||
min={0}
|
||||
max={4096}
|
||||
step={1}
|
||||
stepClick={1}
|
||||
initialValue={field.value ?? 0}
|
||||
onChange={field.onChange}
|
||||
showButtons={true}
|
||||
disabled={!provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="model_parameters.max_output_tokens"
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<ModelParameters
|
||||
label="com_endpoint_context_tokens"
|
||||
ariaLabel="Max Context Tokens"
|
||||
min={0}
|
||||
max={4096}
|
||||
step={1}
|
||||
stepClick={1}
|
||||
initialValue={field.value ?? 0}
|
||||
onChange={field.onChange}
|
||||
showButtons={true}
|
||||
disabled={!provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="model_parameters.top_p"
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<ModelParameters
|
||||
label="com_endpoint_top_p"
|
||||
ariaLabel="Top P"
|
||||
min={-2}
|
||||
max={2}
|
||||
step={0.01}
|
||||
stepClick={0.01}
|
||||
initialValue={field.value ?? 1}
|
||||
onChange={field.onChange}
|
||||
showButtons={true}
|
||||
disabled={!provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="model_parameters.frequency_penalty"
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<ModelParameters
|
||||
label="com_endpoint_frequency_penalty"
|
||||
ariaLabel="Frequency Penalty"
|
||||
min={-2}
|
||||
max={2}
|
||||
step={0.01}
|
||||
stepClick={0.01}
|
||||
initialValue={field.value ?? 0}
|
||||
onChange={field.onChange}
|
||||
showButtons={true}
|
||||
disabled={!provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
name="model_parameters.presence_penalty"
|
||||
control={control}
|
||||
rules={{ required: false }}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<ModelParameters
|
||||
label="com_endpoint_presence_penalty"
|
||||
ariaLabel="Presence Penalty"
|
||||
min={-2}
|
||||
max={2}
|
||||
step={0.01}
|
||||
stepClick={0.01}
|
||||
initialValue={field.value ?? 0}
|
||||
onChange={field.onChange}
|
||||
showButtons={true}
|
||||
disabled={!provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
client/src/components/SidePanel/Agents/Retrieval.tsx
Normal file
91
client/src/components/SidePanel/Agents/Retrieval.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { Capabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
Checkbox,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardPortal,
|
||||
HoverCardTrigger,
|
||||
} from '~/components/ui';
|
||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import { CircleHelpIcon } from '~/components/svg';
|
||||
import type { AgentForm } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ESide } from '~/common';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function Retrieval({ retrievalModels }: { retrievalModels: Set<string> }) {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
const model = useWatch({ control, name: 'model' });
|
||||
|
||||
const isDisabled = useMemo(() => !retrievalModels.has(model), [model, retrievalModels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (model && isDisabled) {
|
||||
setValue(Capabilities.retrieval, false);
|
||||
}
|
||||
}, [model, setValue, isDisabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={50}>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.retrieval}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
disabled={isDisabled}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label
|
||||
className={cn(
|
||||
'form-check-label text-token-text-primary w-full select-none',
|
||||
isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
htmlFor={Capabilities.retrieval}
|
||||
onClick={() =>
|
||||
retrievalModels.has(model) &&
|
||||
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{localize('com_assistants_file_search')}
|
||||
</label>
|
||||
<HoverCardTrigger>
|
||||
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
||||
</HoverCardTrigger>
|
||||
</div>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={ESide.Top} disabled={isDisabled} className="ml-16 w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{/* // TODO: Add description for file search */}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
<OptionHover
|
||||
side={ESide.Top}
|
||||
disabled={!isDisabled}
|
||||
description="com_assistants_non_retrieval_model"
|
||||
langCode={true}
|
||||
sideOffset={20}
|
||||
className="ml-16"
|
||||
/>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,13 +2,7 @@ import { useState } from 'react';
|
|||
import type { Action } from 'librechat-data-provider';
|
||||
import GearIcon from '~/components/svg/GearIcon';
|
||||
|
||||
export default function AssistantAction({
|
||||
action,
|
||||
onClick,
|
||||
}: {
|
||||
action: Action;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
export default function Action({ action, onClick }: { action: Action; onClick: () => void }) {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import type { AssistantPanelProps, ActionAuthForm } from '~/common';
|
||||
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
||||
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
|
|
@ -102,22 +103,7 @@ export default function ActionsPanel({
|
|||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-md"
|
||||
>
|
||||
<path
|
||||
d="M15 5L8 12L15 19"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<ChevronLeft />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ import CapabilitiesForm from './CapabilitiesForm';
|
|||
import { SelectDropDown } from '~/components/ui';
|
||||
import AssistantAvatar from './AssistantAvatar';
|
||||
import AssistantSelect from './AssistantSelect';
|
||||
import AssistantAction from './AssistantAction';
|
||||
import ContextButton from './ContextButton';
|
||||
import AssistantTool from './AssistantTool';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import Knowledge from './Knowledge';
|
||||
import { Panel } from '~/common';
|
||||
import Action from './Action';
|
||||
|
||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||
const inputClass = cn(
|
||||
|
|
@ -42,6 +42,7 @@ export default function AssistantPanel({
|
|||
endpoint,
|
||||
actions = [],
|
||||
setActivePanel,
|
||||
documentsMap,
|
||||
assistant_id: current_assistant_id,
|
||||
setCurrentAssistantId,
|
||||
assistantsConfig,
|
||||
|
|
@ -222,6 +223,7 @@ export default function AssistantPanel({
|
|||
reset={reset}
|
||||
value={field.value}
|
||||
endpoint={endpoint}
|
||||
documentsMap={documentsMap}
|
||||
setCurrentAssistantId={setCurrentAssistantId}
|
||||
selectedAssistant={current_assistant_id ?? null}
|
||||
createMutation={create}
|
||||
|
|
@ -373,7 +375,7 @@ export default function AssistantPanel({
|
|||
/>
|
||||
</div>
|
||||
{/* Knowledge */}
|
||||
{(codeEnabled || retrievalEnabled) && version == 1 && (
|
||||
{(codeEnabled === true || retrievalEnabled === true) && version == 1 && (
|
||||
<Knowledge assistant_id={assistant_id} files={files} endpoint={endpoint} />
|
||||
)}
|
||||
{/* Capabilities */}
|
||||
|
|
@ -387,9 +389,9 @@ export default function AssistantPanel({
|
|||
{/* Tools */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass}>
|
||||
{`${toolsEnabled ? localize('com_assistants_tools') : ''}
|
||||
${toolsEnabled && actionsEnabled ? ' + ' : ''}
|
||||
${actionsEnabled ? localize('com_assistants_actions') : ''}`}
|
||||
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
|
||||
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
|
||||
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{functions.map((func, i) => (
|
||||
|
|
@ -403,12 +405,10 @@ export default function AssistantPanel({
|
|||
{actions
|
||||
.filter((action) => action.assistant_id === assistant_id)
|
||||
.map((action, i) => {
|
||||
return (
|
||||
<AssistantAction key={i} action={action} onClick={() => setAction(action)} />
|
||||
);
|
||||
return <Action key={i} action={action} onClick={() => setAction(action)} />;
|
||||
})}
|
||||
<div className="flex space-x-2">
|
||||
{toolsEnabled && (
|
||||
{toolsEnabled === true && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToolDialog(true)}
|
||||
|
|
@ -419,7 +419,7 @@ export default function AssistantPanel({
|
|||
</div>
|
||||
</button>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
{actionsEnabled === true && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!assistant_id}
|
||||
|
|
@ -463,7 +463,7 @@ export default function AssistantPanel({
|
|||
<ToolSelectDialog
|
||||
isOpen={showToolDialog}
|
||||
setIsOpen={setShowToolDialog}
|
||||
assistant_id={assistant_id}
|
||||
toolsFormKey="functions"
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Plus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import {
|
||||
Tools,
|
||||
FileSources,
|
||||
|
|
@ -13,9 +13,9 @@ import type { UseFormReset } from 'react-hook-form';
|
|||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type {
|
||||
Assistant,
|
||||
AssistantCreateParams,
|
||||
AssistantDocument,
|
||||
AssistantsEndpoint,
|
||||
AssistantCreateParams,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
Actions,
|
||||
|
|
@ -24,11 +24,11 @@ import type {
|
|||
TAssistantOption,
|
||||
LastSelectedModels,
|
||||
} from '~/common';
|
||||
import { useListAssistantsQuery, useGetAssistantDocsQuery } from '~/data-provider';
|
||||
import SelectDropDown from '~/components/ui/SelectDropDown';
|
||||
import { useListAssistantsQuery } from '~/data-provider';
|
||||
import { useLocalize, useLocalStorage } from '~/hooks';
|
||||
import { cn, createDropdownSetter } from '~/utils';
|
||||
import { useFileMapContext } from '~/Providers';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const keys = new Set([
|
||||
'name',
|
||||
|
|
@ -43,6 +43,7 @@ export default function AssistantSelect({
|
|||
reset,
|
||||
value,
|
||||
endpoint,
|
||||
documentsMap,
|
||||
selectedAssistant,
|
||||
setCurrentAssistantId,
|
||||
createMutation,
|
||||
|
|
@ -51,6 +52,7 @@ export default function AssistantSelect({
|
|||
value: TAssistantOption;
|
||||
endpoint: AssistantsEndpoint;
|
||||
selectedAssistant: string | null;
|
||||
documentsMap: Map<string, AssistantDocument> | null;
|
||||
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
|
||||
}) {
|
||||
|
|
@ -62,14 +64,7 @@ export default function AssistantSelect({
|
|||
{} as LastSelectedModels,
|
||||
);
|
||||
|
||||
const { data: documentsMap = new Map<string, AssistantDocument>() } = useGetAssistantDocsQuery(
|
||||
endpoint,
|
||||
{
|
||||
select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])),
|
||||
},
|
||||
);
|
||||
|
||||
const assistants = useListAssistantsQuery(endpoint, undefined, {
|
||||
const query = useListAssistantsQuery(endpoint, undefined, {
|
||||
select: (res) =>
|
||||
res.data.map((_assistant) => {
|
||||
const source =
|
||||
|
|
@ -128,7 +123,7 @@ export default function AssistantSelect({
|
|||
);
|
||||
}
|
||||
|
||||
const assistantDoc = documentsMap.get(_assistant.id);
|
||||
const assistantDoc = documentsMap?.get(_assistant.id);
|
||||
/* If no user updates, use the latest assistant docs */
|
||||
if (assistantDoc && !assistant.conversation_starters) {
|
||||
assistant.conversation_starters = assistantDoc.conversation_starters;
|
||||
|
|
@ -140,7 +135,7 @@ export default function AssistantSelect({
|
|||
|
||||
const onSelect = useCallback(
|
||||
(value: string) => {
|
||||
const assistant = assistants.data?.find((assistant) => assistant.id === value);
|
||||
const assistant = query.data?.find((assistant) => assistant.id === value);
|
||||
|
||||
createMutation.reset();
|
||||
if (!assistant) {
|
||||
|
|
@ -206,7 +201,7 @@ export default function AssistantSelect({
|
|||
reset(formValues);
|
||||
setCurrentAssistantId(assistant.id);
|
||||
},
|
||||
[assistants.data, reset, setCurrentAssistantId, createMutation, endpoint, lastSelectedModels],
|
||||
[query.data, reset, setCurrentAssistantId, createMutation, endpoint, lastSelectedModels],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -216,7 +211,7 @@ export default function AssistantSelect({
|
|||
return;
|
||||
}
|
||||
|
||||
if (selectedAssistant !== '' && selectedAssistant != null && assistants.data) {
|
||||
if (selectedAssistant !== '' && selectedAssistant != null && query.data) {
|
||||
timerId = setTimeout(() => {
|
||||
lastSelectedAssistant.current = selectedAssistant;
|
||||
onSelect(selectedAssistant);
|
||||
|
|
@ -228,15 +223,15 @@ export default function AssistantSelect({
|
|||
clearTimeout(timerId);
|
||||
}
|
||||
};
|
||||
}, [selectedAssistant, assistants.data, onSelect]);
|
||||
}, [selectedAssistant, query.data, onSelect]);
|
||||
|
||||
const createAssistant = localize('com_ui_create') + ' ' + localize('com_ui_assistant');
|
||||
return (
|
||||
<SelectDropDown
|
||||
value={!value ? createAssistant : value}
|
||||
setValue={onSelect}
|
||||
setValue={createDropdownSetter(onSelect)}
|
||||
availableValues={
|
||||
assistants.data ?? [
|
||||
query.data ?? [
|
||||
{
|
||||
label: 'Loading...',
|
||||
value: '',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { defaultAssistantsVersion } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { Action, AssistantsEndpoint, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { useGetActionsQuery } from '~/data-provider';
|
||||
import type { Action, TEndpointsConfig, AssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { ActionsEndpoint } from '~/common';
|
||||
import { useGetActionsQuery, useGetAssistantDocsQuery } from '~/data-provider';
|
||||
import AssistantPanel from './AssistantPanel';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import ActionsPanel from './ActionsPanel';
|
||||
|
|
@ -17,7 +18,10 @@ export default function PanelSwitch() {
|
|||
);
|
||||
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as AssistantsEndpoint);
|
||||
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as ActionsEndpoint);
|
||||
const { data: documentsMap = null } = useGetAssistantDocsQuery(conversation?.endpoint ?? '', {
|
||||
select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])),
|
||||
});
|
||||
|
||||
const assistantsConfig = useMemo(
|
||||
() => endpointsConfig?.[conversation?.endpoint ?? ''],
|
||||
|
|
@ -25,8 +29,9 @@ export default function PanelSwitch() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation?.assistant_id) {
|
||||
setCurrentAssistantId(conversation?.assistant_id);
|
||||
const currentId = conversation?.assistant_id ?? '';
|
||||
if (currentId) {
|
||||
setCurrentAssistantId(currentId);
|
||||
}
|
||||
}, [conversation?.assistant_id]);
|
||||
|
||||
|
|
@ -44,6 +49,7 @@ export default function PanelSwitch() {
|
|||
actions={actions}
|
||||
setAction={setAction}
|
||||
activePanel={activePanel}
|
||||
documentsMap={documentsMap}
|
||||
setActivePanel={setActivePanel}
|
||||
assistant_id={currentAssistantId}
|
||||
setCurrentAssistantId={setCurrentAssistantId}
|
||||
|
|
@ -59,6 +65,7 @@ export default function PanelSwitch() {
|
|||
action={action}
|
||||
actions={actions}
|
||||
setAction={setAction}
|
||||
documentsMap={documentsMap}
|
||||
setActivePanel={setActivePanel}
|
||||
assistant_id={currentAssistantId}
|
||||
setCurrentAssistantId={setCurrentAssistantId}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ const SidePanel = ({
|
|||
}, []);
|
||||
|
||||
const assistants = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]);
|
||||
const agents = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]);
|
||||
|
||||
const userProvidesKey = useMemo(
|
||||
() => !!(endpointsConfig?.[endpoint ?? '']?.userProvide ?? false),
|
||||
[endpointsConfig, endpoint],
|
||||
|
|
@ -102,10 +104,11 @@ const SidePanel = ({
|
|||
}, []);
|
||||
|
||||
const Links = useSideNavLinks({
|
||||
agents,
|
||||
endpoint,
|
||||
hidePanel,
|
||||
assistants,
|
||||
keyProvided,
|
||||
endpoint,
|
||||
interfaceConfig,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
|
||||
import type { SwitcherProps } from '~/common';
|
||||
import { Separator } from '~/components/ui/Separator';
|
||||
import AssistantSwitcher from './AssistantSwitcher';
|
||||
import AgentSwitcher from './AgentSwitcher';
|
||||
import ModelSwitcher from './ModelSwitcher';
|
||||
|
||||
export default function Switcher(props: SwitcherProps) {
|
||||
|
|
@ -12,6 +13,13 @@ export default function Switcher(props: SwitcherProps) {
|
|||
<Separator className="max-w-[98%] bg-surface-tertiary" />
|
||||
</>
|
||||
);
|
||||
} else if (isAgentsEndpoint(props.endpoint) && props.endpointKeyProvided) {
|
||||
return (
|
||||
<>
|
||||
<AgentSwitcher {...props} />
|
||||
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
||||
</>
|
||||
);
|
||||
} else if (isAssistantsEndpoint(props.endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue