Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions client/src/Providers/BadgeRowContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface BadgeRowContextType {
agentsConfig?: TAgentsEndpoint | null;
skills: ReturnType<typeof useToolToggle>;
webSearch: ReturnType<typeof useToolToggle>;
imageGen: ReturnType<typeof useToolToggle>;
artifacts: ReturnType<typeof useToolToggle>;
fileSearch: ReturnType<typeof useToolToggle>;
codeInterpreter: ReturnType<typeof useToolToggle>;
Expand Down Expand Up @@ -98,12 +99,14 @@ export default function BadgeRowProvider({
const codeToggleKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${storageSuffix}`;
const webSearchToggleKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${storageSuffix}`;
const fileSearchToggleKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${storageSuffix}`;
const imageGenToggleKey = `${LocalStorageKeys.LAST_IMAGE_GEN_TOGGLE_}${storageSuffix}`;
const artifactsToggleKey = `${LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_}${storageSuffix}`;
const skillsToggleKey = `${LocalStorageKeys.LAST_SKILLS_TOGGLE_}${storageSuffix}`;

const codeToggleValue = getTimestampedValue(codeToggleKey);
const webSearchToggleValue = getTimestampedValue(webSearchToggleKey);
const fileSearchToggleValue = getTimestampedValue(fileSearchToggleKey);
const imageGenToggleValue = getTimestampedValue(imageGenToggleKey);
const artifactsToggleValue = getTimestampedValue(artifactsToggleKey);
const skillsToggleValue = getTimestampedValue(skillsToggleKey);

Expand Down Expand Up @@ -133,6 +136,14 @@ export default function BadgeRowProvider({
}
}

if (imageGenToggleValue !== null) {
try {
initialValues[AgentCapabilities.image_gen] = JSON.parse(imageGenToggleValue);
} catch (e) {
console.error('Failed to parse image gen toggle value:', e);
}
}

if (artifactsToggleValue !== null) {
try {
initialValues[AgentCapabilities.artifacts] = JSON.parse(artifactsToggleValue);
Expand Down Expand Up @@ -232,6 +243,15 @@ export default function BadgeRowProvider({
isAuthenticated: true,
});

/** ImageGen hook — toggle maps to gemini_image_gen tool server-side */
const imageGen = useToolToggle({
conversationId,
storageContextKey,
toolKey: AgentCapabilities.image_gen,
localStorageKey: LocalStorageKeys.LAST_IMAGE_GEN_TOGGLE_,
isAuthenticated: true,
});

/** Artifacts hook - using a custom key since it's not a Tool but a capability */
const artifacts = useToolToggle({
conversationId,
Expand All @@ -255,6 +275,7 @@ export default function BadgeRowProvider({
const value: BadgeRowContextType = {
skills,
webSearch,
imageGen,
artifacts,
fileSearch,
agentsConfig,
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/Chat/Input/BadgeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useChatBadges } from '~/hooks';
import ToolDialogs from './ToolDialogs';
import FileSearch from './FileSearch';
import Artifacts from './Artifacts';
import ImageGen from './ImageGen';
import MCPSelect from './MCPSelect';
import WebSearch from './WebSearch';
import Skills from './Skills';
Expand Down Expand Up @@ -374,6 +375,7 @@ function BadgeRow({
<WebSearch />
<CodeInterpreter />
<FileSearch />
<ImageGen />
<Skills />
<Artifacts />
<MCPSelect />
Expand Down
26 changes: 26 additions & 0 deletions client/src/components/Chat/Input/ImageGen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { memo } from 'react';
import { Image } from 'lucide-react';
import { CheckboxButton } from '@librechat/client';
import { useLocalize } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';

function ImageGen() {
const localize = useLocalize();
const context = useBadgeRowContext();
const { toggleState: imageGen, debouncedChange, isPinned } = context?.imageGen ?? {};

return (
(imageGen || isPinned) && (
<CheckboxButton
className="max-w-fit"
checked={imageGen}
setValue={debouncedChange}
label={localize('com_ui_image_gen')}
isCheckedClassName="border-pink-600/40 bg-pink-500/10 hover:bg-pink-700/10"
icon={<Image className="icon-md" aria-hidden="true" />}
/>
)
);
}

export default memo(ImageGen);
58 changes: 55 additions & 3 deletions client/src/components/Chat/Input/ToolsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React, { useState, useMemo, useCallback } from 'react';
import * as Ariakit from '@ariakit/react';
import { TooltipAnchor, DropdownPopup, PinIcon, VectorIcon } from '@librechat/client';
import { Globe, ScrollText, Settings, Settings2, TerminalSquareIcon } from 'lucide-react';
import {

Check failure on line 4 in client/src/components/Chat/Input/ToolsDropdown.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint Linting

Replace `⏎··Globe,⏎··Image,⏎··ScrollText,⏎··Settings,⏎··Settings2,⏎··TerminalSquareIcon,⏎` with `·Globe,·Image,·ScrollText,·Settings,·Settings2,·TerminalSquareIcon·`
Globe,
Image,
ScrollText,
Settings,
Settings2,
TerminalSquareIcon,
} from 'lucide-react';
import type { MenuItemProps } from '~/common';
import {
AuthType,
Expand All @@ -26,8 +33,14 @@
const context = useBadgeRowContext();
const { data: startupConfig } = useGetStartupConfig();

const { codeEnabled, webSearchEnabled, artifactsEnabled, fileSearchEnabled, skillsEnabled } =
useAgentCapabilities(context?.agentsConfig?.capabilities ?? defaultAgentCapabilities);
const {
codeEnabled,
webSearchEnabled,
imageGenEnabled,
artifactsEnabled,
fileSearchEnabled,
skillsEnabled,
} = useAgentCapabilities(context?.agentsConfig?.capabilities ?? defaultAgentCapabilities);

const canUseWebSearch = useHasAccess({
permissionType: PermissionTypes.WEB_SEARCH,
Expand Down Expand Up @@ -59,6 +72,7 @@
const {
skills,
webSearch,
imageGen,
artifacts,
fileSearch,
mcpServerManager,
Expand All @@ -74,6 +88,7 @@
authData: webSearchAuthData,
} = webSearch ?? {};
const { isPinned: isCodePinned, setIsPinned: setIsCodePinned } = codeInterpreter ?? {};
const { isPinned: isImageGenPinned, setIsPinned: setIsImageGenPinned } = imageGen ?? {};
const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch ?? {};
const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts ?? {};
const { isPinned: isSkillsPinned, setIsPinned: setIsSkillsPinned } = skills ?? {};
Expand All @@ -99,6 +114,11 @@
fileSearch?.debouncedChange({ value: newValue });
}, [fileSearch]);

const handleImageGenToggle = useCallback(() => {
const newValue = !imageGen?.toggleState;
imageGen?.debouncedChange({ value: newValue });
}, [imageGen]);

const handleArtifactsToggle = useCallback(() => {
const currentState = artifacts?.toggleState;
if (!currentState || currentState === '') {
Expand Down Expand Up @@ -221,6 +241,38 @@
});
}

if (imageGenEnabled) {
dropdownItems.push({
onClick: handleImageGenToggle,
hideOnClick: false,
render: (props) => (
<div {...props}>
<div className="flex items-center gap-2">
<Image className="icon-md" aria-hidden="true" />
<span>{localize('com_ui_image_gen')}</span>
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsImageGenPinned?.(!isImageGenPinned);
}}
className={cn(
'rounded p-1 transition-all duration-200',
'hover:bg-surface-secondary hover:shadow-sm',
!isImageGenPinned && 'text-text-secondary hover:text-text-primary',
)}
aria-label={isImageGenPinned ? localize('com_ui_unpin') : localize('com_ui_pin')}
>
<div className="h-4 w-4">
<PinIcon unpin={isImageGenPinned} />
</div>
</button>
</div>
),
});
}

if (canUseSkills && skillsEnabled) {
dropdownItems.push({
onClick: handleSkillsToggle,
Expand Down
7 changes: 7 additions & 0 deletions client/src/hooks/Agents/useAgentCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface AgentCapabilitiesResult {
contextEnabled: boolean;
fileSearchEnabled: boolean;
webSearchEnabled: boolean;
imageGenEnabled: boolean;
codeEnabled: boolean;
skillsEnabled: boolean;
deferredToolsEnabled: boolean;
Expand Down Expand Up @@ -53,6 +54,11 @@ export default function useAgentCapabilities(
[capabilities],
);

const imageGenEnabled = useMemo(
() => capabilities?.includes(AgentCapabilities.image_gen) ?? false,
[capabilities],
);

const codeEnabled = useMemo(
() => capabilities?.includes(AgentCapabilities.execute_code) ?? false,
[capabilities],
Expand Down Expand Up @@ -82,6 +88,7 @@ export default function useAgentCapabilities(
contextEnabled,
artifactsEnabled,
webSearchEnabled,
imageGenEnabled,
fileSearchEnabled,
deferredToolsEnabled,
programmaticToolsEnabled,
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/agents/added.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export async function loadAddedAgent(
execute_code?: boolean;
file_search?: boolean;
web_search?: boolean;
image_gen?: boolean;
artifacts?: unknown;
};
[key: string]: unknown;
Expand Down Expand Up @@ -121,6 +122,7 @@ export async function loadAddedAgent(
execute_code?: boolean;
file_search?: boolean;
web_search?: boolean;
image_gen?: boolean;
artifacts?: unknown;
}
| undefined;
Expand Down Expand Up @@ -159,6 +161,9 @@ export async function loadAddedAgent(
if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
tools.push(Tools.web_search);
}
if (ephemeralAgent?.image_gen === true) {
tools.push('gemini_image_gen');
}

const addedServers = new Set<string>();
for (const mcpServer of mcpServers) {
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/agents/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export async function loadEphemeralAgent(
if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
tools.push(Tools.web_search);
}
if (ephemeralAgent?.image_gen === true) {
tools.push('gemini_image_gen');
}

const addedServers = new Set<string>();
if (mcpServers.size > 0) {
Expand Down
4 changes: 4 additions & 0 deletions packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ export enum AgentCapabilities {
execute_code = 'execute_code',
file_search = 'file_search',
web_search = 'web_search',
image_gen = 'image_gen',
artifacts = 'artifacts',
subagents = 'subagents',
actions = 'actions',
Expand Down Expand Up @@ -442,6 +443,7 @@ export const defaultAgentCapabilities = [
AgentCapabilities.execute_code,
AgentCapabilities.file_search,
AgentCapabilities.web_search,
AgentCapabilities.image_gen,
AgentCapabilities.artifacts,
AgentCapabilities.subagents,
AgentCapabilities.actions,
Expand Down Expand Up @@ -2201,6 +2203,8 @@ export enum LocalStorageKeys {
LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_',
/** Last checked toggle for File Search per conversation ID */
LAST_FILE_SEARCH_TOGGLE_ = 'LAST_FILE_SEARCH_TOGGLE_',
/** Last checked toggle for Image Generation per conversation ID */
LAST_IMAGE_GEN_TOGGLE_ = 'LAST_IMAGE_GEN_TOGGLE_',
/** Last checked toggle for Artifacts per conversation ID */
LAST_ARTIFACTS_TOGGLE_ = 'LAST_ARTIFACTS_TOGGLE_',
/** Last checked toggle for Skills per conversation ID */
Expand Down
1 change: 1 addition & 0 deletions packages/data-provider/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type TEphemeralAgent = {
web_search?: boolean;
file_search?: boolean;
execute_code?: boolean;
image_gen?: boolean;
artifacts?: string;
skills?: boolean;
};
Expand Down
Loading