🪄 feat: Agent Artifacts (#5804)

* refactor: remove artifacts toggle

* refactor: allow hiding side panel while allowing artifacts view

* chore: rename SidePanelGroup to SidePanel for clarity

* Revert "refactor: remove artifacts toggle"

This reverts commit f884c2cfcd.

* feat: add artifacts capability to agent configuration

* refactor: conditionally set artifacts mode based on endpoint type

* feat: Artifacts Capability for Agents

* refactor: enhance getStreamText method to handle intermediate replies and add `stream_options` for openai/azure

* feat: localize progress text and improve UX in CodeAnalyze and ExecuteCode components for expanding analysis
This commit is contained in:
Danny Avila 2025-02-11 18:00:38 -05:00 committed by GitHub
parent 46f034250d
commit bfbaaebd2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 534 additions and 310 deletions

View file

@ -1066,9 +1066,14 @@ ${convo}
}); });
} }
getStreamText() { /**
*
* @param {string[]} [intermediateReply]
* @returns {string}
*/
getStreamText(intermediateReply) {
if (!this.streamHandler) { if (!this.streamHandler) {
return ''; return intermediateReply?.join('') ?? '';
} }
let thinkMatch; let thinkMatch;
@ -1088,7 +1093,10 @@ ${convo}
} }
} }
const reasoningTokens = reasoningText.length > 0 ? `:::thinking\n${reasoningText}\n:::\n` : ''; const reasoningTokens =
reasoningText.length > 0
? `:::thinking\n${reasoningText.replace('<think>', '').replace('</think>', '').trim()}\n:::\n`
: '';
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`; return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
} }
@ -1327,11 +1335,19 @@ ${convo}
streamPromise = new Promise((resolve) => { streamPromise = new Promise((resolve) => {
streamResolve = resolve; streamResolve = resolve;
}); });
/** @type {OpenAI.OpenAI.CompletionCreateParamsStreaming} */
const params = {
...modelOptions,
stream: true,
};
if (
this.options.endpoint === EModelEndpoint.openAI ||
this.options.endpoint === EModelEndpoint.azureOpenAI
) {
params.stream_options = { include_usage: true };
}
const stream = await openai.beta.chat.completions const stream = await openai.beta.chat.completions
.stream({ .stream(params)
...modelOptions,
stream: true,
})
.on('abort', () => { .on('abort', () => {
/* Do nothing here */ /* Do nothing here */
}) })
@ -1471,7 +1487,7 @@ ${convo}
err?.message?.includes('abort') || err?.message?.includes('abort') ||
(err instanceof OpenAI.APIError && err?.message?.includes('abort')) (err instanceof OpenAI.APIError && err?.message?.includes('abort'))
) { ) {
return intermediateReply.join(''); return this.getStreamText(intermediateReply);
} }
if ( if (
err?.message?.includes( err?.message?.includes(
@ -1489,7 +1505,7 @@ ${convo}
if (this.streamHandler && this.streamHandler.reasoningTokens.length) { if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
return this.getStreamText(); return this.getStreamText();
} else if (intermediateReply.length > 0) { } else if (intermediateReply.length > 0) {
return intermediateReply.join(''); return this.getStreamText(intermediateReply);
} else { } else {
throw err; throw err;
} }
@ -1497,7 +1513,7 @@ ${convo}
if (this.streamHandler && this.streamHandler.reasoningTokens.length) { if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
return this.getStreamText(); return this.getStreamText();
} else if (intermediateReply.length > 0) { } else if (intermediateReply.length > 0) {
return intermediateReply.join(''); return this.getStreamText(intermediateReply);
} else { } else {
throw err; throw err;
} }

View file

@ -35,6 +35,9 @@ const agentSchema = mongoose.Schema(
model_parameters: { model_parameters: {
type: Object, type: Object,
}, },
artifacts: {
type: String,
},
access_level: { access_level: {
type: Number, type: Number,
}, },

View file

@ -13,6 +13,7 @@ const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options')
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize'); const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
const initCustom = require('~/server/services/Endpoints/custom/initialize'); const initCustom = require('~/server/services/Endpoints/custom/initialize');
const initGoogle = require('~/server/services/Endpoints/google/initialize'); const initGoogle = require('~/server/services/Endpoints/google/initialize');
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getCustomEndpointConfig } = require('~/server/services/Config'); const { getCustomEndpointConfig } = require('~/server/services/Config');
const { loadAgentTools } = require('~/server/services/ToolService'); const { loadAgentTools } = require('~/server/services/ToolService');
const AgentClient = require('~/server/controllers/agents/client'); const AgentClient = require('~/server/controllers/agents/client');
@ -72,6 +73,16 @@ const primeResources = async (_attachments, _tool_resources) => {
} }
}; };
/**
* @param {object} params
* @param {ServerRequest} params.req
* @param {ServerResponse} params.res
* @param {Agent} params.agent
* @param {object} [params.endpointOption]
* @param {AgentToolResources} [params.tool_resources]
* @param {boolean} [params.isInitialAgent]
* @returns {Promise<Agent>}
*/
const initializeAgentOptions = async ({ const initializeAgentOptions = async ({
req, req,
res, res,
@ -132,6 +143,13 @@ const initializeAgentOptions = async ({
agent.model_parameters.model = agent.model; agent.model_parameters.model = agent.model;
} }
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
agent.additional_instructions = generateArtifactsPrompt({
endpoint: agent.provider,
artifacts: agent.artifacts,
});
}
const tokensModel = const tokensModel =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model; agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;

View file

@ -200,6 +200,7 @@ function generateConfig(key, baseURL, endpoint) {
config.capabilities = [ config.capabilities = [
AgentCapabilities.execute_code, AgentCapabilities.execute_code,
AgentCapabilities.file_search, AgentCapabilities.file_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions, AgentCapabilities.actions,
AgentCapabilities.tools, AgentCapabilities.tools,
]; ];

View file

@ -1,4 +1,4 @@
import { AgentCapabilities } from 'librechat-data-provider'; import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider'; import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
import type { OptionWithIcon, ExtendedFile } from './types'; import type { OptionWithIcon, ExtendedFile } from './types';
@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon &
}; };
export type TAgentCapabilities = { export type TAgentCapabilities = {
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.file_search]: boolean; [AgentCapabilities.file_search]: boolean;
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.end_after_tools]?: boolean; [AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean; [AgentCapabilities.hide_sequential_outputs]?: boolean;
}; };
@ -26,4 +26,5 @@ export type AgentForm = {
tools?: string[]; tools?: string[];
provider?: AgentProvider | OptionWithIcon; provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[]; agent_ids?: string[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
} & TAgentCapabilities; } & TAgentCapabilities;

View file

@ -68,8 +68,8 @@ export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<t.EModelEndpoint, string>; export type LastSelectedModels = Record<t.EModelEndpoint, string>;
export type LocalizeFunction = ( export type LocalizeFunction = (
phraseKey: TranslationKeys, phraseKey: TranslationKeys,
options?: Record<string, string | number> options?: Record<string, string | number>,
) => string; ) => string;
export type ChatFormValues = { text: string }; export type ChatFormValues = { text: string };
@ -89,6 +89,7 @@ export type IconMapProps = {
iconURL?: string; iconURL?: string;
context?: 'landing' | 'menu-item' | 'nav' | 'message'; context?: 'landing' | 'menu-item' | 'nav' | 'message';
endpoint?: string | null; endpoint?: string | null;
endpointType?: string;
assistantName?: string; assistantName?: string;
agentName?: string; agentName?: string;
avatar?: string; avatar?: string;

View file

@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) {
select: useCallback( select: useCallback(
(data: TMessage[]) => { (data: TMessage[]) => {
const dataTree = buildTree({ messages: data, fileMap }); const dataTree = buildTree({ messages: data, fileMap });
return dataTree?.length === 0 ? null : dataTree ?? null; return dataTree?.length === 0 ? null : (dataTree ?? null);
}, },
[fileMap], [fileMap],
), ),
@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) {
<ChatFormProvider {...methods}> <ChatFormProvider {...methods}>
<ChatContext.Provider value={chatHelpers}> <ChatContext.Provider value={chatHelpers}>
<AddedChatContext.Provider value={addedChatHelpers}> <AddedChatContext.Provider value={addedChatHelpers}>
<Presentation useSidePanel={true}> <Presentation>
{content} {content}
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent"> <div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
<ChatForm index={index} /> <ChatForm index={index} />

View file

@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite'; import MarkdownLite from './MarkdownLite';
import store from '~/store'; import store from '~/store';
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function CodeAnalyze({ export default function CodeAnalyze({
initialProgress = 0.1, initialProgress = 0.1,
code, code,
@ -22,9 +25,6 @@ export default function CodeAnalyze({
const progress = useProgress(initialProgress); const progress = useProgress(initialProgress);
const showAnalysisCode = useRecoilValue(store.showCode); const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode); const [showCode, setShowCode] = useState(showAnalysisCode);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference; const offset = circumference - progress * circumference;
const logs = outputs.reduce((acc, output) => { const logs = outputs.reduce((acc, output) => {
@ -53,9 +53,10 @@ export default function CodeAnalyze({
<ProgressText <ProgressText
progress={progress} progress={progress}
onClick={() => setShowCode((prev) => !prev)} onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing" inProgressText={localize('com_ui_analyzing')}
finishedText="Finished analyzing" finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length} hasInput={!!code.length}
isExpanded={showCode}
/> />
</div> </div>
{showCode && ( {showCode && (

View file

@ -4,10 +4,10 @@ import type { TAttachment } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText'; import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon'; import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import { useProgress, useLocalize } from '~/hooks';
import { CodeInProgress } from './CodeProgress'; import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment'; import Attachment from './Attachment';
import LogContent from './LogContent'; import LogContent from './LogContent';
import { useProgress } from '~/hooks';
import store from '~/store'; import store from '~/store';
interface ParsedArgs { interface ParsedArgs {
@ -36,6 +36,9 @@ export function useParseArgs(args: string): ParsedArgs {
}, [args]); }, [args]);
} }
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function ExecuteCode({ export default function ExecuteCode({
initialProgress = 0.1, initialProgress = 0.1,
args, args,
@ -49,14 +52,12 @@ export default function ExecuteCode({
isSubmitting: boolean; isSubmitting: boolean;
attachments?: TAttachment[]; attachments?: TAttachment[];
}) { }) {
const localize = useLocalize();
const showAnalysisCode = useRecoilValue(store.showCode); const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode); const [showCode, setShowCode] = useState(showAnalysisCode);
const { lang, code } = useParseArgs(args); const { lang, code } = useParseArgs(args);
const progress = useProgress(initialProgress); const progress = useProgress(initialProgress);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference; const offset = circumference - progress * circumference;
return ( return (
@ -78,9 +79,10 @@ export default function ExecuteCode({
<ProgressText <ProgressText
progress={progress} progress={progress}
onClick={() => setShowCode((prev) => !prev)} onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing" inProgressText={localize('com_ui_analyzing')}
finishedText="Finished analyzing" finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length} hasInput={!!code.length}
isExpanded={showCode}
/> />
</div> </div>
{showCode && ( {showCode && (
@ -105,9 +107,7 @@ export default function ExecuteCode({
)} )}
</div> </div>
)} )}
{attachments?.map((attachment, index) => ( {attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
<Attachment attachment={attachment} key={index} />
))}
</> </>
); );
} }

View file

@ -42,6 +42,7 @@ export default function ProgressText({
authText, authText,
hasInput = true, hasInput = true,
popover = false, popover = false,
isExpanded = false,
}: { }: {
progress: number; progress: number;
onClick?: () => void; onClick?: () => void;
@ -50,8 +51,9 @@ export default function ProgressText({
authText?: string; authText?: string;
hasInput?: boolean; hasInput?: boolean;
popover?: boolean; popover?: boolean;
isExpanded?: boolean;
}) { }) {
const text = progress < 1 ? authText ?? inProgressText : finishedText; const text = progress < 1 ? (authText ?? inProgressText) : finishedText;
return ( return (
<Wrapper popover={popover}> <Wrapper popover={popover}>
<button <button
@ -61,7 +63,13 @@ export default function ProgressText({
onClick={onClick} onClick={onClick}
> >
{text} {text}
<svg width="16" height="17" viewBox="0 0 16 17" fill="none"> <svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
className={isExpanded ? 'rotate-180' : 'rotate-0'}
>
<path <path
className={hasInput ? '' : 'stroke-transparent'} className={hasInput ? '' : 'stroke-transparent'}
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203" d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"

View file

@ -1,37 +1,19 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider'; import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider'; import { useDeleteFilesMutation } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper'; import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import Artifacts from '~/components/Artifacts/Artifacts'; import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanel } from '~/components/SidePanel'; import { SidePanelGroup } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks'; import { useSetFilesToDelete } from '~/hooks';
import { EditorProvider } from '~/Providers'; import { EditorProvider } from '~/Providers';
import store from '~/store'; import store from '~/store';
const defaultInterface = getConfigDefaults().interface; export default function Presentation({ children }: { children: React.ReactNode }) {
export default function Presentation({
children,
useSidePanel = false,
panel,
}: {
children: React.ReactNode;
panel?: React.ReactNode;
useSidePanel?: boolean;
}) {
const { data: startupConfig } = useGetStartupConfig();
const artifacts = useRecoilValue(store.artifactsState); const artifacts = useRecoilValue(store.artifactsState);
const codeArtifacts = useRecoilValue(store.codeArtifacts);
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const artifactsVisible = useRecoilValue(store.artifactsVisible); const artifactsVisible = useRecoilValue(store.artifactsVisible);
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const setFilesToDelete = useSetFilesToDelete(); const setFilesToDelete = useSetFilesToDelete();
const { mutateAsync } = useDeleteFilesMutation({ const { mutateAsync } = useDeleteFilesMutation({
@ -83,35 +65,24 @@ export default function Presentation({
</div> </div>
); );
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
<SidePanel
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
artifacts={
artifactsVisible === true &&
codeArtifacts === true &&
Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanel>
</DragDropWrapper>
);
}
return ( return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation"> <DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
{layout()} <SidePanelGroup
{panel != null && panel} defaultLayout={defaultLayout}
fullPanelCollapse={fullCollapse}
defaultCollapsed={defaultCollapsed}
artifacts={
artifactsVisible === true && Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanelGroup>
</DragDropWrapper> </DragDropWrapper>
); );
} }

View file

@ -10,7 +10,7 @@ import {
AgentCapabilities, AgentCapabilities,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider'; import type { TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common'; import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils'; import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider'; import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks'; import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
@ -26,6 +26,7 @@ import AgentAvatar from './AgentAvatar';
import { Spinner } from '~/components'; import { Spinner } from '~/components';
import FileSearch from './FileSearch'; import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent'; import ShareAgent from './ShareAgent';
import Artifacts from './Artifacts';
import AgentTool from './AgentTool'; import AgentTool from './AgentTool';
import CodeForm from './Code/Form'; import CodeForm from './Code/Form';
import { Panel } from '~/common'; import { Panel } from '~/common';
@ -77,6 +78,10 @@ export default function AgentConfig({
() => agentsConfig?.capabilities.includes(AgentCapabilities.actions), () => agentsConfig?.capabilities.includes(AgentCapabilities.actions),
[agentsConfig], [agentsConfig],
); );
const artifactsEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.artifacts) ?? false,
[agentsConfig],
);
const fileSearchEnabled = useMemo( const fileSearchEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false, () => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false,
[agentsConfig], [agentsConfig],
@ -150,7 +155,7 @@ export default function AgentConfig({
onSuccess: (data) => { onSuccess: (data) => {
setCurrentAgentId(data.id); setCurrentAgentId(data.id);
showToast({ showToast({
message: `${localize('com_assistants_create_success ')} ${ message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent') data.name ?? localize('com_ui_agent')
}`, }`,
}); });
@ -178,18 +183,10 @@ export default function AgentConfig({
}, [agent_id, setActivePanel, showToast, localize]); }, [agent_id, setActivePanel, showToast, localize]);
const providerValue = typeof provider === 'string' ? provider : provider?.value; const providerValue = typeof provider === 'string' ? provider : provider?.value;
let Icon: IconComponentTypes | null | undefined;
let endpointType: EModelEndpoint | undefined; let endpointType: EModelEndpoint | undefined;
let endpointIconURL: string | undefined; let endpointIconURL: string | undefined;
let iconKey: string | undefined; let iconKey: string | undefined;
let Icon:
| React.ComponentType<
React.SVGProps<SVGSVGElement> & {
endpoint: string;
endpointType: EModelEndpoint | undefined;
iconURL: string | undefined;
}
>
| undefined;
if (providerValue !== undefined) { if (providerValue !== undefined) {
endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type'); endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type');
@ -346,6 +343,8 @@ export default function AgentConfig({
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />} {codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
{/* File Search */} {/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />} {fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
</div> </div>
)} )}
{/* Agent Tools & Actions */} {/* Agent Tools & Actions */}

