mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
Merge 3f10a4f9d2 into 8ed0bcf5ca
This commit is contained in:
commit
eb2f461a33
2 changed files with 113 additions and 140 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue