🪄 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

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

View file

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

View file

@ -55,8 +55,8 @@ export default function AgentSelect({
};
const capabilities: TAgentCapabilities = {
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false,
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.end_after_tools]: 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>
)}
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<HoverCardPortal>

View file

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