View file

@ -120,6 +120,7 @@ export default function AgentPanel({
const { const {
name, name,
artifacts,
description, description,
instructions, instructions,
model: _model, model: _model,
@ -139,6 +140,7 @@ export default function AgentPanel({
agent_id, agent_id,
data: { data: {
name, name,
artifacts,
description, description,
instructions, instructions,
model, model,
@ -162,6 +164,7 @@ export default function AgentPanel({
create.mutate({ create.mutate({
name, name,
artifacts,
description, description,
instructions, instructions,
model, model,
@ -184,7 +187,7 @@ export default function AgentPanel({
const canEditAgent = useMemo(() => { const canEditAgent = useMemo(() => {
const canEdit = const canEdit =
agentQuery.data?.isCollaborative ?? false (agentQuery.data?.isCollaborative ?? false)
? true ? true
: agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN; : agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN;

View file

@ -55,8 +55,8 @@ export default function AgentSelect({
}; };
const capabilities: TAgentCapabilities = { const capabilities: TAgentCapabilities = {
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false, [AgentCapabilities.file_search]: false,
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.end_after_tools]: false, [AgentCapabilities.end_after_tools]: false,
[AgentCapabilities.hide_sequential_outputs]: false, [AgentCapabilities.hide_sequential_outputs]: false,
}; };

View file

@ -0,0 +1,124 @@
import { useFormContext } from 'react-hook-form';
import { ArtifactModes, AgentCapabilities } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
import {
Switch,
HoverCard,
HoverCardPortal,
HoverCardContent,
HoverCardTrigger,
} from '~/components/ui';
import { useLocalize } from '~/hooks';
import { CircleHelpIcon } from '~/components/svg';
import { ESide } from '~/common';
export default function Artifacts() {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { setValue, watch } = methods;
const artifactsMode = watch(AgentCapabilities.artifacts);
const handleArtifactsChange = (value: boolean) => {
setValue(AgentCapabilities.artifacts, value ? ArtifactModes.DEFAULT : '', {
shouldDirty: true,
});
};
const handleShadcnuiChange = (value: boolean) => {
setValue(AgentCapabilities.artifacts, value ? ArtifactModes.SHADCNUI : ArtifactModes.DEFAULT, {
shouldDirty: true,
});
};
const handleCustomModeChange = (value: boolean) => {
setValue(AgentCapabilities.artifacts, value ? ArtifactModes.CUSTOM : ArtifactModes.DEFAULT, {
shouldDirty: true,
});
};
const isEnabled = artifactsMode !== undefined && artifactsMode !== '';
const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM;
const isShadcnEnabled = artifactsMode === ArtifactModes.SHADCNUI;
return (
<div className="w-full">
<div className="mb-1.5 flex items-center gap-2">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_ui_artifacts')}
</label>
</span>
</div>
<div className="flex flex-col gap-3">
<SwitchItem
id="artifacts"
label={localize('com_ui_artifacts_toggle_agent')}
checked={isEnabled}
onCheckedChange={handleArtifactsChange}
hoverCardText={localize('com_nav_info_code_artifacts_agent')}
/>
<SwitchItem
id="includeShadcnui"
label={localize('com_ui_include_shadcnui_agent')}
checked={isShadcnEnabled}
onCheckedChange={handleShadcnuiChange}
hoverCardText={localize('com_nav_info_include_shadcnui')}
disabled={!isEnabled || isCustomEnabled}
/>
<SwitchItem
id="customPromptMode"
label={localize('com_ui_custom_prompt_mode')}
checked={isCustomEnabled}
onCheckedChange={handleCustomModeChange}
hoverCardText={localize('com_nav_info_custom_prompt_mode')}
disabled={!isEnabled}
/>
</div>
</div>
);
}
function SwitchItem({
id,
label,
checked,
onCheckedChange,
hoverCardText,
disabled = false,
}: {
id: string;
label: string;
checked: boolean;
onCheckedChange: (value: boolean) => void;
hoverCardText: string;
disabled?: boolean;
}) {
return (
<HoverCard openDelay={50}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className={disabled ? 'text-text-tertiary' : ''}>{label}</div>
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">{hoverCardText}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
<Switch
id={id}
checked={checked}
onCheckedChange={onCheckedChange}
className="ml-4"
data-testid={id}
disabled={disabled}
/>
</div>
</HoverCard>
);
}

View file

@ -86,7 +86,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
</button> </button>
)} )}
<HoverCardTrigger> <HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" /> <CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger> </HoverCardTrigger>
</div> </div>
<HoverCardPortal> <HoverCardPortal>

View file

@ -29,7 +29,7 @@ export default function FileSearchCheckbox() {
{...field} {...field}
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()} value={field.value.toString()}
/> />
)} )}
@ -38,7 +38,6 @@ export default function FileSearchCheckbox() {
type="button" type="button"
className="flex items-center space-x-2" className="flex items-center space-x-2"
onClick={() => onClick={() =>
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), { setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
shouldDirty: true, shouldDirty: true,
}) })
@ -51,7 +50,7 @@ export default function FileSearchCheckbox() {
{localize('com_agents_enable_file_search')} {localize('com_agents_enable_file_search')}
</label> </label>
<HoverCardTrigger> <HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" /> <CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger> </HoverCardTrigger>
</button> </button>
<HoverCardPortal> <HoverCardPortal>

View file

@ -1,78 +1,58 @@
import throttle from 'lodash/throttle'; import { useState, useCallback, useMemo, memo } from 'react';
import { getConfigDefaults } from 'librechat-data-provider';
import { useUserKeyQuery } from 'librechat-data-provider/react-query'; import { useUserKeyQuery } from 'librechat-data-provider/react-query';
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider'; import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels'; import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable'; import { ResizableHandleAlt, ResizablePanel } from '~/components/ui/Resizable';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks'; import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks'; import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useGetEndpointsQuery } from '~/data-provider';
import NavToggle from '~/components/Nav/NavToggle'; import NavToggle from '~/components/Nav/NavToggle';
import { cn, getEndpointField } from '~/utils'; import { cn, getEndpointField } from '~/utils';
import { useChatContext } from '~/Providers'; import { useChatContext } from '~/Providers';
import Switcher from './Switcher'; import Switcher from './Switcher';
import Nav from './Nav'; import Nav from './Nav';
interface SidePanelProps {
defaultLayout?: number[] | undefined;
defaultCollapsed?: boolean;
navCollapsedSize?: number;
fullPanelCollapse?: boolean;
artifacts?: React.ReactNode;
children: React.ReactNode;
}
const defaultMinSize = 20; const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface;
const normalizeLayout = (layout: number[]) => {
const sum = layout.reduce((acc, size) => acc + size, 0);
if (Math.abs(sum - 100) < 0.01) {
return layout.map((size) => Number(size.toFixed(2)));
}
const factor = 100 / sum;
const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
const adjustedSum = normalizedLayout.reduce(
(acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
0,
);
normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
return normalizedLayout;
};
const SidePanel = ({ const SidePanel = ({
defaultLayout = [97, 3], defaultSize,
defaultCollapsed = false, panelRef,
fullPanelCollapse = false,
navCollapsedSize = 3, navCollapsedSize = 3,
artifacts, hasArtifacts,
children, minSize,
}: SidePanelProps) => { setMinSize,
collapsedSize,
setCollapsedSize,
isCollapsed,
setIsCollapsed,
fullCollapse,
setFullCollapse,
interfaceConfig,
}: {
defaultSize?: number;
hasArtifacts: boolean;
navCollapsedSize?: number;
minSize: number;
setMinSize: React.Dispatch<React.SetStateAction<number>>;
collapsedSize: number;
setCollapsedSize: React.Dispatch<React.SetStateAction<number>>;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
fullCollapse: boolean;
setFullCollapse: React.Dispatch<React.SetStateAction<boolean>>;
panelRef: React.RefObject<ImperativePanelHandle>;
interfaceConfig: TInterfaceConfig;
}) => {
const localize = useLocalize(); const localize = useLocalize();
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const [minSize, setMinSize] = useState(defaultMinSize);
const [newUser, setNewUser] = useLocalStorage('newUser', true); const [newUser, setNewUser] = useLocalStorage('newUser', true);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => (startupConfig?.interface ?? defaultInterface) as Partial<TInterfaceConfig>,
[startupConfig],
);
const isSmallScreen = useMediaQuery('(max-width: 767px)'); const isSmallScreen = useMediaQuery('(max-width: 767px)');
const { conversation } = useChatContext(); const { conversation } = useChatContext();
const { endpoint } = conversation ?? {}; const { endpoint } = conversation ?? {};
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? ''); const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
const panelRef = useRef<ImperativePanelHandle>(null);
const defaultActive = useMemo(() => { const defaultActive = useMemo(() => {
const activePanel = localStorage.getItem('side:active-panel'); const activePanel = localStorage.getItem('side:active-panel');
return typeof activePanel === 'string' ? activePanel : undefined; return typeof activePanel === 'string' ? activePanel : undefined;
@ -113,46 +93,6 @@ const SidePanel = ({
interfaceConfig, interfaceConfig,
}); });
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledSaveLayout = useCallback(
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
useEffect(() => {
if (isSmallScreen) {
setIsCollapsed(true);
setCollapsedSize(0);
setMinSize(defaultMinSize);
setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
return;
} else {
setIsCollapsed(defaultCollapsed);
setCollapsedSize(navCollapsedSize);
setMinSize(defaultMinSize);
}
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
const toggleNavVisible = useCallback(() => { const toggleNavVisible = useCallback(() => {
if (newUser) { if (newUser) {
setNewUser(false); setNewUser(false);
@ -173,127 +113,84 @@ const SidePanel = ({
} }
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]); }, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
return ( return (
<> <>
<ResizablePanelGroup <div
direction="horizontal" onMouseEnter={() => setIsHovering(true)}
onLayout={(sizes) => throttledSaveLayout(sizes)} onMouseLeave={() => setIsHovering(false)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation" className="relative flex w-px items-center justify-center"
> >
<ResizablePanel <NavToggle
defaultSize={currentLayout[0]} navVisible={!isCollapsed}
minSize={minSizeMain} isHovering={isHovering}
order={1} onToggle={toggleNavVisible}
id="messages-view" setIsHovering={setIsHovering}
>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="relative flex w-px items-center justify-center"
>
<NavToggle
navVisible={!isCollapsed}
isHovering={isHovering}
onToggle={toggleNavVisible}
setIsHovering={setIsHovering}
className={cn(
'fixed top-1/2',
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'mr-9'
: 'mr-16',
)}
translateX={false}
side="right"
/>
</div>
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
<ResizableHandleAlt withHandle className="bg-transparent text-text-primary" />
)}
<ResizablePanel
tagName="nav"
id="controls-nav"
order={artifacts != null ? 3 : 2}
aria-label={localize('com_ui_controls')}
role="region"
collapsedSize={collapsedSize}
defaultSize={currentLayout[currentLayout.length - 1]}
collapsible={true}
minSize={minSize}
maxSize={40}
ref={panelRef}
style={{
overflowY: 'auto',
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
}}
onExpand={() => {
setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false');
}}
onCollapse={() => {
setIsCollapsed(true);
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
className={cn( className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity', 'fixed top-1/2',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]', (isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse ? 'mr-9'
? 'hidden min-w-0' : 'mr-16',
: 'opacity-100',
)} )}
> translateX={false}
{interfaceConfig.modelSelect === true && ( side="right"
<div />
className={cn( </div>
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background', {(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
isCollapsed ? 'h-[52px]' : 'px-2', <ResizableHandleAlt withHandle className="bg-transparent text-text-primary" />
)} )}
> <ResizablePanel
<Switcher tagName="nav"
isCollapsed={isCollapsed} id="controls-nav"
endpointKeyProvided={keyProvided} order={hasArtifacts != null ? 3 : 2}
endpoint={endpoint} aria-label={localize('com_ui_controls')}
/> role="region"
</div> collapsedSize={collapsedSize}
)} defaultSize={defaultSize}
<Nav collapsible={true}
resize={panelRef.current?.resize} minSize={minSize}
isCollapsed={isCollapsed} maxSize={40}
defaultActive={defaultActive} ref={panelRef}
links={Links} style={{
/> overflowY: 'auto',
</ResizablePanel> transition: 'width 0.2s ease, visibility 0s linear 0.2s',
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
}} }}
/> onExpand={() => {
setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false');
}}
onCollapse={() => {
setIsCollapsed(true);
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'hidden min-w-0'
: 'opacity-100',
)}
>
{interfaceConfig.modelSelect === true && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
isCollapsed ? 'h-[52px]' : 'px-2',
)}
>
<Switcher
isCollapsed={isCollapsed}
endpointKeyProvided={keyProvided}
endpoint={endpoint}
/>
</div>
)}
<Nav
resize={panelRef.current?.resize}
isCollapsed={isCollapsed}
defaultActive={defaultActive}
links={Links}
/>
</ResizablePanel>
</> </>
); );
}; };

View file

@ -0,0 +1,152 @@
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import throttle from 'lodash/throttle';
import { useRecoilValue } from 'recoil';
import { getConfigDefaults } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { useGetStartupConfig } from '~/data-provider';
import { normalizeLayout } from '~/utils';
import { useMediaQuery } from '~/hooks';
import SidePanel from './SidePanel';
import store from '~/store';
interface SidePanelProps {
defaultLayout?: number[] | undefined;
defaultCollapsed?: boolean;
navCollapsedSize?: number;
fullPanelCollapse?: boolean;
artifacts?: React.ReactNode;
children: React.ReactNode;
}
const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface;
const SidePanelGroup = ({
defaultLayout = [97, 3],
defaultCollapsed = false,
fullPanelCollapse = false,
navCollapsedSize = 3,
artifacts,
children,
}: SidePanelProps) => {
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const panelRef = useRef<ImperativePanelHandle>(null);
const [minSize, setMinSize] = useState(defaultMinSize);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
const throttledSaveLayout = useCallback(
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
useEffect(() => {
if (isSmallScreen) {
setIsCollapsed(true);
setCollapsedSize(0);
setMinSize(defaultMinSize);
setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
return;
} else {
setIsCollapsed(defaultCollapsed);
setCollapsedSize(navCollapsedSize);
setMinSize(defaultMinSize);
}
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
return (
<>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
{!hideSidePanel && interfaceConfig.sidePanel === true && (
<SidePanel
panelRef={panelRef}
minSize={minSize}
setMinSize={setMinSize}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
collapsedSize={collapsedSize}
setCollapsedSize={setCollapsedSize}
fullCollapse={fullCollapse}
setFullCollapse={setFullCollapse}
defaultSize={currentLayout[currentLayout.length - 1]}
hasArtifacts={artifacts != null}
interfaceConfig={interfaceConfig}
/>
)}
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
}}
/>
</>
);
};
export default memo(SidePanelGroup);

View file

@ -1,2 +1,2 @@
export { default as SidePanel } from './SidePanel'; export { default as SidePanelGroup } from './SidePanelGroup';
export { default as SideNav } from './Nav'; export { default as SideNav } from './Nav';

View file

@ -170,7 +170,10 @@ export default function useChatFunctions({
endpointType, endpointType,
overrideConvoId, overrideConvoId,
overrideUserMessageId, overrideUserMessageId,
artifacts: getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode }), artifacts:
endpoint !== EModelEndpoint.agents
? getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode })
: undefined,
}, },
convo, convo,
) as TEndpointOption; ) as TEndpointOption;
@ -228,7 +231,6 @@ export default function useChatFunctions({
conversationId, conversationId,
unfinished: false, unfinished: false,
isCreatedByUser: false, isCreatedByUser: false,
isEdited: isEditOrContinue,
iconURL: convo?.iconURL, iconURL: convo?.iconURL,
model: convo?.model, model: convo?.model,
error: false, error: false,

View file

@ -1,4 +1,6 @@
{ {
"com_ui_analyzing": "Analyzing",
"com_ui_analyzing_finished": "Finished analyzing",
"com_a11y_ai_composing": "The AI is still composing.", "com_a11y_ai_composing": "The AI is still composing.",
"com_a11y_end": "The AI has finished their reply.", "com_a11y_end": "The AI has finished their reply.",
"com_a11y_start": "The AI has started their reply.", "com_a11y_start": "The AI has started their reply.",
@ -372,6 +374,7 @@
"com_nav_help_faq": "Help & FAQ", "com_nav_help_faq": "Help & FAQ",
"com_nav_hide_panel": "Hide right-most side panel", "com_nav_hide_panel": "Hide right-most side panel",
"com_nav_info_code_artifacts": "Enables the display of experimental code artifacts next to the chat", "com_nav_info_code_artifacts": "Enables the display of experimental code artifacts next to the chat",
"com_nav_info_code_artifacts_agent": "Enables the use of code artifacts for this agent. By default, additional instructions specific to the use of artifacts are added, unless \"Custom Prompt Mode\" is enabled.",
"com_nav_info_custom_prompt_mode": "When enabled, the default artifacts system prompt will not be included. All artifact-generating instructions must be provided manually in this mode.", "com_nav_info_custom_prompt_mode": "When enabled, the default artifacts system prompt will not be included. All artifact-generating instructions must be provided manually in this mode.",
"com_nav_info_delete_cache_storage": "This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.", "com_nav_info_delete_cache_storage": "This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.",
"com_nav_info_enter_to_send": "When enabled, pressing `ENTER` will send your message. When disabled, pressing Enter will add a new line, and you'll need to press `CTRL + ENTER` / `⌘ + ENTER` to send your message.", "com_nav_info_enter_to_send": "When enabled, pressing `ENTER` will send your message. When disabled, pressing Enter will add a new line, and you'll need to press `CTRL + ENTER` / `⌘ + ENTER` to send your message.",
@ -509,6 +512,7 @@
"com_ui_artifact_click": "Click to open", "com_ui_artifact_click": "Click to open",
"com_ui_artifacts": "Artifacts", "com_ui_artifacts": "Artifacts",
"com_ui_artifacts_toggle": "Toggle Artifacts UI", "com_ui_artifacts_toggle": "Toggle Artifacts UI",
"com_ui_artifacts_toggle_agent": "Enable Artifacts",
"com_ui_ascending": "Asc", "com_ui_ascending": "Asc",
"com_ui_assistant": "Assistant", "com_ui_assistant": "Assistant",
"com_ui_assistant_delete_error": "There was an error deleting the assistant", "com_ui_assistant_delete_error": "There was an error deleting the assistant",
@ -687,6 +691,7 @@
"com_ui_import_conversation_info": "Import conversations from a JSON file", "com_ui_import_conversation_info": "Import conversations from a JSON file",
"com_ui_import_conversation_success": "Conversations imported successfully", "com_ui_import_conversation_success": "Conversations imported successfully",
"com_ui_include_shadcnui": "Include shadcn/ui components instructions", "com_ui_include_shadcnui": "Include shadcn/ui components instructions",
"com_ui_include_shadcnui_agent": "Include shadcn/ui instructions",
"com_ui_input": "Input", "com_ui_input": "Input",
"com_ui_instructions": "Instructions", "com_ui_instructions": "Instructions",
"com_ui_latest_footer": "Every AI for Everyone.", "com_ui_latest_footer": "Every AI for Everyone.",

View file

@ -98,3 +98,21 @@ export const extractContent = (
} }
return ''; return '';
}; };
export const normalizeLayout = (layout: number[]) => {
const sum = layout.reduce((acc, size) => acc + size, 0);
if (Math.abs(sum - 100) < 0.01) {
return layout.map((size) => Number(size.toFixed(2)));
}
const factor = 100 / sum;
const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
const adjustedSum = normalizedLayout.reduce(
(acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
0,
);
normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
return normalizedLayout;
};

View file

@ -1,4 +1,3 @@
import { z } from 'zod'; import { z } from 'zod';
import type { ZodError } from 'zod'; import type { ZodError } from 'zod';
import type { TModelsConfig } from './types'; import type { TModelsConfig } from './types';
@ -144,6 +143,7 @@ export enum AgentCapabilities {
end_after_tools = 'end_after_tools', end_after_tools = 'end_after_tools',
execute_code = 'execute_code', execute_code = 'execute_code',
file_search = 'file_search', file_search = 'file_search',
artifacts = 'artifacts',
actions = 'actions', actions = 'actions',
tools = 'tools', tools = 'tools',
} }
@ -217,6 +217,7 @@ export const agentsEndpointSChema = baseEndpointSchema.merge(
.default([ .default([
AgentCapabilities.execute_code, AgentCapabilities.execute_code,
AgentCapabilities.file_search, AgentCapabilities.file_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions, AgentCapabilities.actions,
AgentCapabilities.tools, AgentCapabilities.tools,
]), ]),

View file

@ -155,6 +155,7 @@ export const defaultAgentFormValues = {
tools: [], tools: [],
provider: {}, provider: {},
projectIds: [], projectIds: [],
artifacts: '',
isCollaborative: false, isCollaborative: false,
[Tools.execute_code]: false, [Tools.execute_code]: false,
[Tools.file_search]: false, [Tools.file_search]: false,

View file

@ -3,6 +3,7 @@ import type { AssistantsEndpoint, AgentProvider } from 'src/schemas';
import type { ContentTypes } from './runs'; import type { ContentTypes } from './runs';
import type { Agents } from './agents'; import type { Agents } from './agents';
import type { TFile } from './files'; import type { TFile } from './files';
import { ArtifactModes } from 'src/artifacts';
export type Schema = OpenAPIV3.SchemaObject & { description?: string }; export type Schema = OpenAPIV3.SchemaObject & { description?: string };
export type Reference = OpenAPIV3.ReferenceObject & { description?: string }; export type Reference = OpenAPIV3.ReferenceObject & { description?: string };
@ -204,6 +205,7 @@ export type Agent = {
created_at: number; created_at: number;
avatar: AgentAvatar | null; avatar: AgentAvatar | null;
instructions: string | null; instructions: string | null;
additional_instructions?: string | null;
tools?: string[]; tools?: string[];
projectIds?: string[]; projectIds?: string[];
tool_kwargs?: Record<string, unknown>; tool_kwargs?: Record<string, unknown>;
@ -217,6 +219,7 @@ export type Agent = {
agent_ids?: string[]; agent_ids?: string[];
end_after_tools?: boolean; end_after_tools?: boolean;
hide_sequential_outputs?: boolean; hide_sequential_outputs?: boolean;
artifacts?: ArtifactModes;
}; };
export type TAgentsMap = Record<string, Agent | undefined>; export type TAgentsMap = Record<string, Agent | undefined>;
@ -231,7 +234,7 @@ export type AgentCreateParams = {
provider: AgentProvider; provider: AgentProvider;
model: string | null; model: string | null;
model_parameters: AgentModelParameters; model_parameters: AgentModelParameters;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs'>; } & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts'>;
export type AgentUpdateParams = { export type AgentUpdateParams = {
name?: string | null; name?: string | null;
@ -247,7 +250,7 @@ export type AgentUpdateParams = {
projectIds?: string[]; projectIds?: string[];
removeProjectIds?: string[]; removeProjectIds?: string[];
isCollaborative?: boolean; isCollaborative?: boolean;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs'>; } & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts'>;
export type AgentListParams = { export type AgentListParams = {
limit?: number; limit?: number;