mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 11:50:14 +01:00
✨feat: OAuth for Actions (#5693)
* ✨feat: OAuth for Actions * WIP: PoC flow state manager * refactor: Add identifier field to token model from action schema * chore: fix potential file type issues * ci: fix type issue with action metadata auth * fix: ensure FlowManagerOptions has a default ttl value * WIP: OAUTH actions * WIP: first pass OAuth Action * fix: standardize identifier usage in OAuth flow handling * fix: update token retrieval to include userId in query and use correct identifier * refacotr: update token retrieval to use userId for OAuth token query * feat: Tool Call Auth styling * fix: streamline token creation and add type field to token schema * refactor: cleanup OAuth flow by encrypting client credentials and ensuring oauth operations only run under condition * refactor: use encrypted credentials in OAuth callback * fix: update Token collection indexes to use expiresAt TTL index and not createdAt legacy index * refactor: enhance Token index cleanup by improving logging and removing redundant index creation logic * refactor: remove unused OAuth login route and related logic for improved clarity * refactor: replace fetch with axios for OAuth token exchange and improve error handling * refactor: better UX after authentication before oauth tool execution * refactor: implement cleanup handlers for FlowStateManager intervals to enhance resource management * refactor: encrypt OAuth tokens before storing and decrypt upon retrieval for enhanced security * refactor: enhance authentication success page with improved styling and countdown feature * refactor: add response_type parameter to OAuth redirect URI for improved compatibility * chore: update translation.json new localizations * chore: remove unused OGDialog import from OGDialogTemplate component * refactor: Actions Auth using new Dialog styling, use same component with Agents/Assistants * refactor: update removeNullishValues function to support removal of empty strings and adjust transform usage in schemas * chore: bump version of librechat-data-provider to 0.7.6991 * refactor: integrate removeNullishValues function to clean metadata before encryption in agent and assistant routes * refactor: update OAuth input fields to use 'password' type for better security * refactor: update localization placeholders for sign-in message to use double curly braces * refactor: add access_type parameter for offline access in createActionTool function * refactor: implement handleOAuthToken function for token management and encryption * feat: refresh token support * refactor: add default expiration for access token and error handling for missing token * feat: localizations for ActionAuth * refactor: set refresh token expiration to null to not expire if expiry never given * fix: prevent crash fromerror within async handleAbortError in AskController, EditController, and AgentController * feat: Action Callback URL * 🌍 i18n: Update translation.json with latest translations * refactor: handle errors in flow state checking to prevent unhandled promise rejections * fix: improve flow state concurrency to prevent multiple token creation calls * refactor: RequestExecutor to use separate axios instance * refactor: improve concurrency flows by keeping completed state until TTL expiry * refactor: increase TTL for flow state management and adjust monitoring interval * ci: mock axios instance creation in actions spec * feat: add Babel and Jest configuration files; implement FlowStateManager tests with concurrency handling * chore: add disableOAuth prop to ActionsAuth (not implemented for Assistants yet) --------- Co-authored-by: Danny Avila <danny@librechat.ai> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
71c30a3640
commit
d99a9db3f6
58 changed files with 2146 additions and 1223 deletions
|
|
@ -1,296 +0,0 @@
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import { DialogContent } from '~/components/ui/';
|
||||
|
||||
export default function ActionsAuth({
|
||||
setOpenAuthDialog,
|
||||
}: {
|
||||
setOpenAuthDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { watch, setValue, trigger } = useFormContext();
|
||||
const type = watch('type');
|
||||
return (
|
||||
<DialogContent
|
||||
role="dialog"
|
||||
id="radix-:rf5:"
|
||||
aria-describedby="radix-:rf7:"
|
||||
aria-labelledby="radix-:rf6:"
|
||||
data-state="open"
|
||||
className="left-1/2 col-auto col-start-2 row-auto row-start-2 w-full max-w-md -translate-x-1/2 rounded-xl bg-white pb-0 text-left shadow-xl transition-all dark:bg-gray-700 dark:text-gray-100"
|
||||
tabIndex={-1}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
|
||||
<div className="flex">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow flex-col gap-1">
|
||||
<h2
|
||||
id="radix-:rf6:"
|
||||
className="text-token-text-primary text-lg font-medium leading-6"
|
||||
>
|
||||
Authentication
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 sm:p-6 sm:pt-0">
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-sm font-medium">Authentication Type</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthTypeEnum.None}
|
||||
onValueChange={(value) => setValue('type', value)}
|
||||
value={type}
|
||||
role="radiogroup"
|
||||
aria-required="false"
|
||||
dir="ltr"
|
||||
className="flex gap-4"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rf8:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.None}
|
||||
id=":rf8:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rfa:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.ServiceHttp}
|
||||
id=":rfa:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
API Key
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<label htmlFor=":rfc:" className="flex cursor-not-allowed items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
disabled={true}
|
||||
value={AuthTypeEnum.OAuth}
|
||||
id=":rfc:"
|
||||
className="mr-1 flex h-5 w-5 cursor-not-allowed items-center justify-center rounded-full border border-gray-500 bg-gray-300 dark:border-gray-600 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
OAuth
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
{type === 'none' ? null : type === 'service_http' ? <ApiKey /> : <OAuth />}
|
||||
{/* Cancel/Save */}
|
||||
<div className="mt-5 flex flex-col gap-3 sm:mt-4 sm:flex-row-reverse">
|
||||
<button
|
||||
className="btn relative bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
|
||||
onClick={async () => {
|
||||
const result = await trigger(undefined, { shouldFocus: true });
|
||||
setValue('saved_auth_fields', result);
|
||||
setOpenAuthDialog(!result);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">Save</div>
|
||||
</button>
|
||||
<DialogPrimitive.Close className="btn btn-neutral relative">
|
||||
<div className="flex w-full items-center justify-center gap-2">Cancel</div>
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const ApiKey = () => {
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const authorization_type = watch('authorization_type');
|
||||
const type = watch('type');
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">API Key</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Auth Type</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
onValueChange={(value) => setValue('authorization_type', value)}
|
||||
value={authorization_type}
|
||||
role="radiogroup"
|
||||
aria-required="true"
|
||||
dir="ltr"
|
||||
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rfu:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Basic}
|
||||
id=":rfu:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Basic
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rg0:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Bearer}
|
||||
id=":rg0:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Bearer
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rg2:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Custom}
|
||||
id=":rg2:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Custom
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
{authorization_type === AuthorizationTypeEnum.Custom && (
|
||||
<div className="mt-2">
|
||||
<label className="mb-1 block text-sm font-medium">Custom Header Name</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
placeholder="X-Api-Key"
|
||||
{...register('custom_auth_header', {
|
||||
required: authorization_type === AuthorizationTypeEnum.Custom,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OAuth = () => {
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const token_exchange_method = watch('token_exchange_method');
|
||||
const type = watch('type');
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">Client ID</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('oauth_client_id', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Client Secret</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('oauth_client_secret', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Authorization URL</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('authorization_url', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Token URL</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('client_url', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Scope</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('scope', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Token Exchange Method</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
onValueChange={(value) => setValue('token_exchange_method', value)}
|
||||
value={token_exchange_method}
|
||||
role="radiogroup"
|
||||
aria-required="true"
|
||||
dir="ltr"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rj1:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={TokenExchangeMethodEnum.DefaultPost}
|
||||
id=":rj1:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Default (POST request)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rj3:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={TokenExchangeMethodEnum.BasicAuthHeader}
|
||||
id=":rj3:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
Basic authorization header
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
} from 'librechat-data-provider';
|
||||
import type { ActionAuthForm } from '~/common';
|
||||
import type { Spec } from './ActionsTable';
|
||||
import ActionCallback from '~/components/SidePanel/Builder/ActionCallback';
|
||||
import { ActionsTable, columns } from './ActionsTable';
|
||||
import { useUpdateAgentAction } from '~/data-provider';
|
||||
import { useToastContext } from '~/Providers';
|
||||
|
|
@ -248,8 +249,8 @@ export default function ActionsInput({
|
|||
</div>
|
||||
</div>
|
||||
{!!data && (
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<div className="my-2">
|
||||
<div className="flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_available_actions')}
|
||||
</label>
|
||||
|
|
@ -258,6 +259,7 @@ export default function ActionsInput({
|
|||
</div>
|
||||
)}
|
||||
<div className="relative my-1">
|
||||
<ActionCallback action_id={action?.action_id} />
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_privacy_policy_url')}
|
||||
|
|
@ -267,7 +269,7 @@ export default function ActionsInput({
|
|||
<input
|
||||
type="text"
|
||||
placeholder="https://api.example-weather-app.com/privacy"
|
||||
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-border-light"
|
||||
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
|
|
@ -7,14 +7,14 @@ import {
|
|||
} from 'librechat-data-provider';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import type { AgentPanelProps, ActionAuthForm } from '~/common';
|
||||
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useDeleteAgentAction } from '~/data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import ActionsInput from './ActionsInput';
|
||||
import ActionsAuth from './ActionsAuth';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function ActionsPanel({
|
||||
|
|
@ -26,8 +26,6 @@ export default function ActionsPanel({
|
|||
}: AgentPanelProps) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
const [openAuthDialog, setOpenAuthDialog] = useState(false);
|
||||
const deleteAgentAction = useDeleteAgentAction({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
|
|
@ -65,7 +63,6 @@ export default function ActionsPanel({
|
|||
});
|
||||
|
||||
const { reset, watch } = methods;
|
||||
const type = watch('type');
|
||||
|
||||
useEffect(() => {
|
||||
if (action?.metadata.auth) {
|
||||
|
|
@ -156,40 +153,7 @@ export default function ActionsPanel({
|
|||
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
|
||||
</div> */}
|
||||
</div>
|
||||
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<div className="relative mb-4">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_authentication')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="border-token-border-medium flex rounded-lg border text-sm hover:cursor-pointer">
|
||||
<div className="h-9 grow px-3 py-2">{type}</div>
|
||||
<div className="bg-token-border-medium w-px"></div>
|
||||
<button type="button" color="neutral" className="flex items-center gap-2 px-3">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-sm"
|
||||
>
|
||||
<path
|
||||
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
|
||||
</Dialog>
|
||||
<ActionsAuth />
|
||||
<ActionsInput action={action} agent_id={agent_id} setAction={setAction} />
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
className="border-token-border-light text-token-text-tertiary border-b text-left text-xs"
|
||||
>
|
||||
{headerGroup.headers.map((header, j) => (
|
||||
<th key={j} className="py-1 font-normal">
|
||||
<th key={j} className="py-1 font-normal text-text-secondary-alt">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
|
|
|
|||
63
client/src/components/SidePanel/Builder/ActionCallback.tsx
Normal file
63
client/src/components/SidePanel/Builder/ActionCallback.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { useState } from 'react';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { AuthTypeEnum } from 'librechat-data-provider';
|
||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { Button } from '~/components/ui';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function ActionCallback({ action_id }: { action_id?: string }) {
|
||||
const localize = useLocalize();
|
||||
const { watch } = useFormContext();
|
||||
const { showToast } = useToastContext();
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
const callbackURL = `${window.location.protocol}//${window.location.host}/api/actions/${action_id}/oauth/callback`;
|
||||
const copyLink = useCopyToClipboard({ text: callbackURL });
|
||||
|
||||
if (!action_id) {
|
||||
return null;
|
||||
}
|
||||
const type = watch('type');
|
||||
if (type !== AuthTypeEnum.OAuth) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="mb-1.5 flex flex-col space-y-2">
|
||||
<label className="font-semibold">{localize('com_ui_callback_url')}</label>
|
||||
<div className="relative flex items-center">
|
||||
<div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover flex h-10 w-full rounded-lg border">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={callbackURL}
|
||||
className="w-full border-0 bg-transparent px-3 py-2 pr-12 text-sm text-text-secondary-alt focus:outline-none"
|
||||
style={{ direction: 'rtl' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-0 flex h-full items-center pr-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isCopying) {
|
||||
return;
|
||||
}
|
||||
showToast({ message: localize('com_ui_copied_to_clipboard') });
|
||||
copyLink(setIsCopying);
|
||||
}}
|
||||
className={cn('h-8 rounded-md px-2', isCopying ? 'cursor-default' : '')}
|
||||
aria-label={localize('com_ui_copy_link')}
|
||||
>
|
||||
{isCopying ? <CopyCheck className="size-4" /> : <Copy className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,144 +1,190 @@
|
|||
import { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import { DialogContent } from '~/components/ui/';
|
||||
import {
|
||||
OGDialog,
|
||||
OGDialogClose,
|
||||
OGDialogTitle,
|
||||
OGDialogHeader,
|
||||
OGDialogContent,
|
||||
OGDialogTrigger,
|
||||
} from '~/components/ui';
|
||||
import { TranslationKeys, useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function ActionsAuth({
|
||||
setOpenAuthDialog,
|
||||
}: {
|
||||
setOpenAuthDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
export default function ActionsAuth({ disableOAuth }: { disableOAuth?: boolean }) {
|
||||
const localize = useLocalize();
|
||||
const [openAuthDialog, setOpenAuthDialog] = useState(false);
|
||||
const { watch, setValue, trigger } = useFormContext();
|
||||
const type = watch('type');
|
||||
|
||||
return (
|
||||
<DialogContent
|
||||
role="dialog"
|
||||
id="radix-:rf5:"
|
||||
aria-describedby="radix-:rf7:"
|
||||
aria-labelledby="radix-:rf6:"
|
||||
data-state="open"
|
||||
className="left-1/2 col-auto col-start-2 row-auto row-start-2 w-full max-w-md -translate-x-1/2 rounded-xl bg-white pb-0 text-left shadow-xl transition-all dark:bg-gray-700 dark:text-gray-100"
|
||||
tabIndex={-1}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
|
||||
<div className="flex">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow flex-col gap-1">
|
||||
<h2
|
||||
id="radix-:rf6:"
|
||||
className="text-token-text-primary text-lg font-medium leading-6"
|
||||
>
|
||||
Authentication
|
||||
</h2>
|
||||
<OGDialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
|
||||
<OGDialogTrigger asChild>
|
||||
<div className="relative mb-4">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_authentication')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="border-token-border-medium flex rounded-lg border text-sm hover:cursor-pointer">
|
||||
<div className="h-9 grow px-3 py-2">
|
||||
{localize(`com_ui_${type}` as TranslationKeys)}
|
||||
</div>
|
||||
<div className="bg-token-border-medium w-px"></div>
|
||||
<button type="button" color="neutral" className="flex items-center gap-2 px-3">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-sm"
|
||||
>
|
||||
<path
|
||||
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 sm:p-6 sm:pt-0">
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-sm font-medium">Authentication Type</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthTypeEnum.None}
|
||||
onValueChange={(value) => setValue('type', value)}
|
||||
value={type}
|
||||
role="radiogroup"
|
||||
aria-required="false"
|
||||
dir="ltr"
|
||||
className="flex gap-4"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rf8:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.None}
|
||||
id=":rf8:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-full max-w-md border-none bg-surface-primary text-text-primary">
|
||||
<OGDialogHeader className="border-b border-border-light sm:p-3">
|
||||
<OGDialogTitle>{localize('com_ui_authentication')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<div className="p-4 sm:p-6 sm:pt-0">
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_authentication_type')}
|
||||
</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthTypeEnum.None}
|
||||
onValueChange={(value) => setValue('type', value)}
|
||||
value={type}
|
||||
role="radiogroup"
|
||||
aria-required="false"
|
||||
dir="ltr"
|
||||
className="flex gap-4"
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rf8:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.None}
|
||||
id=":rf8:"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_none')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rfa:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.ServiceHttp}
|
||||
id=":rfa:"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_api_key')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor=":rfc:"
|
||||
className={cn(
|
||||
'flex items-center gap-1',
|
||||
disableOAuth === true ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor=":rfa:" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthTypeEnum.ServiceHttp}
|
||||
id=":rfa:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
API Key
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<label htmlFor=":rfc:" className="flex cursor-not-allowed items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
disabled={true}
|
||||
value={AuthTypeEnum.OAuth}
|
||||
id=":rfc:"
|
||||
className="mr-1 flex h-5 w-5 cursor-not-allowed items-center justify-center rounded-full border border-gray-500 bg-gray-300 dark:border-gray-600 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
</RadioGroup.Item>
|
||||
OAuth
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
disabled={disableOAuth}
|
||||
value={AuthTypeEnum.OAuth}
|
||||
id=":rfc:"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
disableOAuth === true ? 'cursor-not-allowed' : '',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_oauth')}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
{type === 'none' ? null : type === 'service_http' ? <ApiKey /> : <OAuth />}
|
||||
{/* Cancel/Save */}
|
||||
<div className="mt-5 flex flex-col gap-3 sm:mt-4 sm:flex-row-reverse">
|
||||
<button
|
||||
className="btn relative bg-surface-submit text-primary-foreground hover:bg-surface-submit-hover"
|
||||
onClick={async () => {
|
||||
const result = await trigger(undefined, { shouldFocus: true });
|
||||
setValue('saved_auth_fields', result);
|
||||
setOpenAuthDialog(!result);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-white">
|
||||
{localize('com_ui_save')}
|
||||
</div>
|
||||
</button>
|
||||
<OGDialogClose className="btn btn-neutral relative">
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
{localize('com_ui_cancel')}
|
||||
</div>
|
||||
</OGDialogClose>
|
||||
</div>
|
||||
</div>
|
||||
{type === 'none' ? null : type === 'service_http' ? <ApiKey /> : <OAuth />}
|
||||
{/* Cancel/Save */}
|
||||
<div className="mt-5 flex flex-col gap-3 sm:mt-4 sm:flex-row-reverse">
|
||||
<button
|
||||
className="btn relative bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
|
||||
onClick={async () => {
|
||||
const result = await trigger(undefined, { shouldFocus: true });
|
||||
setValue('saved_auth_fields', result);
|
||||
setOpenAuthDialog(!result);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">Save</div>
|
||||
</button>
|
||||
<DialogPrimitive.Close className="btn btn-neutral relative">
|
||||
<div className="flex w-full items-center justify-center gap-2">Cancel</div>
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
|
||||
const ApiKey = () => {
|
||||
const localize = useLocalize();
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const authorization_type = watch('authorization_type');
|
||||
const type = watch('type');
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">API Key</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key')}</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
type="new-password"
|
||||
autoComplete="new-password"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
className={cn(
|
||||
'mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm',
|
||||
'border-border-medium bg-surface-primary outline-none',
|
||||
'focus:ring-2 focus:ring-ring',
|
||||
)}
|
||||
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Auth Type</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_auth_type')}</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
onValueChange={(value) => setValue('authorization_type', value)}
|
||||
|
|
@ -147,7 +193,6 @@ const ApiKey = () => {
|
|||
aria-required="true"
|
||||
dir="ltr"
|
||||
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -157,12 +202,14 @@ const ApiKey = () => {
|
|||
role="radio"
|
||||
value={AuthorizationTypeEnum.Basic}
|
||||
id=":rfu:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
Basic
|
||||
{localize('com_ui_basic')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -172,12 +219,14 @@ const ApiKey = () => {
|
|||
role="radio"
|
||||
value={AuthorizationTypeEnum.Bearer}
|
||||
id=":rg0:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
Bearer
|
||||
{localize('com_ui_bearer')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -187,20 +236,28 @@ const ApiKey = () => {
|
|||
role="radio"
|
||||
value={AuthorizationTypeEnum.Custom}
|
||||
id=":rg2:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
Custom
|
||||
{localize('com_ui_custom')}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
{authorization_type === AuthorizationTypeEnum.Custom && (
|
||||
<div className="mt-2">
|
||||
<label className="mb-1 block text-sm font-medium">Custom Header Name</label>
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_custom_header_name')}
|
||||
</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
className={cn(
|
||||
'mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm',
|
||||
'border-border-medium bg-surface-primary outline-none',
|
||||
'focus:ring-2 focus:ring-ring',
|
||||
)}
|
||||
placeholder="X-Api-Key"
|
||||
{...register('custom_auth_header', {
|
||||
required: authorization_type === AuthorizationTypeEnum.Custom,
|
||||
|
|
@ -213,43 +270,53 @@ const ApiKey = () => {
|
|||
};
|
||||
|
||||
const OAuth = () => {
|
||||
const localize = useLocalize();
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const token_exchange_method = watch('token_exchange_method');
|
||||
const type = watch('type');
|
||||
|
||||
const inputClasses = cn(
|
||||
'mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm',
|
||||
'border-border-medium bg-surface-primary outline-none',
|
||||
'focus:ring-2 focus:ring-ring',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">Client ID</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_client_id')}</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('oauth_client_id', { required: type === AuthTypeEnum.OAuth })}
|
||||
autoComplete="new-password"
|
||||
className={inputClasses}
|
||||
{...register('oauth_client_id', { required: false })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Client Secret</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_client_secret')}</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
{...register('oauth_client_secret', { required: type === AuthTypeEnum.OAuth })}
|
||||
autoComplete="new-password"
|
||||
className={inputClasses}
|
||||
{...register('oauth_client_secret', { required: false })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Authorization URL</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_auth_url')}</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
className={inputClasses}
|
||||
{...register('authorization_url', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Token URL</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_token_url')}</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
className={inputClasses}
|
||||
{...register('client_url', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Scope</label>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_scope')}</label>
|
||||
<input
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
|
||||
className={inputClasses}
|
||||
{...register('scope', { required: type === AuthTypeEnum.OAuth })}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">Token Exchange Method</label>
|
||||
<label className="mb-1 block text-sm font-medium">
|
||||
{localize('com_ui_token_exchange_method')}
|
||||
</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
onValueChange={(value) => setValue('token_exchange_method', value)}
|
||||
|
|
@ -257,7 +324,6 @@ const OAuth = () => {
|
|||
role="radiogroup"
|
||||
aria-required="true"
|
||||
dir="ltr"
|
||||
tabIndex={0}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -267,12 +333,14 @@ const OAuth = () => {
|
|||
role="radio"
|
||||
value={TokenExchangeMethodEnum.DefaultPost}
|
||||
id=":rj1:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
Default (POST request)
|
||||
{localize('com_ui_default_post_request')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -282,12 +350,14 @@ const OAuth = () => {
|
|||
role="radio"
|
||||
value={TokenExchangeMethodEnum.BasicAuthHeader}
|
||||
id=":rj3:"
|
||||
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
Basic authorization header
|
||||
{localize('com_ui_basic_auth_header')}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
} from 'librechat-data-provider';
|
||||
import type { ActionAuthForm, ActionWithNullableMetadata } from '~/common';
|
||||
import type { Spec } from './ActionsTable';
|
||||
import ActionCallback from '~/components/SidePanel/Builder/ActionCallback';
|
||||
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
||||
import { ActionsTable, columns } from './ActionsTable';
|
||||
import { useUpdateAction } from '~/data-provider';
|
||||
|
|
@ -259,8 +260,8 @@ export default function ActionsInput({
|
|||
</div>
|
||||
</div>
|
||||
{!!data && (
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<div className="my-2">
|
||||
<div className="flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_available_actions')}
|
||||
</label>
|
||||
|
|
@ -269,6 +270,7 @@ export default function ActionsInput({
|
|||
</div>
|
||||
)}
|
||||
<div className="relative my-1">
|
||||
<ActionCallback action_id={action?.action_id} />
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_privacy_policy_url')}
|
||||
|
|
@ -278,7 +280,7 @@ export default function ActionsInput({
|
|||
<input
|
||||
type="text"
|
||||
placeholder="https://api.example-weather-app.com/privacy"
|
||||
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-border-light"
|
||||
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { ChevronLeft } from 'lucide-react';
|
||||
import type { AssistantPanelProps, ActionAuthForm } from '~/common';
|
||||
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
||||
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useDeleteAction } from '~/data-provider';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
|
|
@ -29,7 +29,6 @@ export default function ActionsPanel({
|
|||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const [openAuthDialog, setOpenAuthDialog] = useState(false);
|
||||
const deleteAction = useDeleteAction({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
|
|
@ -68,7 +67,6 @@ export default function ActionsPanel({
|
|||
});
|
||||
|
||||
const { reset, watch } = methods;
|
||||
const type = watch('type');
|
||||
|
||||
useEffect(() => {
|
||||
if (action?.metadata?.auth) {
|
||||
|
|
@ -162,40 +160,7 @@ export default function ActionsPanel({
|
|||
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
|
||||
</div> */}
|
||||
</div>
|
||||
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<div className="relative mb-4">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_ui_authentication')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="border-token-border-medium flex rounded-lg border text-sm hover:cursor-pointer">
|
||||
<div className="h-9 grow px-3 py-2">{type}</div>
|
||||
<div className="bg-token-border-medium w-px"></div>
|
||||
<button type="button" color="neutral" className="flex items-center gap-2 px-3">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-sm"
|
||||
>
|
||||
<path
|
||||
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
|
||||
</Dialog>
|
||||
<ActionsAuth disableOAuth={true} />
|
||||
<ActionsInput
|
||||
action={action}
|
||||
assistant_id={assistant_id}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import ImagePreview from '~/components/Chat/Input/Files/ImagePreview';
|
|||
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
|
||||
import { getFileType } from '~/utils';
|
||||
|
||||
export default function PanelFileCell({ row }: { row: Row<TFile> }) {
|
||||
export default function PanelFileCell({ row }: { row: Row<TFile | undefined> }) {
|
||||
const file = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{file.type.startsWith('image') ? (
|
||||
{file?.type.startsWith('image') === true ? (
|
||||
<ImagePreview
|
||||
url={file.filepath}
|
||||
className="h-10 w-10 flex-shrink-0"
|
||||
|
|
@ -17,11 +17,11 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
|
|||
alt={file.filename}
|
||||
/>
|
||||
) : (
|
||||
<FilePreview fileType={getFileType(file.type)} file={file} />
|
||||
<FilePreview fileType={getFileType(file?.type)} file={file} />
|
||||
)}
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<span className="block w-full overflow-hidden truncate text-ellipsis whitespace-nowrap text-xs">
|
||||
{file.filename}
|
||||
{file?.filename}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue