mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🖥️ feat: Code Interpreter API for Non-Agent Endpoints (#6803)
* fix: Prevent parsing 'undefined' string in useLocalStorage initialization * feat: first pass, code interpreter badge * feat: Integrate API key authentication and default checked value in Code Interpreter Badge * refactor: Rename showMCPServers to showEphemeralBadges and update related components, memoize values in useChatBadges * refactor: Enhance AttachFileChat to support ephemeral agents in file attachment logic * fix: Add baseURL configuration option to legacy function call * refactor: Update dependency array in useDragHelpers to include handleFiles * refactor: Update isEphemeralAgent function to accept optional endpoint parameter * refactor: Update file handling to support ephemeral agents in AttachFileMenu and useDragHelpers * fix: improve compatibility issues with OpenAI usage field handling in createRun function * refactor: usage field compatibility * fix: ensure mcp servers are no longer "selected" if mcp servers are now unavailable
This commit is contained in:
parent
5d668748f9
commit
24c0433dcf
19 changed files with 311 additions and 48 deletions
|
|
@ -34,6 +34,7 @@ function createLLM({
|
||||||
let credentials = { openAIApiKey };
|
let credentials = { openAIApiKey };
|
||||||
let configuration = {
|
let configuration = {
|
||||||
apiKey: openAIApiKey,
|
apiKey: openAIApiKey,
|
||||||
|
...(configOptions.basePath && { baseURL: configOptions.basePath }),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {AzureOptions} */
|
/** @type {AzureOptions} */
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const { agentSchema } = require('@librechat/data-schemas');
|
const { agentSchema } = require('@librechat/data-schemas');
|
||||||
const { SystemRoles } = require('librechat-data-provider');
|
const { SystemRoles, Tools } = require('librechat-data-provider');
|
||||||
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
|
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
|
||||||
require('librechat-data-provider').Constants;
|
require('librechat-data-provider').Constants;
|
||||||
const { CONFIG_STORE, STARTUP_CONFIG } = require('librechat-data-provider').CacheKeys;
|
const { CONFIG_STORE, STARTUP_CONFIG } = require('librechat-data-provider').CacheKeys;
|
||||||
|
|
@ -51,16 +51,22 @@ const loadEphemeralAgent = ({ req, agent_id, endpoint, model_parameters: _m }) =
|
||||||
const mcpServers = new Set(req.body.ephemeralAgent?.mcp);
|
const mcpServers = new Set(req.body.ephemeralAgent?.mcp);
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
const tools = [];
|
const tools = [];
|
||||||
|
if (req.body.ephemeralAgent?.execute_code === true) {
|
||||||
|
tools.push(Tools.execute_code);
|
||||||
|
}
|
||||||
|
|
||||||
for (const toolName of Object.keys(availableTools)) {
|
if (mcpServers.size > 0) {
|
||||||
if (!toolName.includes(mcp_delimiter)) {
|
for (const toolName of Object.keys(availableTools)) {
|
||||||
continue;
|
if (!toolName.includes(mcp_delimiter)) {
|
||||||
}
|
continue;
|
||||||
const mcpServer = toolName.split(mcp_delimiter)?.[1];
|
}
|
||||||
if (mcpServer && mcpServers.has(mcpServer)) {
|
const mcpServer = toolName.split(mcp_delimiter)?.[1];
|
||||||
tools.push(toolName);
|
if (mcpServer && mcpServers.has(mcpServer)) {
|
||||||
|
tools.push(toolName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instructions = req.body.promptPrefix;
|
const instructions = req.body.promptPrefix;
|
||||||
return {
|
return {
|
||||||
id: agent_id,
|
id: agent_id,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,13 @@ const { providerEndpointMap, KnownEndpoints } = require('librechat-data-provider
|
||||||
* @typedef {import('@librechat/agents').IState} IState
|
* @typedef {import('@librechat/agents').IState} IState
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const customProviders = new Set([
|
||||||
|
Providers.XAI,
|
||||||
|
Providers.OLLAMA,
|
||||||
|
Providers.DEEPSEEK,
|
||||||
|
Providers.OPENROUTER,
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Run instance with custom handlers and configuration.
|
* Creates a new Run instance with custom handlers and configuration.
|
||||||
*
|
*
|
||||||
|
|
@ -43,8 +50,11 @@ async function createRun({
|
||||||
agent.model_parameters,
|
agent.model_parameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Resolves Mistral type strictness due to new OpenAI usage field */
|
/** Resolves issues with new OpenAI usage field */
|
||||||
if (agent.endpoint?.toLowerCase().includes(KnownEndpoints.mistral)) {
|
if (
|
||||||
|
customProviders.has(agent.provider) ||
|
||||||
|
(agent.provider === Providers.OPENAI && agent.endpoint !== agent.provider)
|
||||||
|
) {
|
||||||
llmConfig.streamUsage = false;
|
llmConfig.streamUsage = false;
|
||||||
llmConfig.usage = true;
|
llmConfig.usage = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import React, {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
import CodeInterpreter from './CodeInterpreter';
|
||||||
import type { BadgeItem } from '~/common';
|
import type { BadgeItem } from '~/common';
|
||||||
import { useChatBadges } from '~/hooks';
|
import { useChatBadges } from '~/hooks';
|
||||||
import { Badge } from '~/components/ui';
|
import { Badge } from '~/components/ui';
|
||||||
|
|
@ -17,7 +18,7 @@ import MCPSelect from './MCPSelect';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface BadgeRowProps {
|
interface BadgeRowProps {
|
||||||
showMCPServers?: boolean;
|
showEphemeralBadges?: boolean;
|
||||||
onChange: (badges: Pick<BadgeItem, 'id'>[]) => void;
|
onChange: (badges: Pick<BadgeItem, 'id'>[]) => void;
|
||||||
onToggle?: (badgeId: string, currentActive: boolean) => void;
|
onToggle?: (badgeId: string, currentActive: boolean) => void;
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
|
|
@ -131,7 +132,13 @@ const dragReducer = (state: DragState, action: DragAction): DragState => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat }: BadgeRowProps) {
|
function BadgeRow({
|
||||||
|
showEphemeralBadges,
|
||||||
|
conversationId,
|
||||||
|
onChange,
|
||||||
|
onToggle,
|
||||||
|
isInChat,
|
||||||
|
}: BadgeRowProps) {
|
||||||
const [orderedBadges, setOrderedBadges] = useState<BadgeItem[]>([]);
|
const [orderedBadges, setOrderedBadges] = useState<BadgeItem[]>([]);
|
||||||
const [dragState, dispatch] = useReducer(dragReducer, {
|
const [dragState, dispatch] = useReducer(dragReducer, {
|
||||||
draggedBadge: null,
|
draggedBadge: null,
|
||||||
|
|
@ -146,7 +153,7 @@ function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat
|
||||||
const animationFrame = useRef<number | null>(null);
|
const animationFrame = useRef<number | null>(null);
|
||||||
const containerRectRef = useRef<DOMRect | null>(null);
|
const containerRectRef = useRef<DOMRect | null>(null);
|
||||||
|
|
||||||
const allBadges = useChatBadges() || [];
|
const allBadges = useChatBadges();
|
||||||
const isEditing = useRecoilValue(store.isEditingBadges);
|
const isEditing = useRecoilValue(store.isEditingBadges);
|
||||||
|
|
||||||
const badges = useMemo(
|
const badges = useMemo(
|
||||||
|
|
@ -345,7 +352,12 @@ function BadgeRow({ showMCPServers, conversationId, onChange, onToggle, isInChat
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showMCPServers === true && <MCPSelect conversationId={conversationId} />}
|
{showEphemeralBadges === true && (
|
||||||
|
<>
|
||||||
|
<CodeInterpreter conversationId={conversationId} />
|
||||||
|
<MCPSelect conversationId={conversationId} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{ghostBadge && (
|
{ghostBadge && (
|
||||||
<div
|
<div
|
||||||
className="ghost-badge h-full"
|
className="ghost-badge h-full"
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
<AttachFileChat disableInputs={disableInputs} />
|
<AttachFileChat disableInputs={disableInputs} />
|
||||||
</div>
|
</div>
|
||||||
<BadgeRow
|
<BadgeRow
|
||||||
showMCPServers={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
|
showEphemeralBadges={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
|
||||||
conversationId={conversation?.conversationId ?? Constants.NEW_CONVO}
|
conversationId={conversation?.conversationId ?? Constants.NEW_CONVO}
|
||||||
onChange={setBadges}
|
onChange={setBadges}
|
||||||
isInChat={
|
isInChat={
|
||||||
|
|
|
||||||
104
client/src/components/Chat/Input/CodeInterpreter.tsx
Normal file
104
client/src/components/Chat/Input/CodeInterpreter.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import React, { memo, useMemo, useCallback } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { TerminalSquareIcon } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Tools,
|
||||||
|
AuthType,
|
||||||
|
Constants,
|
||||||
|
LocalStorageKeys,
|
||||||
|
PermissionTypes,
|
||||||
|
Permissions,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
|
||||||
|
import { useLocalize, useHasAccess, useCodeApiKeyForm } from '~/hooks';
|
||||||
|
import CheckboxButton from '~/components/ui/CheckboxButton';
|
||||||
|
import useLocalStorage from '~/hooks/useLocalStorageAlt';
|
||||||
|
import { useVerifyAgentToolAuth } from '~/data-provider';
|
||||||
|
import { ephemeralAgentByConvoId } from '~/store';
|
||||||
|
|
||||||
|
function CodeInterpreter({ conversationId }: { conversationId?: string | null }) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const key = conversationId ?? Constants.NEW_CONVO;
|
||||||
|
|
||||||
|
const canRunCode = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.RUN_CODE,
|
||||||
|
permission: Permissions.USE,
|
||||||
|
});
|
||||||
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
||||||
|
const isCodeToggleEnabled = useMemo(() => {
|
||||||
|
return ephemeralAgent?.execute_code ?? false;
|
||||||
|
}, [ephemeralAgent?.execute_code]);
|
||||||
|
|
||||||
|
const { data } = useVerifyAgentToolAuth(
|
||||||
|
{ toolId: Tools.execute_code },
|
||||||
|
{
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const authType = useMemo(() => data?.message ?? false, [data?.message]);
|
||||||
|
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
|
||||||
|
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
|
||||||
|
useCodeApiKeyForm({});
|
||||||
|
|
||||||
|
const setValue = useCallback(
|
||||||
|
(isChecked: boolean) => {
|
||||||
|
setEphemeralAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
execute_code: isChecked,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[setEphemeralAgent],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [runCode, setRunCode] = useLocalStorage<boolean>(
|
||||||
|
`${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`,
|
||||||
|
isCodeToggleEnabled,
|
||||||
|
setValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(isChecked: boolean) => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRunCode(isChecked);
|
||||||
|
},
|
||||||
|
[setRunCode, setIsDialogOpen, isAuthenticated],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedChange = useMemo(
|
||||||
|
() => debounce(handleChange, 50, { leading: true }),
|
||||||
|
[handleChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canRunCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckboxButton
|
||||||
|
className="max-w-fit"
|
||||||
|
defaultChecked={runCode}
|
||||||
|
setValue={debouncedChange}
|
||||||
|
label={localize('com_assistants_code_interpreter')}
|
||||||
|
isCheckedClassName="border-purple-600/40 bg-purple-500/10 hover:bg-purple-700/10"
|
||||||
|
icon={<TerminalSquareIcon className="icon-md" />}
|
||||||
|
/>
|
||||||
|
<ApiKeyDialog
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
isOpen={isDialogOpen}
|
||||||
|
register={methods.register}
|
||||||
|
onRevoke={handleRevokeApiKey}
|
||||||
|
onOpenChange={setIsDialogOpen}
|
||||||
|
handleSubmit={methods.handleSubmit}
|
||||||
|
isToolAuthenticated={isAuthenticated}
|
||||||
|
isUserProvided={authType === AuthType.USER_PROVIDED}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CodeInterpreter);
|
||||||
|
|
@ -1,24 +1,31 @@
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import {
|
import {
|
||||||
|
Constants,
|
||||||
supportsFiles,
|
supportsFiles,
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
isAgentsEndpoint,
|
isAgentsEndpoint,
|
||||||
|
isEphemeralAgent,
|
||||||
EndpointFileConfig,
|
EndpointFileConfig,
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
|
import { ephemeralAgentByConvoId } from '~/store';
|
||||||
import AttachFileMenu from './AttachFileMenu';
|
import AttachFileMenu from './AttachFileMenu';
|
||||||
import AttachFile from './AttachFile';
|
import AttachFile from './AttachFile';
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
|
|
||||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||||
|
|
||||||
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
|
const key = conversation?.conversationId ?? Constants.NEW_CONVO;
|
||||||
|
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(key));
|
||||||
|
const isAgents = useMemo(
|
||||||
|
() => isAgentsEndpoint(_endpoint) || isEphemeralAgent(_endpoint, ephemeralAgent),
|
||||||
|
[_endpoint, ephemeralAgent],
|
||||||
|
);
|
||||||
|
|
||||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||||
select: (data) => mergeFileConfig(data),
|
select: (data) => mergeFileConfig(data),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ const AttachFile = ({ disabled }: AttachFileProps) => {
|
||||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||||
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
|
const [toolResource, setToolResource] = useState<EToolResources | undefined>();
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
const { handleFileChange } = useFileHandling();
|
const { handleFileChange } = useFileHandling({
|
||||||
|
overrideEndpoint: EModelEndpoint.agents,
|
||||||
|
});
|
||||||
|
|
||||||
const capabilities = useMemo(
|
const capabilities = useMemo(
|
||||||
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],
|
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { memo, useCallback } from 'react';
|
import React, { memo, useRef, useMemo, useEffect, useCallback } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import { useAvailableToolsQuery } from '~/data-provider';
|
import { useAvailableToolsQuery } from '~/data-provider';
|
||||||
|
|
@ -10,8 +10,12 @@ import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const hasSetFetched = useRef(false);
|
||||||
const key = conversationId ?? Constants.NEW_CONVO;
|
const key = conversationId ?? Constants.NEW_CONVO;
|
||||||
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
|
||||||
|
const mcpState = useMemo(() => {
|
||||||
|
return ephemeralAgent?.mcp ?? [];
|
||||||
|
}, [ephemeralAgent?.mcp]);
|
||||||
const setSelectedValues = useCallback(
|
const setSelectedValues = useCallback(
|
||||||
(values: string[] | null | undefined) => {
|
(values: string[] | null | undefined) => {
|
||||||
if (!values) {
|
if (!values) {
|
||||||
|
|
@ -29,10 +33,10 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
||||||
);
|
);
|
||||||
const [mcpValues, setMCPValues] = useLocalStorage<string[]>(
|
const [mcpValues, setMCPValues] = useLocalStorage<string[]>(
|
||||||
`${LocalStorageKeys.LAST_MCP_}${key}`,
|
`${LocalStorageKeys.LAST_MCP_}${key}`,
|
||||||
ephemeralAgent?.mcp ?? [],
|
mcpState,
|
||||||
setSelectedValues,
|
setSelectedValues,
|
||||||
);
|
);
|
||||||
const { data: mcpServers } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
const { data: mcpServers, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, {
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const serverNames = new Set<string>();
|
const serverNames = new Set<string>();
|
||||||
data.forEach((tool) => {
|
data.forEach((tool) => {
|
||||||
|
|
@ -45,6 +49,20 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasSetFetched.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isFetched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasSetFetched.current = true;
|
||||||
|
if ((mcpServers?.length ?? 0) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMCPValues([]);
|
||||||
|
}, [isFetched, setMCPValues, mcpServers?.length]);
|
||||||
|
|
||||||
const renderSelectedValues = useCallback(
|
const renderSelectedValues = useCallback(
|
||||||
(values: string[], placeholder?: string) => {
|
(values: string[], placeholder?: string) => {
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
|
|
@ -70,8 +88,8 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
||||||
defaultSelectedValues={mcpValues ?? []}
|
defaultSelectedValues={mcpValues ?? []}
|
||||||
renderSelectedValues={renderSelectedValues}
|
renderSelectedValues={renderSelectedValues}
|
||||||
placeholder={localize('com_ui_mcp_servers')}
|
placeholder={localize('com_ui_mcp_servers')}
|
||||||
popoverClassName="min-w-[200px]"
|
popoverClassName="min-w-fit"
|
||||||
className="badge-icon h-full min-w-[150px]"
|
className="badge-icon min-w-fit"
|
||||||
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
|
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
|
||||||
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
|
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
|
||||||
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-shadow md:w-full size-9 p-2 md:p-3 bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
|
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-shadow md:w-full size-9 p-2 md:p-3 bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
|
||||||
|
|
|
||||||
62
client/src/components/ui/CheckboxButton.tsx
Normal file
62
client/src/components/ui/CheckboxButton.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Checkbox, useStoreState, useCheckboxStore } from '@ariakit/react';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
export default function CheckboxButton({
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
setValue,
|
||||||
|
className,
|
||||||
|
defaultChecked,
|
||||||
|
isCheckedClassName,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
className?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
isCheckedClassName?: string;
|
||||||
|
setValue?: (isChecked: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const checkbox = useCheckboxStore();
|
||||||
|
const isChecked = useStoreState(checkbox, (state) => state?.value);
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (typeof isChecked !== 'boolean') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue?.(!isChecked);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultChecked) {
|
||||||
|
checkbox.setValue(defaultChecked);
|
||||||
|
}
|
||||||
|
}, [defaultChecked, checkbox]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
store={checkbox}
|
||||||
|
onChange={onChange}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
className={cn(
|
||||||
|
// Base styling from MultiSelect's selectClassName
|
||||||
|
'group relative inline-flex items-center justify-center gap-1.5',
|
||||||
|
'rounded-full border border-border-medium text-sm font-medium',
|
||||||
|
'size-9 p-2 transition-shadow md:w-full md:p-3',
|
||||||
|
'bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner',
|
||||||
|
|
||||||
|
// Checked state styling
|
||||||
|
isChecked && isCheckedClassName && isCheckedClassName,
|
||||||
|
|
||||||
|
// Additional custom classes
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
render={<button type="button" aria-label={label} />}
|
||||||
|
>
|
||||||
|
{/* Icon if provided */}
|
||||||
|
{icon && <span className="icon-md text-text-primary">{icon}</span>}
|
||||||
|
|
||||||
|
{/* Show the label on larger screens */}
|
||||||
|
<span className="hidden truncate md:block">{label}</span>
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -66,7 +66,7 @@ export default function MultiSelect<T extends string>({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('h-full', className)}>
|
<div className={className}>
|
||||||
<SelectProvider value={selectedValues} setValue={handleValueChange}>
|
<SelectProvider value={selectedValues} setValue={handleValueChange}>
|
||||||
{label && (
|
{label && (
|
||||||
<SelectLabel className={cn('mb-1 block text-sm text-text-primary', labelClassName)}>
|
<SelectLabel className={cn('mb-1 block text-sm text-text-primary', labelClassName)}>
|
||||||
|
|
@ -82,9 +82,10 @@ export default function MultiSelect<T extends string>({
|
||||||
selectClassName,
|
selectClassName,
|
||||||
selectedValues.length > 0 && selectItemsClassName != null && selectItemsClassName,
|
selectedValues.length > 0 && selectItemsClassName != null && selectItemsClassName,
|
||||||
)}
|
)}
|
||||||
|
onChange={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{selectIcon && selectIcon}
|
{selectIcon && selectIcon}
|
||||||
<span className="hidden truncate md:block">
|
<span className="mr-auto hidden truncate md:block">
|
||||||
{renderSelectedValues(selectedValues, placeholder)}
|
{renderSelectedValues(selectedValues, placeholder)}
|
||||||
</span>
|
</span>
|
||||||
<SelectArrow className="ml-1 hidden stroke-1 text-base opacity-75 md:block" />
|
<SelectArrow className="ml-1 hidden stroke-1 text-base opacity-75 md:block" />
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,28 @@ import { useRecoilValue } from 'recoil';
|
||||||
import { NativeTypes } from 'react-dnd-html5-backend';
|
import { NativeTypes } from 'react-dnd-html5-backend';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
isAgentsEndpoint,
|
Constants,
|
||||||
EModelEndpoint,
|
|
||||||
AgentCapabilities,
|
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
|
EModelEndpoint,
|
||||||
|
isAgentsEndpoint,
|
||||||
|
isEphemeralAgent,
|
||||||
|
AgentCapabilities,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import type { DropTargetMonitor } from 'react-dnd';
|
import type { DropTargetMonitor } from 'react-dnd';
|
||||||
import useFileHandling from './useFileHandling';
|
import useFileHandling from './useFileHandling';
|
||||||
import store from '~/store';
|
import store, { ephemeralAgentByConvoId } from '~/store';
|
||||||
|
|
||||||
export default function useDragHelpers() {
|
export default function useDragHelpers() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { handleFiles } = useFileHandling();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
|
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||||
|
const key = useMemo(
|
||||||
|
() => conversation?.conversationId ?? Constants.NEW_CONVO,
|
||||||
|
[conversation?.conversationId],
|
||||||
|
);
|
||||||
|
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(key));
|
||||||
|
|
||||||
const handleOptionSelect = (toolResource: string | undefined) => {
|
const handleOptionSelect = (toolResource: string | undefined) => {
|
||||||
handleFiles(draggedFiles, toolResource);
|
handleFiles(draggedFiles, toolResource);
|
||||||
|
|
@ -28,10 +34,16 @@ export default function useDragHelpers() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAgents = useMemo(
|
const isAgents = useMemo(
|
||||||
() => isAgentsEndpoint(conversation?.endpoint),
|
() =>
|
||||||
[conversation?.endpoint],
|
isAgentsEndpoint(conversation?.endpoint) ||
|
||||||
|
isEphemeralAgent(conversation?.endpoint, ephemeralAgent),
|
||||||
|
[conversation?.endpoint, ephemeralAgent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { handleFiles } = useFileHandling({
|
||||||
|
overrideEndpoint: isAgents ? EModelEndpoint.agents : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const [{ canDrop, isOver }, drop] = useDrop(
|
const [{ canDrop, isOver }, drop] = useDrop(
|
||||||
() => ({
|
() => ({
|
||||||
accept: [NativeTypes.FILE],
|
accept: [NativeTypes.FILE],
|
||||||
|
|
@ -61,7 +73,7 @@ export default function useDragHelpers() {
|
||||||
canDrop: monitor.canDrop(),
|
canDrop: monitor.canDrop(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
[],
|
[handleFiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { MessageCircleDashed, Box } from 'lucide-react';
|
import { MessageCircleDashed, Box } from 'lucide-react';
|
||||||
import type { BadgeItem } from '~/common';
|
import type { BadgeItem } from '~/common';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface ChatBadgeConfig {
|
interface ChatBadgeConfig {
|
||||||
|
|
@ -25,15 +26,22 @@ const badgeConfig: ReadonlyArray<ChatBadgeConfig> = [
|
||||||
export default function useChatBadges(): BadgeItem[] {
|
export default function useChatBadges(): BadgeItem[] {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const activeBadges = useRecoilValue(store.chatBadges) as Array<{ id: string }>;
|
const activeBadges = useRecoilValue(store.chatBadges) as Array<{ id: string }>;
|
||||||
const activeBadgeIds = new Set(activeBadges.map((badge) => badge.id));
|
const activeBadgeIds = useMemo(
|
||||||
|
() => new Set(activeBadges.map((badge) => badge.id)),
|
||||||
return badgeConfig.map((cfg) => ({
|
[activeBadges],
|
||||||
id: cfg.id,
|
);
|
||||||
label: localize(cfg.label),
|
const allBadges = useMemo(() => {
|
||||||
icon: cfg.icon,
|
return (
|
||||||
atom: cfg.atom,
|
badgeConfig.map((cfg) => ({
|
||||||
isAvailable: activeBadgeIds.has(cfg.id),
|
id: cfg.id,
|
||||||
}));
|
label: localize(cfg.label as TranslationKeys),
|
||||||
|
icon: cfg.icon,
|
||||||
|
atom: cfg.atom,
|
||||||
|
isAvailable: activeBadgeIds.has(cfg.id),
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [activeBadgeIds, localize]);
|
||||||
|
return allBadges;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useResetChatBadges() {
|
export function useResetChatBadges() {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export default function useLocalStorage<T>(
|
||||||
localStorage.setItem(key, JSON.stringify(defaultValue));
|
localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValue = item ? JSON.parse(item) : defaultValue;
|
const initialValue = item && item !== 'undefined' ? JSON.parse(item) : defaultValue;
|
||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
if (globalSetState) {
|
if (globalSetState) {
|
||||||
globalSetState(initialValue);
|
globalSetState(initialValue);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export function clearLocalStorage(skipFirst?: boolean) {
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
key.startsWith(LocalStorageKeys.LAST_MCP_) ||
|
key.startsWith(LocalStorageKeys.LAST_MCP_) ||
|
||||||
|
key.startsWith(LocalStorageKeys.LAST_CODE_TOGGLE_) ||
|
||||||
key.startsWith(LocalStorageKeys.ASST_ID_PREFIX) ||
|
key.startsWith(LocalStorageKeys.ASST_ID_PREFIX) ||
|
||||||
key.startsWith(LocalStorageKeys.AGENT_ID_PREFIX) ||
|
key.startsWith(LocalStorageKeys.AGENT_ID_PREFIX) ||
|
||||||
key.startsWith(LocalStorageKeys.LAST_CONVO_SETUP) ||
|
key.startsWith(LocalStorageKeys.LAST_CONVO_SETUP) ||
|
||||||
|
|
|
||||||
|
|
@ -1285,6 +1285,8 @@ export enum LocalStorageKeys {
|
||||||
SHOW_ANALYSIS_CODE = 'showAnalysisCode',
|
SHOW_ANALYSIS_CODE = 'showAnalysisCode',
|
||||||
/** Last selected MCP values per conversation ID */
|
/** Last selected MCP values per conversation ID */
|
||||||
LAST_MCP_ = 'LAST_MCP_',
|
LAST_MCP_ = 'LAST_MCP_',
|
||||||
|
/** Last checked toggle for Code Interpreter API per conversation ID */
|
||||||
|
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ForkOptions {
|
export enum ForkOptions {
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,20 @@ export default function createPayload(submission: t.TSubmission) {
|
||||||
};
|
};
|
||||||
|
|
||||||
let server = EndpointURLs[endpointType ?? endpoint];
|
let server = EndpointURLs[endpointType ?? endpoint];
|
||||||
const isEphemeralAgent = (ephemeralAgent?.mcp?.length ?? 0) > 0 && !s.isAgentsEndpoint(endpoint);
|
const isEphemeral = s.isEphemeralAgent(endpoint, ephemeralAgent);
|
||||||
|
|
||||||
if (isEdited && s.isAssistantsEndpoint(endpoint)) {
|
if (isEdited && s.isAssistantsEndpoint(endpoint)) {
|
||||||
server += '/modify';
|
server += '/modify';
|
||||||
} else if (isEdited) {
|
} else if (isEdited) {
|
||||||
server = server.replace('/ask/', '/edit/');
|
server = server.replace('/ask/', '/edit/');
|
||||||
} else if (isEphemeralAgent) {
|
} else if (isEphemeral) {
|
||||||
server = `${EndpointURLs[s.EModelEndpoint.agents]}/${endpoint}`;
|
server = `${EndpointURLs[s.EModelEndpoint.agents]}/${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: t.TPayload = {
|
const payload: t.TPayload = {
|
||||||
...userMessage,
|
...userMessage,
|
||||||
...endpointOption,
|
...endpointOption,
|
||||||
ephemeralAgent: isEphemeralAgent ? ephemeralAgent : undefined,
|
ephemeralAgent: isEphemeral ? ephemeralAgent : undefined,
|
||||||
isContinued: !!(isEdited && isContinued),
|
isContinued: !!(isEdited && isContinued),
|
||||||
conversationId,
|
conversationId,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Tools } from './types/assistants';
|
import { Tools } from './types/assistants';
|
||||||
import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants';
|
import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants';
|
||||||
|
import type { TEphemeralAgent } from './types';
|
||||||
import type { TFile } from './types/files';
|
import type { TFile } from './types/files';
|
||||||
|
|
||||||
export const isUUID = z.string().uuid();
|
export const isUUID = z.string().uuid();
|
||||||
|
|
@ -88,6 +89,21 @@ export const isAgentsEndpoint = (_endpoint?: EModelEndpoint.agents | null | stri
|
||||||
return endpoint === EModelEndpoint.agents;
|
return endpoint === EModelEndpoint.agents;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isEphemeralAgent = (
|
||||||
|
endpoint?: EModelEndpoint.agents | null | string,
|
||||||
|
ephemeralAgent?: TEphemeralAgent | null,
|
||||||
|
) => {
|
||||||
|
if (!ephemeralAgent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isAgentsEndpoint(endpoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasMCPSelected = (ephemeralAgent?.mcp?.length ?? 0) > 0;
|
||||||
|
const hasCodeSelected = (ephemeralAgent?.execute_code ?? false) === true;
|
||||||
|
return hasMCPSelected || hasCodeSelected;
|
||||||
|
};
|
||||||
|
|
||||||
export const isParamEndpoint = (
|
export const isParamEndpoint = (
|
||||||
endpoint: EModelEndpoint | string,
|
endpoint: EModelEndpoint | string,
|
||||||
endpointType?: EModelEndpoint | string,
|
endpointType?: EModelEndpoint | string,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ export type TEndpointOption = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TEphemeralAgent = {
|
export type TEphemeralAgent = {
|
||||||
mcp: string[];
|
mcp?: string[];
|
||||||
|
execute_code?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPayload = Partial<TMessage> &
|
export type TPayload = Partial<TMessage> &
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue