mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 20:00:15 +01:00
🚧 chore: merge latest dev build (#4288)
* fix: agent initialization, add `collectedUsage` handling * style: improve side panel styling * refactor(loadAgent): Optimize order agent project ID retrieval * feat: code execution * fix: typing issues * feat: ExecuteCode content part * refactor: use local state for default collapsed state of analysis content parts * fix: code parsing in ExecuteCode component * chore: bump agents package, export loadAuthValues * refactor: Update handleTools.js to use EnvVar for code execution tool authentication * WIP * feat: download code outputs * fix(useEventHandlers): type issues * feat: backend handling for code outputs * Refactor: Remove console.log statement in Part.tsx * refactor: add attachments to TMessage/messageSchema * WIP: prelim handling for code outputs * feat: attachments rendering * refactor: improve attachments rendering * fix: attachments, nullish edge case, handle attachments from event stream, bump agents package * fix filename download * fix: tool assignment for 'run code' on agent creation * fix: image handling by adding attachments * refactor: prevent agent creation without provider/model * refactor: remove unnecessary space in agent creation success message * refactor: select first model if selecting provider from empty on form * fix: Agent avatar bug * fix: `defaultAgentFormValues` causing boolean typing issue and typeerror * fix: capabilities counting as tools, causing duplication of them * fix: formatted messages edge case where consecutive content text type parts with the latter having tool_call_ids would cause consecutive AI messages to be created. furthermore, content could not be an array for tool_use messages (anthropic limitation) * chore: bump @librechat/agents dependency to version 1.6.9 * feat: bedrock agents * feat: new Agents icon * feat: agent titling * feat: agent landing * refactor: allow sharing agent globally only if user is admin or author * feat: initial AgentPanelSkeleton * feat: AgentPanelSkeleton * feat: collaborative agents * chore: add potential authorName as part of schema * chore: Remove unnecessary console.log statement * WIP: agent model parameters * chore: ToolsDialog typing and tool related localization chnages * refactor: update tool instance type (latest langchain class), and rename google tool to 'google' proper * chore: add back tools * feat: Agent knowledge files upload * refactor: better verbiage for disabled knowledge * chore: debug logs for file deletions * chore: debug logs for file deletions * feat: upload/delete agent knowledge/file-search files * feat: file search UI for agents * feat: first pass, file search tool * chore: update default agent capabilities and info
This commit is contained in:
parent
f33e75e2ee
commit
ad74350036
123 changed files with 3611 additions and 1541 deletions
|
|
@ -21,7 +21,7 @@ import { useLocalize } from '~/hooks';
|
|||
import { formatBytes } from '~/utils';
|
||||
|
||||
function Avatar({
|
||||
agent_id,
|
||||
agent_id = '',
|
||||
avatar,
|
||||
createMutation,
|
||||
}: {
|
||||
|
|
@ -31,9 +31,9 @@ function Avatar({
|
|||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
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),
|
||||
|
|
@ -54,7 +54,8 @@ function Avatar({
|
|||
}
|
||||
|
||||
setInput(null);
|
||||
setPreviewUrl(data.avatar?.filepath as string | null);
|
||||
const newUrl = data.avatar?.filepath ?? '';
|
||||
setPreviewUrl(newUrl);
|
||||
|
||||
const res = queryClient.getQueryData<AgentListResponse>([
|
||||
QueryKeys.agents,
|
||||
|
|
@ -65,16 +66,15 @@ function Avatar({
|
|||
return;
|
||||
}
|
||||
|
||||
const agents =
|
||||
res.data.map((agent) => {
|
||||
if (agent.id === agent_id) {
|
||||
return {
|
||||
...agent,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
return agent;
|
||||
}) ?? [];
|
||||
const agents = res.data.map((agent) => {
|
||||
if (agent.id === agent_id) {
|
||||
return {
|
||||
...agent,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
return agent;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
|
||||
...res,
|
||||
|
|
@ -86,7 +86,7 @@ function Avatar({
|
|||
onError: (error) => {
|
||||
console.error('Error:', error);
|
||||
setInput(null);
|
||||
setPreviewUrl(null);
|
||||
setPreviewUrl('');
|
||||
showToast({ message: localize('com_ui_upload_error'), status: 'error' });
|
||||
setProgress(1);
|
||||
},
|
||||
|
|
@ -103,8 +103,10 @@ function Avatar({
|
|||
}, [input]);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatar) {
|
||||
setPreviewUrl((avatar.filepath as string | undefined) ?? null);
|
||||
if (avatar && avatar.filepath) {
|
||||
setPreviewUrl(avatar.filepath);
|
||||
} else {
|
||||
setPreviewUrl('');
|
||||
}
|
||||
}, [avatar]);
|
||||
|
||||
|
|
@ -147,29 +149,31 @@ function Avatar({
|
|||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0];
|
||||
const sizeLimit = fileConfig.avatarSizeLimit ?? 0;
|
||||
|
||||
if (fileConfig.avatarSizeLimit && file && file.size <= fileConfig.avatarSizeLimit) {
|
||||
if (sizeLimit && file && file.size <= sizeLimit) {
|
||||
setInput(file);
|
||||
setMenuOpen(false);
|
||||
|
||||
if (!agent_id) {
|
||||
const currentId = agent_id ?? '';
|
||||
if (!currentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
formData.append('agent_id', agent_id);
|
||||
formData.append('agent_id', currentId);
|
||||
|
||||
if (typeof avatar === 'object') {
|
||||
formData.append('avatar', JSON.stringify(avatar));
|
||||
}
|
||||
|
||||
uploadAvatar({
|
||||
agent_id,
|
||||
agent_id: currentId,
|
||||
formData,
|
||||
});
|
||||
} else {
|
||||
const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
||||
const megabytes = sizeLimit ? formatBytes(sizeLimit) : 2;
|
||||
showToast({
|
||||
message: localize('com_ui_upload_invalid_var', megabytes + ''),
|
||||
status: 'error',
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
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 { QueryKeys, AgentCapabilities, EModelEndpoint, SystemRoles } 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 { useToastContext, useFileMapContext } from '~/Providers';
|
||||
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 { useLocalize, useAuthContext } from '~/hooks';
|
||||
import CapabilitiesForm from './CapabilitiesForm';
|
||||
import { processAgentOption } from '~/utils';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import FileSearch from './FileSearch';
|
||||
import ShareAgent from './ShareAgent';
|
||||
import AgentTool from './AgentTool';
|
||||
import { Panel } from '~/common';
|
||||
|
|
@ -33,6 +36,8 @@ export default function AgentConfig({
|
|||
setActivePanel,
|
||||
setCurrentAgentId,
|
||||
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
|
||||
const { user } = useAuthContext();
|
||||
const fileMap = useFileMapContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
|
||||
|
|
@ -51,21 +56,41 @@ export default function AgentConfig({
|
|||
const agent_id = useWatch({ control, name: 'id' });
|
||||
|
||||
const toolsEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(Capabilities.tools),
|
||||
() => agentsConfig?.capabilities?.includes(AgentCapabilities.tools),
|
||||
[agentsConfig],
|
||||
);
|
||||
const actionsEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(Capabilities.actions),
|
||||
() => agentsConfig?.capabilities?.includes(AgentCapabilities.actions),
|
||||
[agentsConfig],
|
||||
);
|
||||
// const retrievalEnabled = useMemo(
|
||||
// () => agentsConfig?.capabilities?.includes(Capabilities.retrieval),
|
||||
// [agentsConfig],
|
||||
// );
|
||||
// const codeEnabled = useMemo(
|
||||
// () => agentsConfig?.capabilities?.includes(Capabilities.code_interpreter),
|
||||
// [agentsConfig],
|
||||
// );
|
||||
const fileSearchEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(AgentCapabilities.file_search) ?? false,
|
||||
[agentsConfig],
|
||||
);
|
||||
const codeEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(AgentCapabilities.execute_code) ?? false,
|
||||
[agentsConfig],
|
||||
);
|
||||
|
||||
const knowledge_files = useMemo(() => {
|
||||
if (typeof agent === 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (agent?.id !== agent_id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (agent.knowledge_files) {
|
||||
return agent.knowledge_files;
|
||||
}
|
||||
|
||||
const _agent = processAgentOption({
|
||||
agent,
|
||||
fileMap,
|
||||
});
|
||||
return _agent.knowledge_files ?? [];
|
||||
}, [agent, agent_id, fileMap]);
|
||||
|
||||
/* Mutations */
|
||||
const update = useUpdateAgentMutation({
|
||||
|
|
@ -118,8 +143,6 @@ export default function AgentConfig({
|
|||
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;
|
||||
|
|
@ -280,10 +303,17 @@ export default function AgentConfig({
|
|||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<CapabilitiesForm
|
||||
codeEnabled={codeEnabled}
|
||||
agentsConfig={agentsConfig}
|
||||
retrievalEnabled={false}
|
||||
/>
|
||||
{/* File Search */}
|
||||
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
|
||||
{/* Agent Tools & Actions */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass}>
|
||||
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
|
||||
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
|
||||
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
|
||||
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
|
||||
</label>
|
||||
|
|
@ -344,11 +374,14 @@ export default function AgentConfig({
|
|||
setCurrentAgentId={setCurrentAgentId}
|
||||
createMutation={create}
|
||||
/>
|
||||
<ShareAgent
|
||||
agent_id={agent_id}
|
||||
agentName={agent?.name ?? ''}
|
||||
projectIds={agent?.projectIds ?? []}
|
||||
/>
|
||||
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && (
|
||||
<ShareAgent
|
||||
agent_id={agent_id}
|
||||
agentName={agent?.name ?? ''}
|
||||
projectIds={agent?.projectIds ?? []}
|
||||
isCollaborative={agent?.isCollaborative}
|
||||
/>
|
||||
)}
|
||||
{/* 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"
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@ import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
|||
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
Tools,
|
||||
SystemRoles,
|
||||
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 type { AgentForm, AgentPanelProps, StringOption } from '~/common';
|
||||
import {
|
||||
useCreateAgentMutation,
|
||||
useUpdateAgentMutation,
|
||||
useGetAgentByIdQuery,
|
||||
} from '~/data-provider';
|
||||
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
|
||||
import AgentPanelSkeleton from './AgentPanelSkeleton';
|
||||
import { createProviderOption } from '~/utils';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import AgentConfig from './AgentConfig';
|
||||
|
|
@ -29,11 +34,17 @@ export default function AgentPanel({
|
|||
agentsConfig,
|
||||
endpointsConfig,
|
||||
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
|
||||
const { onSelect: onSelectAgent } = useSelectAgent();
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const { onSelect: onSelectAgent } = useSelectAgent();
|
||||
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const agentQuery = useGetAgentByIdQuery(current_agent_id ?? '', {
|
||||
enabled: !!(current_agent_id ?? ''),
|
||||
});
|
||||
|
||||
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: defaultAgentFormValues,
|
||||
|
|
@ -81,7 +92,7 @@ export default function AgentPanel({
|
|||
onSuccess: (data) => {
|
||||
setCurrentAgentId(data.id);
|
||||
showToast({
|
||||
message: `${localize('com_assistants_create_success ')} ${
|
||||
message: `${localize('com_assistants_create_success')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
|
|
@ -101,23 +112,25 @@ export default function AgentPanel({
|
|||
(data: AgentForm) => {
|
||||
const tools = data.tools ?? [];
|
||||
|
||||
if (data.code_interpreter) {
|
||||
tools.push(Tools.code_interpreter);
|
||||
if (data.execute_code === true) {
|
||||
tools.push(Tools.execute_code);
|
||||
}
|
||||
if (data.retrieval) {
|
||||
if (data.file_search === true) {
|
||||
tools.push(Tools.file_search);
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
model,
|
||||
model_parameters,
|
||||
provider: _provider,
|
||||
description,
|
||||
instructions,
|
||||
model: _model,
|
||||
model_parameters,
|
||||
provider: _provider,
|
||||
} = data;
|
||||
|
||||
const provider = typeof _provider === 'string' ? _provider : (_provider as Option).value;
|
||||
const model = _model ?? '';
|
||||
const provider =
|
||||
(typeof _provider === 'string' ? _provider : (_provider as StringOption).value) ?? '';
|
||||
|
||||
if (agent_id) {
|
||||
update.mutate({
|
||||
|
|
@ -135,6 +148,13 @@ export default function AgentPanel({
|
|||
return;
|
||||
}
|
||||
|
||||
if (!provider || !model) {
|
||||
return showToast({
|
||||
message: localize('com_agents_missing_provider_model'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
create.mutate({
|
||||
name,
|
||||
description,
|
||||
|
|
@ -145,7 +165,7 @@ export default function AgentPanel({
|
|||
model_parameters,
|
||||
});
|
||||
},
|
||||
[agent_id, create, update],
|
||||
[agent_id, create, update, showToast, localize],
|
||||
);
|
||||
|
||||
const handleSelectAgent = useCallback(() => {
|
||||
|
|
@ -154,6 +174,15 @@ export default function AgentPanel({
|
|||
}
|
||||
}, [agent_id, onSelectAgent]);
|
||||
|
||||
if (agentQuery.isInitialLoading) {
|
||||
return <AgentPanelSkeleton />;
|
||||
}
|
||||
|
||||
const canEditAgent =
|
||||
agentQuery.data?.isCollaborative ?? false
|
||||
? true
|
||||
: agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN;
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
|
|
@ -169,6 +198,7 @@ export default function AgentPanel({
|
|||
<AgentSelect
|
||||
reset={reset}
|
||||
value={field.value}
|
||||
agentQuery={agentQuery}
|
||||
setCurrentAgentId={setCurrentAgentId}
|
||||
selectedAgentId={current_agent_id ?? null}
|
||||
createMutation={create}
|
||||
|
|
@ -188,10 +218,25 @@ export default function AgentPanel({
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{activePanel === Panel.model ? (
|
||||
<ModelPanel setActivePanel={setActivePanel} providers={providers} models={models} />
|
||||
) : null}
|
||||
{activePanel === Panel.builder ? (
|
||||
{!canEditAgent && (
|
||||
<div className="flex h-[30vh] w-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-token-text-primary m-2 text-xl font-semibold">
|
||||
{localize('com_agents_not_available')}
|
||||
</h2>
|
||||
<p className="text-token-text-secondary">{localize('com_agents_no_access')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{canEditAgent && activePanel === Panel.model && (
|
||||
<ModelPanel
|
||||
setActivePanel={setActivePanel}
|
||||
agent_id={agent_id}
|
||||
providers={providers}
|
||||
models={models}
|
||||
/>
|
||||
)}
|
||||
{canEditAgent && activePanel === Panel.builder && (
|
||||
<AgentConfig
|
||||
actions={actions}
|
||||
setAction={setAction}
|
||||
|
|
@ -200,7 +245,7 @@ export default function AgentPanel({
|
|||
endpointsConfig={endpointsConfig}
|
||||
setCurrentAgentId={setCurrentAgentId}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { Skeleton } from '~/components/ui';
|
||||
|
||||
export default function AgentPanelSkeleton() {
|
||||
return (
|
||||
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
|
||||
{/* Agent Select and Button */}
|
||||
<div className="mt-1 flex w-full gap-2">
|
||||
<Skeleton className="h-[40px] w-3/4 rounded" />
|
||||
<Skeleton className="h-[40px] w-1/4 rounded" />
|
||||
</div>
|
||||
|
||||
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
|
||||
{/* Avatar */}
|
||||
<div className="mb-4">
|
||||
<div className="flex w-full items-center justify-center gap-4">
|
||||
<Skeleton className="relative h-20 w-20 rounded-full" />
|
||||
</div>
|
||||
{/* Name */}
|
||||
<Skeleton className="mb-2 h-5 w-1/5 rounded" />
|
||||
<Skeleton className="mb-1 h-[40px] w-full rounded" />
|
||||
<Skeleton className="h-3 w-1/4 rounded" />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-4">
|
||||
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
|
||||
<Skeleton className="h-[40px] w-full rounded" />
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
|
||||
<Skeleton className="h-[100px] w-full rounded" />
|
||||
</div>
|
||||
|
||||
{/* Model and Provider */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
|
||||
<Skeleton className="h-[40px] w-full rounded" />
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
|
||||
<Skeleton className="mb-2 h-[40px] w-full rounded" />
|
||||
<Skeleton className="h-[40px] w-full rounded" />
|
||||
</div>
|
||||
|
||||
{/* Tools & Actions */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
|
||||
<Skeleton className="mb-2 h-[40px] w-full rounded" />
|
||||
<Skeleton className="mb-2 h-[40px] w-full rounded" />
|
||||
<div className="flex space-x-2">
|
||||
<Skeleton className="h-8 w-1/2 rounded" />
|
||||
<Skeleton className="h-8 w-1/2 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Buttons */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Skeleton className="h-[40px] w-[100px] rounded" />
|
||||
<Skeleton className="h-[40px] w-[100px] rounded" />
|
||||
<Skeleton className="h-[40px] w-[100px] rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Capabilities } from 'librechat-data-provider';
|
||||
import { EModelEndpoint } 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';
|
||||
|
|
@ -18,19 +18,14 @@ export default function AgentPanelSwitch() {
|
|||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const agentsConfig = useMemo(
|
||||
() =>
|
||||
// endpointsConfig?.[EModelEndpoint.agents] ??
|
||||
({
|
||||
// for testing purposes
|
||||
capabilities: [Capabilities.tools, Capabilities.actions],
|
||||
} as TConfig),
|
||||
// [endpointsConfig]);
|
||||
[],
|
||||
() => endpointsConfig?.[EModelEndpoint.agents] ?? ({} as TConfig | null),
|
||||
[endpointsConfig],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation?.agent_id) {
|
||||
setCurrentAgentId(conversation?.agent_id);
|
||||
const agent_id = conversation?.agent_id ?? '';
|
||||
if (agent_id) {
|
||||
setCurrentAgentId(agent_id);
|
||||
}
|
||||
}, [conversation?.agent_id]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { Plus, EarthIcon } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
|
||||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { UseFormReset } from 'react-hook-form';
|
||||
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
|
||||
import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common';
|
||||
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
|
||||
import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider';
|
||||
import SelectDropDown from '~/components/ui/SelectDropDown';
|
||||
// import { useFileMapContext } from '~/Providers';
|
||||
import { useListAgentsQuery } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const keys = new Set(Object.keys(defaultAgentFormValues));
|
||||
|
||||
export default function AgentSelect({
|
||||
reset,
|
||||
agentQuery,
|
||||
value: currentAgentValue,
|
||||
selectedAgentId = null,
|
||||
setCurrentAgentId,
|
||||
|
|
@ -24,12 +24,11 @@ export default function AgentSelect({
|
|||
reset: UseFormReset<AgentForm>;
|
||||
value?: TAgentOption;
|
||||
selectedAgentId: string | null;
|
||||
agentQuery: QueryObserverResult<Agent>;
|
||||
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: startupConfig } = useGetStartupConfig();
|
||||
|
|
@ -39,15 +38,10 @@ export default function AgentSelect({
|
|||
processAgentOption({
|
||||
agent,
|
||||
instanceProjectId: startupConfig?.instanceProjectId,
|
||||
/* fileMap */
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
|
||||
enabled: !!(selectedAgentId ?? ''),
|
||||
});
|
||||
|
||||
const resetAgentForm = useCallback(
|
||||
(fullAgent: Agent) => {
|
||||
const { instanceProjectId } = startupConfig ?? {};
|
||||
|
|
@ -61,17 +55,26 @@ export default function AgentSelect({
|
|||
icon: isGlobal ? <EarthIcon className={'icon-lg text-green-400'} /> : null,
|
||||
};
|
||||
|
||||
const actions: AgentCapabilities = {
|
||||
[Capabilities.code_interpreter]: false,
|
||||
[Capabilities.image_vision]: false,
|
||||
[Capabilities.retrieval]: false,
|
||||
const capabilities: TAgentCapabilities = {
|
||||
[AgentCapabilities.execute_code]: false,
|
||||
[AgentCapabilities.file_search]: false,
|
||||
};
|
||||
|
||||
const formValues: Partial<AgentForm & AgentCapabilities> = {
|
||||
...actions,
|
||||
const agentTools: string[] = [];
|
||||
(fullAgent.tools ?? []).forEach((tool) => {
|
||||
if (capabilities[tool] !== undefined) {
|
||||
capabilities[tool] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
agentTools.push(tool);
|
||||
});
|
||||
|
||||
const formValues: Partial<AgentForm & TAgentCapabilities> = {
|
||||
...capabilities,
|
||||
agent: update,
|
||||
model: update.model,
|
||||
tools: update.tools ?? [],
|
||||
tools: agentTools,
|
||||
};
|
||||
|
||||
Object.entries(fullAgent).forEach(([name, value]) => {
|
||||
|
|
@ -91,7 +94,7 @@ export default function AgentSelect({
|
|||
|
||||
reset(formValues);
|
||||
},
|
||||
[reset],
|
||||
[reset, startupConfig],
|
||||
);
|
||||
|
||||
const onSelect = useCallback(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useMemo } from 'react';
|
||||
// import { Capabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
// import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import type { TConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
// import type { AgentForm } from '~/common';
|
||||
// import ImageVision from './ImageVision';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Retrieval from './Retrieval';
|
||||
import CodeFiles from './CodeFiles';
|
||||
// import CodeFiles from './CodeFiles';
|
||||
import Code from './Code';
|
||||
|
||||
export default function CapabilitiesForm({
|
||||
|
|
@ -20,25 +20,21 @@ export default function CapabilitiesForm({
|
|||
}) {
|
||||
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 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">
|
||||
|
|
@ -50,10 +46,10 @@ export default function CapabilitiesForm({
|
|||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{codeEnabled && <Code />}
|
||||
{retrievalEnabled && <Retrieval retrievalModels={retrievalModels} />}
|
||||
{codeEnabled === true && <Code />}
|
||||
{retrievalEnabled === true && <Retrieval retrievalModels={retrievalModels} />}
|
||||
{/* {imageVisionEnabled && version == 1 && <ImageVision />} */}
|
||||
{codeEnabled && <CodeFiles agent_id={agent_id} files={files} />}
|
||||
{/* {codeEnabled && <CodeFiles agent_id={agent_id} files={files} />} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Capabilities } from 'librechat-data-provider';
|
||||
import { AgentCapabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import {
|
||||
|
|
@ -22,7 +22,7 @@ export default function Code() {
|
|||
<HoverCard openDelay={50}>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.code_interpreter}
|
||||
name={AgentCapabilities.execute_code}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
|
|
@ -30,30 +30,34 @@ export default function Code() {
|
|||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() =>
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
setValue(AgentCapabilities.execute_code, !getValues(AgentCapabilities.execute_code), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<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,
|
||||
})
|
||||
}
|
||||
htmlFor={AgentCapabilities.execute_code}
|
||||
>
|
||||
{localize('com_assistants_code_interpreter')}
|
||||
{localize('com_agents_execute_code')}
|
||||
</label>
|
||||
<HoverCardTrigger>
|
||||
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
||||
</HoverCardTrigger>
|
||||
</div>
|
||||
</button>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={ESide.Top} className="w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{/* // TODO: add a Code Interpreter description */}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
120
client/src/components/SidePanel/Agents/FileSearch.tsx
Normal file
120
client/src/components/SidePanel/Agents/FileSearch.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
EToolResources,
|
||||
mergeFileConfig,
|
||||
AgentCapabilities,
|
||||
retrievalMimeTypes,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ExtendedFile, AgentForm } from '~/common';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import FileSearchCheckbox from './FileSearchCheckbox';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { AttachmentIcon } from '~/components/svg';
|
||||
import { useFileHandling } from '~/hooks/Files';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
export default function FileSearch({
|
||||
agent_id,
|
||||
files: _files,
|
||||
}: {
|
||||
agent_id: string;
|
||||
files?: [string, ExtendedFile][];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
const { watch } = useFormContext<AgentForm>();
|
||||
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: EToolResources.file_search },
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (_files) {
|
||||
setFiles(new Map(_files));
|
||||
}
|
||||
}, [_files]);
|
||||
|
||||
const fileSearchChecked = watch(AgentCapabilities.file_search);
|
||||
|
||||
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
|
||||
const disabled = endpointFileConfig.disabled ?? false;
|
||||
|
||||
if (disabled === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleButtonClick = () => {
|
||||
// necessary to reset the input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="mb-1.5 flex items-center gap-2">
|
||||
<span>
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_file_search')}
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<FileSearchCheckbox />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id || fileSearchChecked === false}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 rounded-lg font-medium"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
|
||||
<input
|
||||
multiple={true}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
tabIndex={-1}
|
||||
ref={fileInputRef}
|
||||
disabled={!agent_id || fileSearchChecked === false}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{localize('com_ui_upload_files')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/* Disabled Message */}
|
||||
{agent_id ? null : (
|
||||
<div className="text-sm text-text-secondary">
|
||||
{localize('com_agents_file_search_disabled')}
|
||||
</div>
|
||||
)}
|
||||
{/* Knowledge Files */}
|
||||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setFilesLoading={setFilesLoading}
|
||||
agent_id={agent_id}
|
||||
tool_resource={EToolResources.file_search}
|
||||
fileFilter={(file: ExtendedFile) =>
|
||||
retrievalMimeTypes.some((regex) => regex.test(file.type ?? ''))
|
||||
}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { AgentCapabilities } 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 FileSearchCheckbox() {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={50}>
|
||||
<div className="my-2 flex items-center">
|
||||
<Controller
|
||||
name={AgentCapabilities.file_search}
|
||||
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()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() =>
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={AgentCapabilities.file_search}
|
||||
>
|
||||
{localize('com_agents_enable_file_search')}
|
||||
</label>
|
||||
<HoverCardTrigger>
|
||||
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
||||
</HoverCardTrigger>
|
||||
</button>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={ESide.Top} className="w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{localize('com_agents_file_search_info')}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import React, { useMemo, useEffect } 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 { getSettingsKeys } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import { agentSettings } from '~/components/SidePanel/Parameters/settings';
|
||||
import { getEndpointField, cn, cardStyle } from '~/utils';
|
||||
import { SelectDropDown } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function ModelPanel({
|
||||
export default function Parameters({
|
||||
setActivePanel,
|
||||
providers,
|
||||
models: modelsData,
|
||||
|
|
@ -15,30 +20,56 @@ export default function ModelPanel({
|
|||
const localize = useLocalize();
|
||||
|
||||
const { control, setValue, watch } = useFormContext<AgentForm>();
|
||||
const model = watch('model');
|
||||
const modelParameters = watch('model_parameters');
|
||||
const providerOption = watch('provider');
|
||||
const model = watch('model');
|
||||
|
||||
const provider = useMemo(() => {
|
||||
if (!providerOption) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return typeof providerOption === 'string' ? providerOption : providerOption.value;
|
||||
const value =
|
||||
typeof providerOption === 'string'
|
||||
? providerOption
|
||||
: (providerOption as StringOption | undefined)?.value;
|
||||
return value ?? '';
|
||||
}, [providerOption]);
|
||||
const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && model) {
|
||||
const modelExists = models.includes(model);
|
||||
const _model = model ?? '';
|
||||
if (provider && _model) {
|
||||
const modelExists = models.includes(_model);
|
||||
if (!modelExists) {
|
||||
const newModels = modelsData[provider];
|
||||
setValue('model', newModels[0] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if (provider && !_model) {
|
||||
setValue('model', models[0] ?? '');
|
||||
}
|
||||
}, [provider, models, modelsData, setValue, model]);
|
||||
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const bedrockRegions = useMemo(() => {
|
||||
return endpointsConfig?.[provider]?.availableRegions ?? [];
|
||||
}, [endpointsConfig, provider]);
|
||||
|
||||
const endpointType = useMemo(
|
||||
() => getEndpointField(endpointsConfig, provider, 'type'),
|
||||
[provider, endpointsConfig],
|
||||
);
|
||||
|
||||
const parameters = useMemo(() => {
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model ?? '');
|
||||
return agentSettings[combinedKey] ?? agentSettings[endpointKey];
|
||||
}, [endpointType, model, provider]);
|
||||
|
||||
const setOption = (optionKey: keyof t.AgentModelParameters) => (value: t.AgentParameterValue) => {
|
||||
setValue(`model_parameters.${optionKey}`, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
||||
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto 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
|
||||
|
|
@ -56,228 +87,125 @@ export default function ModelPanel({
|
|||
|
||||
<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',
|
||||
<div className="p-2">
|
||||
{/* 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 === undefined || 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>
|
||||
)}
|
||||
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',
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Model */}
|
||||
<div className="model-panel-section mb-4">
|
||||
<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>
|
||||
)}
|
||||
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>
|
||||
{/* Model Parameters */}
|
||||
{parameters && (
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{' '}
|
||||
{/* This is the parent element containing all settings */}
|
||||
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
||||
{parameters.map((setting) => {
|
||||
const Component = componentMapping[setting.component];
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
const { key, default: defaultValue, ...rest } = setting;
|
||||
|
||||
if (key === 'region' && bedrockRegions.length) {
|
||||
rest.options = bedrockRegions;
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
key={key}
|
||||
settingKey={key}
|
||||
defaultValue={defaultValue}
|
||||
{...rest}
|
||||
setOption={setOption as t.TSetOption}
|
||||
conversation={modelParameters as Partial<t.TConversation>}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,19 @@ import { useLocalize } from '~/hooks';
|
|||
|
||||
type FormValues = {
|
||||
[Permissions.SHARED_GLOBAL]: boolean;
|
||||
[Permissions.UPDATE]: boolean;
|
||||
};
|
||||
|
||||
export default function ShareAgent({
|
||||
agent_id = '',
|
||||
agentName,
|
||||
projectIds = [],
|
||||
isCollaborative = false,
|
||||
}: {
|
||||
agent_id?: string;
|
||||
agentName?: string;
|
||||
projectIds?: string[];
|
||||
isCollaborative?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
|
@ -40,6 +43,7 @@ export default function ShareAgent({
|
|||
);
|
||||
|
||||
const {
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
|
|
@ -49,12 +53,22 @@ export default function ShareAgent({
|
|||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
[Permissions.SHARED_GLOBAL]: agentIsGlobal,
|
||||
[Permissions.UPDATE]: isCollaborative,
|
||||
},
|
||||
});
|
||||
|
||||
const sharedGlobalValue = watch(Permissions.SHARED_GLOBAL);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sharedGlobalValue) {
|
||||
setValue(Permissions.UPDATE, false);
|
||||
}
|
||||
}, [sharedGlobalValue, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(Permissions.SHARED_GLOBAL, agentIsGlobal);
|
||||
}, [agentIsGlobal, setValue]);
|
||||
setValue(Permissions.UPDATE, isCollaborative);
|
||||
}, [agentIsGlobal, isCollaborative, setValue]);
|
||||
|
||||
const updateAgent = useUpdateAgentMutation({
|
||||
onSuccess: (data) => {
|
||||
|
|
@ -87,16 +101,30 @@ export default function ShareAgent({
|
|||
|
||||
const payload = {} as AgentUpdateParams;
|
||||
|
||||
if (data[Permissions.SHARED_GLOBAL]) {
|
||||
payload.projectIds = [startupConfig.instanceProjectId];
|
||||
} else {
|
||||
payload.removeProjectIds = [startupConfig.instanceProjectId];
|
||||
if (data[Permissions.UPDATE] !== isCollaborative) {
|
||||
payload.isCollaborative = data[Permissions.UPDATE];
|
||||
}
|
||||
|
||||
updateAgent.mutate({
|
||||
agent_id,
|
||||
data: payload,
|
||||
});
|
||||
if (data[Permissions.SHARED_GLOBAL] !== agentIsGlobal) {
|
||||
if (data[Permissions.SHARED_GLOBAL]) {
|
||||
payload.projectIds = [startupConfig.instanceProjectId];
|
||||
} else {
|
||||
payload.removeProjectIds = [startupConfig.instanceProjectId];
|
||||
payload.isCollaborative = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length > 0) {
|
||||
updateAgent.mutate({
|
||||
agent_id,
|
||||
data: payload,
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
message: localize('com_ui_no_changes'),
|
||||
status: 'info',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -113,12 +141,12 @@ export default function ShareAgent({
|
|||
)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-blue-500">
|
||||
<div className="flex items-center justify-center gap-2 text-blue-500">
|
||||
<Share2Icon className="icon-md h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="border-border-light bg-surface-primary-alt text-text-secondary">
|
||||
<OGDialogContent className="w-1/4 border-border-light bg-surface-primary-alt text-text-secondary">
|
||||
<OGDialogTitle>
|
||||
{localize(
|
||||
'com_ui_share_var',
|
||||
|
|
@ -133,11 +161,12 @@ export default function ShareAgent({
|
|||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between gap-2 py-4">
|
||||
<div className="flex items-center justify-between gap-2 py-2">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="mr-2 cursor-pointer"
|
||||
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
|
||||
onClick={() =>
|
||||
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
|
||||
shouldDirty: true,
|
||||
|
|
@ -166,18 +195,54 @@ export default function ShareAgent({
|
|||
name={Permissions.SHARED_GLOBAL}
|
||||
control={control}
|
||||
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
const isValid = !(value && agentIsGlobal);
|
||||
if (!isValid) {
|
||||
showToast({
|
||||
message: localize('com_ui_agent_already_shared_to_all'),
|
||||
status: 'warning',
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between gap-2 py-2">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="mr-2 cursor-pointer"
|
||||
disabled={
|
||||
isFetching || updateAgent.isLoading || !instanceProjectId || !sharedGlobalValue
|
||||
}
|
||||
onClick={() =>
|
||||
setValue(Permissions.UPDATE, !getValues(Permissions.UPDATE), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setValue(Permissions.UPDATE, !getValues(Permissions.UPDATE), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
return isValid;
|
||||
},
|
||||
}}
|
||||
}}
|
||||
aria-checked={getValues(Permissions.UPDATE)}
|
||||
role="checkbox"
|
||||
>
|
||||
{localize('com_agents_allow_editing')}
|
||||
</button>
|
||||
{/* <label htmlFor={Permissions.UPDATE} className="select-none">
|
||||
{agentIsGlobal && (
|
||||
<span className="ml-2 text-xs">{localize('com_ui_agent_editing_allowed')}</span>
|
||||
)}
|
||||
</label> */}
|
||||
</div>
|
||||
<Controller
|
||||
name={Permissions.UPDATE}
|
||||
control={control}
|
||||
disabled={
|
||||
isFetching || updateAgent.isLoading || !instanceProjectId || !sharedGlobalValue
|
||||
}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
{...field}
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ export default function AssistantPanel({
|
|||
{/* Tools */}
|
||||
<div className="mb-6">
|
||||
<label className={labelClass}>
|
||||
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
|
||||
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
|
||||
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
|
||||
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import { useState } from 'react';
|
|||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import type { NavLink, NavProps } from '~/common';
|
||||
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
|
||||
import { buttonVariants } from '~/components/ui/Button';
|
||||
import { TooltipAnchor, Button } from '~/components';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -20,7 +19,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
return (
|
||||
<div
|
||||
data-collapsed={isCollapsed}
|
||||
className="bg-token-sidebar-surface-primary hide-scrollbar group flex-shrink-0 overflow-x-hidden py-2 data-[collapsed=true]:py-2"
|
||||
className="bg-token-sidebar-surface-primary hide-scrollbar group flex-shrink-0 overflow-x-hidden"
|
||||
>
|
||||
<div className="h-full">
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
|
|
@ -76,12 +75,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{localize(link.title)}
|
||||
{link.label && (
|
||||
{link.label != null && link.label && (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto transition-all duration-300 ease-in-out',
|
||||
'ml-auto opacity-100 transition-all duration-300 ease-in-out',
|
||||
variant === 'default' ? 'text-background dark:text-white' : '',
|
||||
isCollapsed ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
|
|
|
|||
|
|
@ -572,3 +572,12 @@ export const presetSettings: Record<
|
|||
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
|
||||
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
|
||||
};
|
||||
|
||||
export const agentSettings: Record<string, SettingsConfiguration | undefined> = Object.entries(
|
||||
presetSettings,
|
||||
).reduce((acc, [key, value]) => {
|
||||
if (value) {
|
||||
acc[key] = value.col2;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
|
|
|||
|
|
@ -1,33 +1,17 @@
|
|||
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) {
|
||||
if (isAssistantsEndpoint(props.endpoint) && props.endpointKeyProvided) {
|
||||
return (
|
||||
<>
|
||||
<AssistantSwitcher {...props} />
|
||||
<Separator className="max-w-[98%] bg-surface-tertiary" />
|
||||
</>
|
||||
);
|
||||
return <AssistantSwitcher {...props} />;
|
||||
} else if (isAgentsEndpoint(props.endpoint) && props.endpointKeyProvided) {
|
||||
return (
|
||||
<>
|
||||
<AgentSwitcher {...props} />
|
||||
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
||||
</>
|
||||
);
|
||||
return <AgentSwitcher {...props} />;
|
||||
} else if (isAssistantsEndpoint(props.endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModelSwitcher {...props} />
|
||||
<Separator className="max-w-[98%] bg-surface-tertiary" />
|
||||
</>
|
||||
);
|
||||
return <ModelSwitcher {...props} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue