This commit is contained in:
Marco Beretta 2026-04-04 17:50:55 +02:00 committed by GitHub
commit eb2f461a33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 113 additions and 140 deletions

View file

@ -1,5 +1,14 @@
import { useState } from 'react';
import { Button, OGDialog, OGDialogTemplate } from '@librechat/client';
import { Globe } from 'lucide-react';
import {
Button,
OGDialog,
OGDialogClose,
OGDialogTitle,
OGDialogFooter,
OGDialogHeader,
OGDialogContent,
} from '@librechat/client';
import {
AuthType,
RerankerTypes,
@ -183,84 +192,86 @@ export default function ApiKeyDialog({
triggerRef={triggerRef}
triggerRefs={triggerRefs}
>
<OGDialogTemplate
className="w-11/12 sm:w-[500px]"
title=""
main={
<>
<div className="mb-4 text-center font-medium">{localize('com_ui_web_search')}</div>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Provider Section */}
{providerAuthType !== AuthType.SYSTEM_DEFINED && (
<InputSection
title={localize('com_ui_web_search_provider')}
selectedKey={selectedProvider}
onSelectionChange={handleProviderChange}
dropdownOptions={providerOptions}
showDropdown={!config?.webSearch?.searchProvider}
register={register}
dropdownOpen={dropdownOpen.provider}
setDropdownOpen={(open) =>
setDropdownOpen((prev) => ({ ...prev, provider: open }))
}
dropdownKey="provider"
/>
)}
<OGDialogContent
showCloseButton={false}
className="w-11/12 max-w-lg border-none bg-surface-primary"
>
<OGDialogHeader className="gap-2 py-2">
<div className="flex items-center justify-center gap-2">
<div className="flex size-10 items-center justify-center rounded-full bg-blue-500/10">
<Globe className="size-5 text-blue-500" aria-hidden="true" />
</div>
</div>
<OGDialogTitle className="text-center text-lg font-semibold">
{localize('com_ui_web_search')}
</OGDialogTitle>
</OGDialogHeader>
{/* Scraper Section */}
{scraperAuthType !== AuthType.SYSTEM_DEFINED && (
<InputSection
title={localize('com_ui_web_search_scraper')}
selectedKey={selectedScraper}
onSelectionChange={handleScraperChange}
dropdownOptions={scraperOptions}
showDropdown={!config?.webSearch?.scraperProvider}
register={register}
dropdownOpen={dropdownOpen.scraper}
setDropdownOpen={(open) =>
setDropdownOpen((prev) => ({ ...prev, scraper: open }))
}
dropdownKey="scraper"
/>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{providerAuthType !== AuthType.SYSTEM_DEFINED && (
<InputSection
title={localize('com_ui_web_search_provider')}
selectedKey={selectedProvider}
onSelectionChange={handleProviderChange}
dropdownOptions={providerOptions}
showDropdown={!config?.webSearch?.searchProvider}
register={register}
dropdownOpen={dropdownOpen.provider}
setDropdownOpen={(open) => setDropdownOpen((prev) => ({ ...prev, provider: open }))}
dropdownKey="provider"
/>
)}
{/* Reranker Section */}
{rerankerAuthType !== AuthType.SYSTEM_DEFINED && (
<InputSection
title={localize('com_ui_web_search_reranker')}
selectedKey={selectedReranker}
onSelectionChange={handleRerankerChange}
dropdownOptions={rerankerOptions}
showDropdown={!config?.webSearch?.rerankerType}
register={register}
dropdownOpen={dropdownOpen.reranker}
setDropdownOpen={(open) =>
setDropdownOpen((prev) => ({ ...prev, reranker: open }))
}
dropdownKey="reranker"
/>
)}
</form>
</>
}
selection={{
selectHandler: handleSubmit(onSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectText: localize('com_ui_save'),
}}
buttons={
isToolAuthenticated && (
{scraperAuthType !== AuthType.SYSTEM_DEFINED && (
<InputSection
title={localize('com_ui_web_search_scraper')}
selectedKey={selectedScraper}
onSelectionChange={handleScraperChange}
dropdownOptions={scraperOptions}
showDropdown={!config?.webSearch?.scraperProvider}
register={register}
dropdownOpen={dropdownOpen.scraper}
setDropdownOpen={(open) => setDropdownOpen((prev) => ({ ...prev, scraper: open }))}
dropdownKey="scraper"
/>
)}
{rerankerAuthType !== AuthType.SYSTEM_DEFINED && (
<InputSection
title={localize('com_ui_web_search_reranker')}
selectedKey={selectedReranker}
onSelectionChange={handleRerankerChange}
dropdownOptions={rerankerOptions}
showDropdown={!config?.webSearch?.rerankerType}
register={register}
dropdownOpen={dropdownOpen.reranker}
setDropdownOpen={(open) => setDropdownOpen((prev) => ({ ...prev, reranker: open }))}
dropdownKey="reranker"
/>
)}
</form>
<OGDialogFooter>
<OGDialogClose asChild>
<Button variant="outline" className="h-10">
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
{isToolAuthenticated && (
<Button
variant="destructive"
onClick={onRevoke}
className="bg-red-500 text-white hover:bg-red-600"
className="h-10"
aria-label={localize('com_ui_revoke')}
>
{localize('com_ui_revoke')}
</Button>
)
}
showCancelButton={true}
/>
)}
<Button variant="submit" onClick={handleSubmit(onSubmit)} className="h-10">
{localize('com_ui_save')}
</Button>
</OGDialogFooter>
</OGDialogContent>
</OGDialog>
);
}

View file

@ -1,11 +1,9 @@
import { useState } from 'react';
import * as Menu from '@ariakit/react/menu';
import { ChevronDown, Eye, EyeOff } from 'lucide-react';
import { Input, Label, DropdownPopup } from '@librechat/client';
import { ChevronDown } from 'lucide-react';
import { Input, Label, SecretInput, DropdownPopup } from '@librechat/client';
import type { SearchApiKeyFormData } from '~/hooks/Plugins/useAuthSearchTool';
import type { UseFormRegister } from 'react-hook-form';
import type { MenuItemProps } from '~/common';
import { useLocalize } from '~/hooks';
interface InputConfig {
placeholder: string;
@ -45,25 +43,16 @@ export default function InputSection({
setDropdownOpen,
dropdownKey,
}: InputSectionProps) {
const localize = useLocalize();
const [passwordVisibility, setPasswordVisibility] = useState<Record<string, boolean>>({});
const selectedOption = dropdownOptions.find((opt) => opt.key === selectedKey);
const dropdownItems: MenuItemProps[] = dropdownOptions.map((option) => ({
label: option.label,
onClick: () => onSelectionChange(option.key),
}));
const togglePasswordVisibility = (fieldName: string) => {
setPasswordVisibility((prev) => ({
...prev,
[fieldName]: !prev[fieldName],
}));
};
return (
<div className="mb-6">
<div className="mb-2 flex items-center justify-between">
<Label className="text-md w-fit font-medium">{title}</Label>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">{title}</Label>
{showDropdown ? (
<DropdownPopup
menuId={`${dropdownKey}-dropdown`}
@ -73,75 +62,48 @@ export default function InputSection({
trigger={
<Menu.MenuButton
onClick={() => setDropdownOpen(!dropdownOpen)}
className="flex items-center rounded-md border border-border-light px-3 py-1 text-sm text-text-secondary"
className="flex items-center gap-1.5 whitespace-nowrap rounded-lg border border-border-light px-3 py-1.5 text-sm text-text-secondary transition-colors hover:bg-surface-hover"
>
{selectedOption?.label}
<ChevronDown className="ml-1 h-4 w-4" />
<ChevronDown className="size-4" aria-hidden="true" />
</Menu.MenuButton>
}
/>
) : (
<div className="text-sm text-text-secondary">{selectedOption?.label}</div>
<span className="text-sm text-text-secondary">{selectedOption?.label}</span>
)}
</div>
{selectedOption?.inputs &&
Object.entries(selectedOption.inputs).map(([name, config], index) => (
<div key={name}>
<div className="relative">
<Input
type={'text'} // so password autofill doesn't show
placeholder={config.placeholder}
autoComplete={config.type === 'password' ? 'one-time-code' : 'off'}
readOnly={config.type === 'password'}
onFocus={
config.type === 'password' ? (e) => (e.target.readOnly = false) : undefined
}
className={`${index > 0 ? 'mb-2' : 'mb-2'} ${
config.type === 'password' ? 'pr-10' : ''
}`}
{...register(name as keyof SearchApiKeyFormData)}
/>
{config.type === 'password' && (
<button
type="button"
onClick={() => togglePasswordVisibility(name)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary transition-colors hover:text-text-primary"
aria-label={
passwordVisibility[name]
? localize('com_ui_hide_password')
: localize('com_ui_show_password')
}
>
<div className="relative h-4 w-4">
{passwordVisibility[name] ? (
<EyeOff
className="absolute inset-0 h-4 w-4 duration-200 animate-in fade-in"
aria-hidden="true"
/>
) : (
<Eye
className="absolute inset-0 h-4 w-4 duration-200 animate-in fade-in"
aria-hidden="true"
/>
)}
</div>
</button>
{selectedOption?.inputs && (
<div className="space-y-2">
{Object.entries(selectedOption.inputs).map(([name, config]) => (
<div key={name} className="space-y-1">
{config.type === 'password' ? (
<SecretInput
placeholder={config.placeholder}
{...register(name as keyof SearchApiKeyFormData)}
/>
) : (
<Input
type="text"
placeholder={config.placeholder}
autoComplete="off"
{...register(name as keyof SearchApiKeyFormData)}
/>
)}
</div>
{config.link && (
<div className="mt-1 text-xs text-text-secondary">
{config.link && (
<a
href={config.link.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
className="inline-block text-xs text-blue-500 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
>
{config.link.text}
</a>
</div>
)}
</div>
))}
)}
</div>
))}
</div>
)}
</div>
);
}