mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-19 16:56:12 +01:00
Merge branch 'main' into feat/Multitenant-login-OIDC
This commit is contained in:
commit
b9b0c03a63
255 changed files with 20660 additions and 41548 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.6",
|
||||
"version": "v0.7.7-rc1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
@ -50,11 +50,8 @@
|
|||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-table": "^8.11.7",
|
||||
"@zattoo/use-double-click": "1.2.0",
|
||||
"axios": "^1.7.7",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
|
|
@ -65,7 +62,7 @@
|
|||
"filenamify": "^6.0.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"html-to-image": "^1.11.11",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"i18next": "^24.2.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"librechat-data-provider": "*",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
@ -79,10 +76,10 @@
|
|||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-flip-toolkit": "^7.1.0",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-lazy-load-image-component": "^1.6.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.1.1",
|
||||
|
|
@ -103,7 +100,6 @@
|
|||
"tailwind-merge": "^1.9.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"tailwindcss-radix": "^2.8.0",
|
||||
"url": "^0.11.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -116,31 +112,31 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20.3.0",
|
||||
"@types/react": "^18.2.11",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
|
||||
"babel-plugin-root-import": "^6.6.0",
|
||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-canvas-mock": "^2.5.1",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-file-loader": "^1.0.3",
|
||||
"jest-junit": "^16.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.2.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^5.4.14",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.17.0",
|
||||
"vite-plugin-pwa": "^0.21.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AgentCapabilities } from 'librechat-data-provider';
|
||||
import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
|
||||
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
|
||||
import type { OptionWithIcon, ExtendedFile } from './types';
|
||||
|
||||
|
|
@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon &
|
|||
};
|
||||
|
||||
export type TAgentCapabilities = {
|
||||
[AgentCapabilities.execute_code]: boolean;
|
||||
[AgentCapabilities.file_search]: boolean;
|
||||
[AgentCapabilities.execute_code]: boolean;
|
||||
[AgentCapabilities.end_after_tools]?: boolean;
|
||||
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
||||
};
|
||||
|
|
@ -26,4 +26,5 @@ export type AgentForm = {
|
|||
tools?: string[];
|
||||
provider?: AgentProvider | OptionWithIcon;
|
||||
agent_ids?: string[];
|
||||
[AgentCapabilities.artifacts]?: ArtifactModes | string;
|
||||
} & TAgentCapabilities;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { SetterOrUpdater } from 'recoil';
|
|||
import type * as t from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type { TranslationKeys } from '~/hooks';
|
||||
|
||||
export type CodeBarProps = {
|
||||
lang: string;
|
||||
|
|
@ -66,7 +67,10 @@ export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
|||
|
||||
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
|
||||
|
||||
export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string;
|
||||
export type LocalizeFunction = (
|
||||
phraseKey: TranslationKeys,
|
||||
options?: Record<string, string | number>,
|
||||
) => string;
|
||||
|
||||
export type ChatFormValues = { text: string };
|
||||
|
||||
|
|
@ -85,6 +89,7 @@ export type IconMapProps = {
|
|||
iconURL?: string;
|
||||
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
||||
endpoint?: string | null;
|
||||
endpointType?: string;
|
||||
assistantName?: string;
|
||||
agentName?: string;
|
||||
avatar?: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useLocalize } from '~/hooks';
|
||||
import { TranslationKeys, useLocalize } from '~/hooks';
|
||||
import { BlinkAnimation } from './BlinkAnimation';
|
||||
import { TStartupConfig } from 'librechat-data-provider';
|
||||
import SocialLoginRender from './SocialLoginRender';
|
||||
|
|
@ -33,7 +33,7 @@ function AuthLayout({
|
|||
startupConfig: TStartupConfig | null | undefined;
|
||||
startupConfigError: unknown | null | undefined;
|
||||
pathname: string;
|
||||
error: string | null;
|
||||
error: TranslationKeys | null;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ function AuthLayout({
|
|||
<img
|
||||
src="/assets/logo.svg"
|
||||
className="h-full w-full object-contain"
|
||||
alt={localize('com_ui_logo', startupConfig?.appTitle ?? 'LibreChat')}
|
||||
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
|
||||
/>
|
||||
</div>
|
||||
</BlinkAnimation>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { TRegisterUser, TError } from 'librechat-data-provider';
|
|||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { ErrorMessage } from './ErrorMessage';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
|
||||
const Registration: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -56,7 +56,7 @@ const Registration: React.FC = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const renderInput = (id: string, label: string, type: string, validation: object) => (
|
||||
const renderInput = (id: string, label: TranslationKeys, type: string, validation: object) => (
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
|
|
@ -114,7 +114,7 @@ const Registration: React.FC = () => {
|
|||
: 'com_auth_registration_success_insecure',
|
||||
) +
|
||||
' ' +
|
||||
localize('com_auth_email_verification_redirecting', countdown.toString())}
|
||||
localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
|
||||
</div>
|
||||
)}
|
||||
{!startupConfigError && !isFetching && (
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function RequestPasswordReset() {
|
|||
</h1>
|
||||
{countdown > 0 && (
|
||||
<p className="text-center text-lg text-gray-600 dark:text-gray-400">
|
||||
{localize('com_auth_email_verification_redirecting', countdown.toString())}
|
||||
{localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
|
||||
</p>
|
||||
)}
|
||||
{showResendLink && countdown === 0 && (
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
select: useCallback(
|
||||
(data: TMessage[]) => {
|
||||
const dataTree = buildTree({ messages: data, fileMap });
|
||||
return dataTree?.length === 0 ? null : dataTree ?? null;
|
||||
return dataTree?.length === 0 ? null : (dataTree ?? null);
|
||||
},
|
||||
[fileMap],
|
||||
),
|
||||
|
|
@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
<ChatFormProvider {...methods}>
|
||||
<ChatContext.Provider value={chatHelpers}>
|
||||
<AddedChatContext.Provider value={addedChatHelpers}>
|
||||
<Presentation useSidePanel={true}>
|
||||
<Presentation>
|
||||
{content}
|
||||
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
||||
<ChatForm index={index} />
|
||||
|
|
|
|||
|
|
@ -220,8 +220,10 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
<span className="hidden sm:inline">
|
||||
{localize(
|
||||
'com_files_number_selected',
|
||||
`${table.getFilteredSelectedRowModel().rows.length}`,
|
||||
`${table.getFilteredRowModel().rows.length}`,
|
||||
{
|
||||
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
||||
1: `${table.getFilteredRowModel().rows.length}`,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from '~/components/ui/DropdownMenu';
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface SortFilterHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
|
@ -78,9 +78,12 @@ export function SortFilterHeader<TData, TValue>({
|
|||
<DropdownMenuSeparator className="dark:bg-gray-500" />
|
||||
{filters &&
|
||||
Object.entries(filters).map(([key, values]) =>
|
||||
values.map((value: string | number) => {
|
||||
const localizedValue = localize(valueMap?.[value] ?? '');
|
||||
const filterValue = localizedValue.length ? localizedValue : valueMap?.[value];
|
||||
values.map((value?: string | number) => {
|
||||
const translationKey = valueMap?.[value ?? ''];
|
||||
const filterValue =
|
||||
translationKey != null && translationKey.length
|
||||
? localize(translationKey as TranslationKeys)
|
||||
: String(value);
|
||||
if (!filterValue) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ export default function PopoverButtons({
|
|||
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
|
||||
const model = overrideModel ?? _model;
|
||||
|
||||
const isGenerativeModel = model?.toLowerCase()?.includes('gemini') ?? false;
|
||||
const isChatModel = (!isGenerativeModel && model?.toLowerCase()?.includes('chat')) ?? false;
|
||||
const isGenerativeModel = model?.toLowerCase().includes('gemini') ?? false;
|
||||
const isChatModel = (!isGenerativeModel && model?.toLowerCase().includes('chat')) ?? false;
|
||||
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
|
||||
|
||||
const { showExamples } = optionSettings;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const TemporaryChat = ({ isTemporaryChat, setIsTemporaryChat }: Temporary
|
|||
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
|
||||
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
|
||||
<div className="icon-md">
|
||||
<MessageCircleDashed className="icon-md" />
|
||||
<MessageCircleDashed className="icon-md" aria-hidden="true" />
|
||||
</div>
|
||||
</span>
|
||||
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon';
|
|||
import MarkdownLite from './MarkdownLite';
|
||||
import store from '~/store';
|
||||
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
export default function CodeAnalyze({
|
||||
initialProgress = 0.1,
|
||||
code,
|
||||
|
|
@ -22,9 +25,6 @@ export default function CodeAnalyze({
|
|||
const progress = useProgress(initialProgress);
|
||||
const showAnalysisCode = useRecoilValue(store.showCode);
|
||||
const [showCode, setShowCode] = useState(showAnalysisCode);
|
||||
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - progress * circumference;
|
||||
|
||||
const logs = outputs.reduce((acc, output) => {
|
||||
|
|
@ -53,9 +53,10 @@ export default function CodeAnalyze({
|
|||
<ProgressText
|
||||
progress={progress}
|
||||
onClick={() => setShowCode((prev) => !prev)}
|
||||
inProgressText="Analyzing"
|
||||
finishedText="Finished analyzing"
|
||||
inProgressText={localize('com_ui_analyzing')}
|
||||
finishedText={localize('com_ui_analyzing_finished')}
|
||||
hasInput={!!code.length}
|
||||
isExpanded={showCode}
|
||||
/>
|
||||
</div>
|
||||
{showCode && (
|
||||
|
|
|
|||
|
|
@ -50,11 +50,24 @@ const ContentParts = memo(
|
|||
[attachments, messageAttachmentsMap, messageId],
|
||||
);
|
||||
|
||||
const hasReasoningParts = useMemo(
|
||||
() => content?.some((part) => part?.type === ContentTypes.THINK && part.think) ?? false,
|
||||
[content],
|
||||
);
|
||||
const hasReasoningParts = useMemo(() => {
|
||||
const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
|
||||
const allThinkPartsHaveContent =
|
||||
content?.every((part) => {
|
||||
if (part?.type !== ContentTypes.THINK) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof part.think === 'string') {
|
||||
const cleanedContent = part.think.replace(/<\/?think>/g, '').trim();
|
||||
return cleanedContent.length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}) ?? false;
|
||||
|
||||
return hasThinkPart && allThinkPartsHaveContent;
|
||||
}, [content]);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ const Files = ({ message }: { message?: TMessage }) => {
|
|||
}, [message?.files]);
|
||||
|
||||
const otherFiles = useMemo(() => {
|
||||
return message?.files?.filter((file) => !file.type?.startsWith('image/')) || [];
|
||||
return message?.files?.filter((file) => !(file.type?.startsWith('image/') === true)) || [];
|
||||
}, [message?.files]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{otherFiles.length > 0 &&
|
||||
otherFiles.map((file) => <FileContainer key={file.file_id} file={file as TFile} />)}
|
||||
{imageFiles &&
|
||||
{imageFiles.length > 0 &&
|
||||
imageFiles.map((file) => (
|
||||
<Image
|
||||
key={file.file_id}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ const scaleImage = ({
|
|||
originalHeight,
|
||||
containerRef,
|
||||
}: {
|
||||
originalWidth: number;
|
||||
originalHeight: number;
|
||||
originalWidth?: number;
|
||||
originalHeight?: number;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const containerWidth = containerRef.current?.offsetWidth ?? 0;
|
||||
if (containerWidth === 0 || originalWidth === undefined || originalHeight === undefined) {
|
||||
if (containerWidth === 0 || originalWidth == null || originalHeight == null) {
|
||||
return { width: 'auto', height: 'auto' };
|
||||
}
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
|
|
@ -35,8 +35,8 @@ const Image = ({
|
|||
height: number;
|
||||
width: number;
|
||||
placeholderDimensions?: {
|
||||
height: string;
|
||||
width: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
};
|
||||
}) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
|
@ -47,8 +47,8 @@ const Image = ({
|
|||
const { width: scaledWidth, height: scaledHeight } = useMemo(
|
||||
() =>
|
||||
scaleImage({
|
||||
originalWidth: Number(placeholderDimensions?.width?.split('px')[0]) ?? width,
|
||||
originalHeight: Number(placeholderDimensions?.height?.split('px')[0]) ?? height,
|
||||
originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width),
|
||||
originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height),
|
||||
containerRef,
|
||||
}),
|
||||
[placeholderDimensions, height, width],
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function InProgressCall({
|
|||
progress: number;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if ((!isSubmitting && progress < 1) || error) {
|
||||
if ((!isSubmitting && progress < 1) || error === true) {
|
||||
return <CancelledIcon />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,9 @@ const MessageContent = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{thinkingContent && <Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>}
|
||||
{thinkingContent.length > 0 && (
|
||||
<Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>
|
||||
)}
|
||||
<DisplayMessage
|
||||
key={`display-${messageId}`}
|
||||
showCursor={showRegularCursor}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUse
|
|||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
auth={toolCall.auth}
|
||||
expires_at={toolCall.expires_at}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import type { TAttachment } from 'librechat-data-provider';
|
|||
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
|
||||
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
|
||||
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||
import { useProgress, useLocalize } from '~/hooks';
|
||||
import { CodeInProgress } from './CodeProgress';
|
||||
import Attachment from './Attachment';
|
||||
import LogContent from './LogContent';
|
||||
import { useProgress } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
interface ParsedArgs {
|
||||
|
|
@ -36,6 +36,9 @@ export function useParseArgs(args: string): ParsedArgs {
|
|||
}, [args]);
|
||||
}
|
||||
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
export default function ExecuteCode({
|
||||
initialProgress = 0.1,
|
||||
args,
|
||||
|
|
@ -49,14 +52,12 @@ export default function ExecuteCode({
|
|||
isSubmitting: boolean;
|
||||
attachments?: TAttachment[];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const showAnalysisCode = useRecoilValue(store.showCode);
|
||||
const [showCode, setShowCode] = useState(showAnalysisCode);
|
||||
|
||||
const { lang, code } = useParseArgs(args);
|
||||
const progress = useProgress(initialProgress);
|
||||
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - progress * circumference;
|
||||
|
||||
return (
|
||||
|
|
@ -78,9 +79,10 @@ export default function ExecuteCode({
|
|||
<ProgressText
|
||||
progress={progress}
|
||||
onClick={() => setShowCode((prev) => !prev)}
|
||||
inProgressText="Analyzing"
|
||||
finishedText="Finished analyzing"
|
||||
inProgressText={localize('com_ui_analyzing')}
|
||||
finishedText={localize('com_ui_analyzing_finished')}
|
||||
hasInput={!!code.length}
|
||||
isExpanded={showCode}
|
||||
/>
|
||||
</div>
|
||||
{showCode && (
|
||||
|
|
@ -105,9 +107,7 @@ export default function ExecuteCode({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{attachments?.map((attachment, index) => (
|
||||
<Attachment attachment={attachment} key={index} />
|
||||
))}
|
||||
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, atta
|
|||
}
|
||||
|
||||
// const expirationText = expiresAt
|
||||
// ? ` ${localize('com_download_expires', format(expiresAt, 'MM/dd/yy HH:mm'))}`
|
||||
// ? ` ${localize('com_download_expires', { 0: format(expiresAt, 'MM/dd/yy HH:mm') })}`
|
||||
// : ` ${localize('com_click_to_download')}`;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -11,9 +11,16 @@ type ReasoningProps = {
|
|||
const Reasoning = memo(({ reasoning }: ReasoningProps) => {
|
||||
const { isExpanded, nextType } = useMessageContext();
|
||||
const reasoningText = useMemo(() => {
|
||||
return reasoning.replace(/^<think>\s*/, '').replace(/\s*<\/think>$/, '');
|
||||
return reasoning
|
||||
.replace(/^<think>\s*/, '')
|
||||
.replace(/\s*<\/think>$/, '')
|
||||
.trim();
|
||||
}, [reasoning]);
|
||||
|
||||
if (!reasoningText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -39,16 +39,21 @@ export default function ProgressText({
|
|||
onClick,
|
||||
inProgressText,
|
||||
finishedText,
|
||||
authText,
|
||||
hasInput = true,
|
||||
popover = false,
|
||||
isExpanded = false,
|
||||
}: {
|
||||
progress: number;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
inProgressText: string;
|
||||
finishedText: string;
|
||||
authText?: string;
|
||||
hasInput?: boolean;
|
||||
popover?: boolean;
|
||||
isExpanded?: boolean;
|
||||
}) {
|
||||
const text = progress < 1 ? (authText ?? inProgressText) : finishedText;
|
||||
return (
|
||||
<Wrapper popover={popover}>
|
||||
<button
|
||||
|
|
@ -57,8 +62,14 @@ export default function ProgressText({
|
|||
disabled={!hasInput}
|
||||
onClick={onClick}
|
||||
>
|
||||
{progress < 1 ? inProgressText : finishedText}
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none">
|
||||
{text}
|
||||
<svg
|
||||
width="16"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
className={isExpanded ? 'rotate-180' : 'rotate-0'}
|
||||
>
|
||||
<path
|
||||
className={hasInput ? '' : 'stroke-transparent'}
|
||||
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { ShieldCheck, TriangleAlert } from 'lucide-react';
|
||||
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
|
|
@ -14,6 +15,9 @@ import WrenchIcon from './WrenchIcon';
|
|||
import { useProgress } from '~/hooks';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
export default function ToolCall({
|
||||
initialProgress = 0.1,
|
||||
isSubmitting,
|
||||
|
|
@ -21,6 +25,7 @@ export default function ToolCall({
|
|||
args: _args = '',
|
||||
output,
|
||||
attachments,
|
||||
auth,
|
||||
}: {
|
||||
initialProgress: number;
|
||||
isSubmitting: boolean;
|
||||
|
|
@ -28,13 +33,10 @@ export default function ToolCall({
|
|||
args: string | Record<string, unknown>;
|
||||
output?: string | null;
|
||||
attachments?: TAttachment[];
|
||||
auth?: string;
|
||||
expires_at?: number;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const progress = useProgress(initialProgress);
|
||||
const radius = 56.08695652173913;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - progress * circumference;
|
||||
|
||||
const { function_name, domain, isMCPToolCall } = useMemo(() => {
|
||||
if (typeof name !== 'string') {
|
||||
return { function_name: '', domain: null, isMCPToolCall: false };
|
||||
|
|
@ -83,8 +85,37 @@ export default function ToolCall({
|
|||
[args, output],
|
||||
);
|
||||
|
||||
const authDomain = useMemo(() => {
|
||||
const authURL = auth ?? '';
|
||||
if (!authURL) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const url = new URL(authURL);
|
||||
return url.hostname;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
const progress = useProgress(error === true ? 1 : initialProgress);
|
||||
const cancelled = (!isSubmitting && progress < 1) || error === true;
|
||||
const offset = circumference - progress * circumference;
|
||||
|
||||
const renderIcon = () => {
|
||||
if (progress < 1) {
|
||||
if (progress < 1 && authDomain.length > 0) {
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-text-secondary"
|
||||
style={{ opacity: 1, transform: 'none' }}
|
||||
data-projection-id="849"
|
||||
>
|
||||
<div>
|
||||
<ShieldCheck />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (progress < 1) {
|
||||
return (
|
||||
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
|
||||
<div
|
||||
|
|
@ -101,43 +132,67 @@ export default function ToolCall({
|
|||
);
|
||||
}
|
||||
|
||||
return error === true ? <CancelledIcon /> : <FinishedIcon />;
|
||||
return cancelled ? <CancelledIcon /> : <FinishedIcon />;
|
||||
};
|
||||
|
||||
const getFinishedText = () => {
|
||||
if (cancelled) {
|
||||
return localize('com_ui_error');
|
||||
}
|
||||
if (isMCPToolCall === true) {
|
||||
return localize('com_assistants_completed_function', function_name);
|
||||
return localize('com_assistants_completed_function', { 0: function_name });
|
||||
}
|
||||
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
|
||||
return localize('com_assistants_completed_action', domain);
|
||||
return localize('com_assistants_completed_action', { 0: domain });
|
||||
}
|
||||
return localize('com_assistants_completed_function', function_name);
|
||||
return localize('com_assistants_completed_function', { 0: function_name });
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<div className="my-2.5 flex items-center gap-2.5">
|
||||
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
|
||||
<ProgressText
|
||||
progress={progress}
|
||||
onClick={() => ({})}
|
||||
inProgressText={localize('com_assistants_running_action')}
|
||||
finishedText={getFinishedText()}
|
||||
hasInput={hasInfo}
|
||||
popover={true}
|
||||
/>
|
||||
{hasInfo && (
|
||||
<ToolPopover
|
||||
input={args ?? ''}
|
||||
output={output}
|
||||
domain={domain ?? ''}
|
||||
function_name={function_name}
|
||||
<div className="my-2.5 flex flex-wrap items-center gap-2.5">
|
||||
<div className="flex w-full items-center gap-2.5">
|
||||
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
|
||||
<ProgressText
|
||||
progress={cancelled ? 1 : progress}
|
||||
inProgressText={localize('com_assistants_running_action')}
|
||||
authText={
|
||||
!cancelled && authDomain.length > 0 ? localize('com_ui_requires_auth') : undefined
|
||||
}
|
||||
finishedText={getFinishedText()}
|
||||
hasInput={hasInfo}
|
||||
popover={true}
|
||||
/>
|
||||
{hasInfo && (
|
||||
<ToolPopover
|
||||
input={args ?? ''}
|
||||
output={output}
|
||||
domain={authDomain || (domain ?? '')}
|
||||
function_name={function_name}
|
||||
pendingAuth={authDomain.length > 0 && !cancelled && progress < 1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{auth != null && auth && progress < 1 && !cancelled && (
|
||||
<div className="flex w-full flex-col gap-2.5">
|
||||
<div className="mb-1 mt-2">
|
||||
<a
|
||||
className="inline-flex items-center justify-center gap-2 rounded-3xl bg-surface-tertiary px-4 py-2 text-sm font-medium hover:bg-surface-hover"
|
||||
href={auth}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{localize('com_ui_sign_in_to_domain', { 0: authDomain })}
|
||||
</a>
|
||||
</div>
|
||||
<p className="flex items-center text-xs text-text-secondary">
|
||||
<TriangleAlert className="mr-1.5 inline-block h-4 w-4" />
|
||||
{localize('com_assistants_allow_sites_you_trust')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{attachments?.map((attachment, index) => (
|
||||
<Attachment attachment={attachment} key={index} />
|
||||
))}
|
||||
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ import useLocalize from '~/hooks/useLocalize';
|
|||
export default function ToolPopover({
|
||||
input,
|
||||
output,
|
||||
function_name,
|
||||
domain,
|
||||
function_name,
|
||||
pendingAuth,
|
||||
}: {
|
||||
input: string;
|
||||
function_name: string;
|
||||
output?: string | null;
|
||||
domain?: string;
|
||||
pendingAuth?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const formatText = (text: string) => {
|
||||
|
|
@ -21,6 +23,17 @@ export default function ToolPopover({
|
|||
}
|
||||
};
|
||||
|
||||
let title =
|
||||
domain != null && domain
|
||||
? localize('com_assistants_domain_info', { 0: domain })
|
||||
: localize('com_assistants_function_use', { 0: function_name });
|
||||
if (pendingAuth === true) {
|
||||
title =
|
||||
domain != null && domain
|
||||
? localize('com_assistants_action_attempt', { 0: domain })
|
||||
: localize('com_assistants_attempt_info');
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
|
|
@ -28,27 +41,23 @@ export default function ToolPopover({
|
|||
align="start"
|
||||
sideOffset={12}
|
||||
alignOffset={-5}
|
||||
className="w-18 min-w-[180px] max-w-sm rounded-lg bg-white dark:bg-gray-900"
|
||||
className="w-18 min-w-[180px] max-w-sm rounded-lg bg-surface-primary px-1"
|
||||
>
|
||||
<div tabIndex={-1}>
|
||||
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
|
||||
<div className="mb-2 text-sm font-medium dark:text-gray-100">
|
||||
{domain != null && domain
|
||||
? localize('com_assistants_domain_info', domain)
|
||||
: localize('com_assistants_function_use', function_name)}
|
||||
</div>
|
||||
<div className="mb-2 text-sm font-medium text-text-primary">{title}</div>
|
||||
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
|
||||
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700">
|
||||
<div className="max-h-32 overflow-y-auto rounded-md bg-surface-tertiary p-2">
|
||||
<code className="!whitespace-pre-wrap ">{formatText(input)}</code>
|
||||
</div>
|
||||
</div>
|
||||
{output != null && output && (
|
||||
<>
|
||||
<div className="mb-2 mt-2 text-sm font-medium dark:text-gray-100">
|
||||
<div className="mb-2 mt-2 text-sm font-medium text-text-primary">
|
||||
{localize('com_ui_result')}
|
||||
</div>
|
||||
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
|
||||
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700">
|
||||
<div className="max-h-32 overflow-y-auto rounded-md bg-surface-tertiary p-2">
|
||||
<code className="!whitespace-pre-wrap ">{formatText(output)}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,19 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider';
|
||||
import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider';
|
||||
import { useDeleteFilesMutation } from '~/data-provider';
|
||||
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
|
||||
import Artifacts from '~/components/Artifacts/Artifacts';
|
||||
import { SidePanel } from '~/components/SidePanel';
|
||||
import { SidePanelGroup } from '~/components/SidePanel';
|
||||
import { useSetFilesToDelete } from '~/hooks';
|
||||
import { EditorProvider } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
export default function Presentation({
|
||||
children,
|
||||
useSidePanel = false,
|
||||
panel,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
panel?: React.ReactNode;
|
||||
useSidePanel?: boolean;
|
||||
}) {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
export default function Presentation({ children }: { children: React.ReactNode }) {
|
||||
const artifacts = useRecoilValue(store.artifactsState);
|
||||
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
||||
const artifactsVisible = useRecoilValue(store.artifactsVisible);
|
||||
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
|
||||
const { mutateAsync } = useDeleteFilesMutation({
|
||||
|
|
@ -83,35 +65,24 @@ export default function Presentation({
|
|||
</div>
|
||||
);
|
||||
|
||||
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
|
||||
return (
|
||||
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
||||
<SidePanel
|
||||
defaultLayout={defaultLayout}
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
fullPanelCollapse={fullCollapse}
|
||||
artifacts={
|
||||
artifactsVisible === true &&
|
||||
codeArtifacts === true &&
|
||||
Object.keys(artifacts ?? {}).length > 0 ? (
|
||||
<EditorProvider>
|
||||
<Artifacts />
|
||||
</EditorProvider>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<main className="flex h-full flex-col overflow-y-auto" role="main">
|
||||
{children}
|
||||
</main>
|
||||
</SidePanel>
|
||||
</DragDropWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
|
||||
{layout()}
|
||||
{panel != null && panel}
|
||||
<SidePanelGroup
|
||||
defaultLayout={defaultLayout}
|
||||
fullPanelCollapse={fullCollapse}
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
artifacts={
|
||||
artifactsVisible === true && Object.keys(artifacts ?? {}).length > 0 ? (
|
||||
<EditorProvider>
|
||||
<Artifacts />
|
||||
</EditorProvider>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<main className="flex h-full flex-col overflow-y-auto" role="main">
|
||||
{children}
|
||||
</main>
|
||||
</SidePanelGroup>
|
||||
</DragDropWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { TPromptGroup } from 'librechat-data-provider';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
|
||||
export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) {
|
||||
export default function PromptCard({ promptGroup }: { promptGroup?: TPromptGroup }) {
|
||||
return (
|
||||
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
|
||||
<div className="">
|
||||
<CategoryIcon className="size-4" category={promptGroup.category || ''} />
|
||||
<CategoryIcon className="size-4" category={promptGroup?.category ?? ''} />
|
||||
</div>
|
||||
<p className="break-word line-clamp-3 text-balance text-gray-600 dark:text-gray-400">
|
||||
{promptGroup?.oneliner || promptGroup?.productionPrompt?.prompt}
|
||||
{(promptGroup?.oneliner ?? '') || promptGroup?.productionPrompt?.prompt}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useMemo, memo } from 'react';
|
||||
import { parseISO, isToday } from 'date-fns';
|
||||
import { TConversation } from 'librechat-data-provider';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { groupConversationsByDate } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Convo from './Convo';
|
||||
|
||||
const Conversations = ({
|
||||
|
|
@ -41,8 +41,7 @@ const Conversations = ({
|
|||
paddingLeft: '10px',
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */}
|
||||
{localize(groupName) || groupName}
|
||||
{localize(groupName as TranslationKeys) || groupName}
|
||||
</div>
|
||||
{convos.map((convo, i) => (
|
||||
<Convo
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
HoverCardContent,
|
||||
} from '~/components/ui';
|
||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import { useLocalize, useNavigateToConvo } from '~/hooks';
|
||||
import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
|
||||
import { useForkConvoMutation } from '~/data-provider';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { ESide } from '~/common';
|
||||
|
|
@ -201,7 +201,7 @@ export default function Fork({
|
|||
align="center"
|
||||
>
|
||||
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
|
||||
{localize(activeSetting)}
|
||||
{localize(activeSetting as TranslationKeys)}
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" />
|
||||
|
|
@ -216,7 +216,9 @@ export default function Fork({
|
|||
<span>{localize('com_ui_fork_info_1')}</span>
|
||||
<span>{localize('com_ui_fork_info_2')}</span>
|
||||
<span>
|
||||
{localize('com_ui_fork_info_3', localize('com_ui_fork_split_target'))}
|
||||
{localize('com_ui_fork_info_3', {
|
||||
0: localize('com_ui_fork_split_target'),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
|
|
@ -233,7 +235,7 @@ export default function Fork({
|
|||
hoverTitle={
|
||||
<>
|
||||
<GitCommit className="h-5 w-5 rotate-90" />
|
||||
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
|
||||
{localize(optionLabels[ForkOptions.DIRECT_PATH] as TranslationKeys)}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_visible')}
|
||||
|
|
@ -251,7 +253,7 @@ export default function Fork({
|
|||
hoverTitle={
|
||||
<>
|
||||
<GitBranchPlus className="h-4 w-4 rotate-180" />
|
||||
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
|
||||
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES] as TranslationKeys)}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_branches')}
|
||||
|
|
@ -269,9 +271,9 @@ export default function Fork({
|
|||
hoverTitle={
|
||||
<>
|
||||
<ListTree className="h-5 w-5" />
|
||||
{`${localize(optionLabels[ForkOptions.TARGET_LEVEL])} (${localize(
|
||||
'com_endpoint_default',
|
||||
)})`}
|
||||
{`${localize(
|
||||
optionLabels[ForkOptions.TARGET_LEVEL] as TranslationKeys,
|
||||
)} (${localize('com_endpoint_default')})`}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_target')}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
|||
<div
|
||||
data-testid="convo-icon"
|
||||
title={name}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export default function Settings({
|
|||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_temperature')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', '1')})
|
||||
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
conversation ?? {};
|
||||
|
||||
const currentList = useMemo(
|
||||
() => Object.values(assistantListMap?.[endpoint ?? ''] ?? {}) as Assistant[],
|
||||
() => Object.values(assistantListMap[endpoint ?? ''] ?? {}) as Assistant[],
|
||||
[assistantListMap, endpoint],
|
||||
);
|
||||
|
||||
const assistants = useMemo(() => {
|
||||
const currentAssistants = (currentList ?? []).map(({ id, name }) => ({
|
||||
const currentAssistants = currentList.map(({ id, name }) => ({
|
||||
label: name,
|
||||
value: id,
|
||||
}));
|
||||
|
|
@ -52,8 +52,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
});
|
||||
|
||||
const activeAssistant = useMemo(() => {
|
||||
if (assistant_id) {
|
||||
return assistantListMap[endpoint ?? '']?.[assistant_id];
|
||||
if (assistant_id != null && assistant_id) {
|
||||
return assistantListMap[endpoint ?? '']?.[assistant_id] as Assistant | null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -70,11 +70,13 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
}, [models, activeAssistant, localize]);
|
||||
|
||||
const [assistantValue, setAssistantValue] = useState<Option>(
|
||||
activeAssistant ? { label: activeAssistant.name, value: activeAssistant.id } : defaultOption,
|
||||
activeAssistant != null
|
||||
? { label: activeAssistant.name ?? '', value: activeAssistant.id }
|
||||
: defaultOption,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (assistantValue && assistantValue.value === '') {
|
||||
if (assistantValue.value === '') {
|
||||
setOption('presetOverride')({
|
||||
assistant_id: assistantValue.value,
|
||||
} as Partial<TPreset>);
|
||||
|
|
@ -95,7 +97,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
return;
|
||||
}
|
||||
|
||||
const assistant = assistantListMap[endpoint ?? '']?.[value];
|
||||
const assistant = assistantListMap[endpoint ?? '']?.[value] as Assistant | null;
|
||||
if (!assistant) {
|
||||
setAssistantValue(defaultOption);
|
||||
return;
|
||||
|
|
@ -103,7 +105,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
|
||||
setAssistantValue({
|
||||
label: assistant.name ?? '',
|
||||
value: assistant.id ?? '',
|
||||
value: assistant.id || '',
|
||||
});
|
||||
setOption('assistant_id')(assistant.id);
|
||||
if (assistant.model) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
|
|||
<TextareaAutosize
|
||||
id={`input-${idx}`}
|
||||
disabled={readonly}
|
||||
value={example?.input?.content || ''}
|
||||
value={example.input.content || ''}
|
||||
onChange={(e) => setExample(idx, 'input', e.target.value ?? null)}
|
||||
placeholder="Set example input. Example is ignored if empty."
|
||||
className={cn(
|
||||
|
|
@ -62,7 +62,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
|
|||
<TextareaAutosize
|
||||
id={`output-${idx}`}
|
||||
disabled={readonly}
|
||||
value={example?.output?.content || ''}
|
||||
value={example.output.content || ''}
|
||||
onChange={(e) => setExample(idx, 'output', e.target.value ?? null)}
|
||||
placeholder={'Set example output. Example is ignored if empty.'}
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_top_p')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', google.topP.default + '')})
|
||||
({localize('com_endpoint_default_with_num', { 0: google.topP.default + '' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
@ -221,7 +221,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_top_k')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', google.topK.default + '')})
|
||||
({localize('com_endpoint_default_with_num',{ 0: google.topK.default + '' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
@ -261,7 +261,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_max_output_tokens')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', google.maxOutputTokens.default + '')})
|
||||
({localize('com_endpoint_default_with_num', { 0: google.maxOutputTokens.default + '' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ export default function Settings({
|
|||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_temperature')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', '0.8')})
|
||||
({localize('com_endpoint_default_with_num', { 0: '0.8' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
@ -266,7 +266,7 @@ export default function Settings({
|
|||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_top_p')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', '1')})
|
||||
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
@ -307,7 +307,7 @@ export default function Settings({
|
|||
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_frequency_penalty')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', '0')})
|
||||
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
@ -348,7 +348,7 @@ export default function Settings({
|
|||
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_presence_penalty')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_with_num', '0')})
|
||||
({localize('com_endpoint_default_with_num', { 0: '0' })})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const FileDashboardView = () => {
|
|||
return (
|
||||
<div className="bg-[#f9f9f9] p-0 lg:p-7">
|
||||
<div className="ml-3 mt-3 flex flex-row justify-between">
|
||||
{params?.vectorStoreId && (
|
||||
{params.vectorStoreId && (
|
||||
<Button
|
||||
className="block lg:hidden"
|
||||
variant={'outline'}
|
||||
|
|
|
|||
|
|
@ -244,9 +244,10 @@ export default function DataTableFile<TData, TValue>({
|
|||
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
|
||||
<div className="text-muted-foreground ml-2 flex-1 text-sm">
|
||||
{localize(
|
||||
'com_files_number_selected',
|
||||
`${table.getFilteredSelectedRowModel().rows.length}`,
|
||||
`${table.getFilteredRowModel().rows.length}`,
|
||||
'com_files_number_selected', {
|
||||
0: `${table.getFilteredSelectedRowModel().rows.length}`,
|
||||
1: `${table.getFilteredRowModel().rows.length}`,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ import React from 'react';
|
|||
import FileSidePanel from './FileList/FileSidePanel';
|
||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
import FilesSectionSelector from './FilesSectionSelector';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Button } from '../ui';
|
||||
|
||||
export default function FilesListView() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<div className="bg-[#f9f9f9] p-0 lg:p-7">
|
||||
<div className="m-4 flex w-full flex-row justify-between md:m-2">
|
||||
<FilesSectionSelector />
|
||||
{params?.fileId && (
|
||||
{params.fileId != null && params.fileId && (
|
||||
<Button
|
||||
className="block lg:hidden"
|
||||
variant={'outline'}
|
||||
|
|
@ -20,21 +22,21 @@ export default function FilesListView() {
|
|||
navigate('/d/files');
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
{localize('com_ui_go_back')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full flex-row divide-x">
|
||||
<div
|
||||
className={`mr-2 w-full xl:w-1/3 ${
|
||||
params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
|
||||
params.fileId != null && params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
|
||||
}`}
|
||||
>
|
||||
<FileSidePanel />
|
||||
</div>
|
||||
<div
|
||||
className={`h-[85vh] w-full overflow-y-auto xl:w-2/3 ${
|
||||
params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
|
||||
params.fileId != null && params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
|
||||
}`}
|
||||
>
|
||||
<Outlet />
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export default function VectorStorePreview() {
|
|||
<Clock3 className="text-base text-gray-500 md:text-lg lg:text-xl" />
|
||||
Created At
|
||||
</span>
|
||||
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt?.toString()}</span>
|
||||
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt.toString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function ModelSelect({
|
|||
}
|
||||
|
||||
const { endpoint: _endpoint, endpointType } = conversation;
|
||||
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
||||
const models = modelsQuery.data?.[_endpoint] ?? [];
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
|
||||
const OptionComponent = multiChatOptions[endpoint];
|
||||
|
|
|
|||
|
|
@ -40,13 +40,15 @@ export const TemporaryChat = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="sticky bottom-0 border-none bg-surface-tertiary px-6 py-4 ">
|
||||
<div className="flex items-center">
|
||||
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
|
||||
<MessageCircleDashed className="icon-sm" />
|
||||
<span className="text-sm text-text-primary">{localize('com_ui_temporary_chat')}</span>
|
||||
<div className="sticky bottom-0 mt-auto w-full border-none bg-surface-tertiary px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('flex items-center gap-2', isActiveConvo && 'opacity-40')}>
|
||||
<MessageCircleDashed className="icon-sm" aria-hidden="true" />
|
||||
<span className="truncate text-sm text-text-primary">
|
||||
{localize('com_ui_temporary_chat')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<div className="flex flex-shrink-0 items-center">
|
||||
<Switch
|
||||
id="temporary-chat-switch"
|
||||
checked={isTemporary}
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ const errorMessages = {
|
|||
[ErrorTypes.NO_SYSTEM_MESSAGES]: `com_error_${ErrorTypes.NO_SYSTEM_MESSAGES}`,
|
||||
[ErrorTypes.EXPIRED_USER_KEY]: (json: TExpiredKey, localize: LocalizeFunction) => {
|
||||
const { expiredAt, endpoint } = json;
|
||||
return localize('com_error_expired_user_key', endpoint, expiredAt);
|
||||
return localize('com_error_expired_user_key', { 0: endpoint, 1: expiredAt });
|
||||
},
|
||||
[ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => {
|
||||
const { info } = json;
|
||||
return localize('com_error_input_length', info);
|
||||
return localize('com_error_input_length', { 0: info });
|
||||
},
|
||||
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
|
||||
const { info } = json;
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
|
|||
<div className="h-7 w-7 flex-shrink-0">
|
||||
<div className="relative flex h-full items-center justify-center rounded-full border border-border-medium bg-surface-primary-alt text-text-primary">
|
||||
{tags.length > 0 ? (
|
||||
<BookmarkFilledIcon className="h-4 w-4" />
|
||||
<BookmarkFilledIcon className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<BookmarkIcon className="h-4 w-4" />
|
||||
<BookmarkIcon className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function Avatar() {
|
|||
const megabytes =
|
||||
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
||||
showToast({
|
||||
message: localize('com_ui_upload_invalid_var', megabytes + ''),
|
||||
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const RevokeKeysButton = ({
|
|||
|
||||
const dialogTitle = all
|
||||
? localize('com_ui_revoke_keys')
|
||||
: localize('com_ui_revoke_key_endpoint', endpoint);
|
||||
: localize('com_ui_revoke_key_endpoint', { 0: endpoint });
|
||||
|
||||
const dialogMessage = all
|
||||
? localize('com_ui_revoke_keys_confirm')
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function DecibelSelector() {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_db_sensitivity')}</div>
|
||||
<div className="w-2" />
|
||||
<small className="opacity-40">({localize('com_endpoint_default_with_num', '-45')})</small>
|
||||
<small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '-45' })})</small>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Slider
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function DecibelSelector() {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_playback_rate')}</div>
|
||||
<div className="w-2" />
|
||||
<small className="opacity-40">({localize('com_endpoint_default_with_num', '1')})</small>
|
||||
<small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '1' })})</small>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Slider
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const AdminSettings = () => {
|
|||
variant="outline"
|
||||
className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" />
|
||||
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
|
||||
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const Command = ({
|
|||
return (
|
||||
<div className="rounded-xl border border-border-light">
|
||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||
<SquareSlash className="icon-sm" />
|
||||
<SquareSlash className="icon-sm" aria-hidden="true" />
|
||||
<Input
|
||||
type="text"
|
||||
tabIndex={tabIndex}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const DeleteVersion = ({
|
|||
htmlFor="dialog-delete-confirm-prompt"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{localize('com_ui_delete_confirm_prompt_version_var', name)}
|
||||
{localize('com_ui_delete_confirm_prompt_version_var', { 0: name })}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const Description = ({
|
|||
return (
|
||||
<div className="rounded-xl border border-border-light">
|
||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||
<Info className="icon-sm" />
|
||||
<Info className="icon-sm" aria-hidden="true" />
|
||||
<Input
|
||||
type="text"
|
||||
tabIndex={tabIndex}
|
||||
|
|
|
|||
|
|
@ -48,5 +48,5 @@ export default function CategoryIcon({
|
|||
if (!IconComponent) {
|
||||
return null;
|
||||
}
|
||||
return <IconComponent className={cn(colorClass, className)} />;
|
||||
return <IconComponent className={cn(colorClass, className)} aria-hidden="true" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,17 +30,13 @@ export default function GroupSidePanel({
|
|||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4',
|
||||
isDetailView && isSmallerScreen ? 'hidden' : '',
|
||||
isDetailView === true && isSmallerScreen ? 'hidden' : '',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<List
|
||||
groups={promptGroups}
|
||||
isChatRoute={isChatRoute}
|
||||
isLoading={!!groupsQuery.isLoading}
|
||||
/>
|
||||
<List groups={promptGroups} isChatRoute={isChatRoute} isLoading={!!groupsQuery.isLoading} />
|
||||
</div>
|
||||
<PanelNavigation
|
||||
nextPage={nextPage}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ export default function VariableForm({
|
|||
return (
|
||||
<InputCombobox
|
||||
options={field.config.options || []}
|
||||
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
||||
placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
||||
|
|
@ -192,7 +192,7 @@ export default function VariableForm({
|
|||
defaultTextProps,
|
||||
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
||||
)}
|
||||
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
||||
placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
|
||||
maxRows={8}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const PromptForm = () => {
|
|||
|
||||
const selectedPrompt = useMemo(
|
||||
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
|
||||
[prompts, /* eslint-disable-line react-hooks/exhaustive-deps */ selectionIndex],
|
||||
[prompts, selectionIndex],
|
||||
);
|
||||
|
||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||
|
|
@ -102,7 +102,7 @@ const PromptForm = () => {
|
|||
);
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) {
|
||||
if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) {
|
||||
makeProductionMutation.mutate({
|
||||
id: data.prompt._id,
|
||||
groupId: data.prompt.groupId,
|
||||
|
|
@ -336,7 +336,7 @@ const PromptForm = () => {
|
|||
variant="ghost"
|
||||
className="h-10 w-10 border border-border-light p-0 lg:hidden"
|
||||
onClick={() => setShowSidePanel(true)}
|
||||
aria-label={localize('com_ui_open_menu')}
|
||||
aria-label={localize('com_endpoint_open_menu')}
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
</Button>
|
||||
|
|
@ -382,8 +382,8 @@ const PromptForm = () => {
|
|||
onClick={() => setShowSidePanel(false)}
|
||||
aria-hidden={!showSidePanel}
|
||||
tabIndex={showSidePanel ? 0 : -1}
|
||||
aria-label={localize('com_ui_close_menu')}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 z-50 lg:hidden"
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const PromptVariables = ({
|
|||
return (
|
||||
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md ">
|
||||
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||
<Variable className="icon-sm" />
|
||||
<Variable className="icon-sm" aria-hidden="true" />
|
||||
{localize('com_ui_variables')}
|
||||
</h3>
|
||||
<div className="flex flex-col space-y-4">
|
||||
|
|
|
|||
|
|
@ -111,12 +111,12 @@ const VersionCard = ({
|
|||
onClick={onClick}
|
||||
aria-selected={isSelected}
|
||||
role="tab"
|
||||
aria-label={localize('com_ui_version_var', `${totalVersions - index}`)}
|
||||
aria-label={localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-start justify-between lg:flex-col xl:flex-row">
|
||||
<h3 className="font-bold text-text-primary">
|
||||
{localize('com_ui_version_var', `${totalVersions - index}`)}
|
||||
{localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
|
||||
</h3>
|
||||
<time className="text-xs text-text-secondary" dateTime={prompt.createdAt}>
|
||||
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
|
|||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
|
||||
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
|
||||
{localize('com_ui_share_var', `"${group.name}"`)}
|
||||
{localize('com_ui_share_var', { 0: `"${group.name}"` })}
|
||||
</OGDialogTitle>
|
||||
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
|
||||
<div id="form-description" className="sr-only">
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function MessagesView({
|
|||
}}
|
||||
>
|
||||
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
|
||||
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? (
|
||||
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
|
||||
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
||||
Nothing found
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import Message from './Message';
|
||||
|
|
@ -25,17 +26,16 @@ export default function MultiMessage({
|
|||
}, [messagesTree?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesTree?.length && siblingIdx >= messagesTree?.length) {
|
||||
if (messagesTree?.length != null && siblingIdx >= messagesTree.length) {
|
||||
setSiblingIdx(0);
|
||||
}
|
||||
}, [siblingIdx, messagesTree?.length, setSiblingIdx]);
|
||||
|
||||
if (!(messagesTree && messagesTree?.length)) {
|
||||
if (!(messagesTree && messagesTree.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = messagesTree[messagesTree.length - siblingIdx - 1];
|
||||
|
||||
const message = messagesTree[messagesTree.length - siblingIdx - 1] as TMessage | null;
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ const AdminSettings = () => {
|
|||
variant={'outline'}
|
||||
className="btn btn-neutral border-token-border-light relative mb-4 h-9 w-full gap-1 rounded-lg font-medium"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" />
|
||||
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
|
||||
{localize('com_ui_admin_settings')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function Avatar({
|
|||
} else {
|
||||
const megabytes = sizeLimit ? formatBytes(sizeLimit) : 2;
|
||||
showToast({
|
||||
message: localize('com_ui_upload_invalid_var', megabytes + ''),
|
||||
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
@ -337,7 +334,7 @@ export default function AgentConfig({
|
|||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{(codeEnabled || fileSearchEnabled) && (
|
||||
{(codeEnabled || fileSearchEnabled || artifactsEnabled) && (
|
||||
<div className="mb-4 flex w-full flex-col items-start gap-3">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_capabilities')}
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
124
client/src/components/SidePanel/Agents/Artifacts.tsx
Normal file
124
client/src/components/SidePanel/Agents/Artifacts.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() =>
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function ImageVision() {
|
|||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
value={field.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -221,8 +221,8 @@ export default function Parameters({
|
|||
onClick={handleResetParameters}
|
||||
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
{localize('com_ui_reset_var', localize('com_ui_model_parameters'))}
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function Retrieval({ retrievalModels }: { retrievalModels: Set<st
|
|||
disabled={isDisabled}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
value={field.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default function HideSequential() {
|
|||
type="button"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() =>
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
|
||||
setValue(
|
||||
AgentCapabilities.hide_sequential_outputs,
|
||||
!getValues(AgentCapabilities.hide_sequential_outputs),
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export default function ShareAgent({
|
|||
)}
|
||||
aria-label={localize(
|
||||
'com_ui_share_var',
|
||||
agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
|
||||
{ 0: agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent') },
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -150,7 +150,7 @@ export default function ShareAgent({
|
|||
<OGDialogTitle>
|
||||
{localize(
|
||||
'com_ui_share_var',
|
||||
agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
|
||||
{ 0: agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent') },
|
||||
)}
|
||||
</OGDialogTitle>
|
||||
<form
|
||||
|
|
|
|||
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}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ function Avatar({
|
|||
} else {
|
||||
const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
||||
showToast({
|
||||
message: localize('com_ui_upload_invalid_var', megabytes + ''),
|
||||
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function ImageVision() {
|
|||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default function Retrieval({
|
|||
disabled={isDisabled}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
onClick={() => setShowFiles(true)}
|
||||
aria-label={localize('com_sidepanel_manage_files')}
|
||||
>
|
||||
<ArrowUpLeft className="h-4 w-4" />
|
||||
<ArrowUpLeft className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="ml-2">{localize('com_sidepanel_manage_files')}</span>
|
||||
</Button>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { OptionTypes } from 'librechat-data-provider';
|
|||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
|
@ -75,7 +75,7 @@ function DynamicTags({
|
|||
|
||||
if (minTags != null && currentTags.length <= minTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_min_tags', minTags + ''),
|
||||
message: localize('com_ui_min_tags',{ 0: minTags + '' }),
|
||||
status: 'warning',
|
||||
});
|
||||
return;
|
||||
|
|
@ -94,7 +94,7 @@ function DynamicTags({
|
|||
let update = [...(currentTags ?? []), tagText];
|
||||
if (maxTags != null && update.length > maxTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_max_tags', maxTags + ''),
|
||||
message: localize('com_ui_max_tags', { 0: maxTags + '' }),
|
||||
status: 'warning',
|
||||
});
|
||||
update = update.slice(-maxTags);
|
||||
|
|
@ -126,7 +126,7 @@ function DynamicTags({
|
|||
htmlFor={`${settingKey}-dynamic-input`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
|
||||
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
(
|
||||
|
|
@ -174,7 +174,7 @@ function DynamicTags({
|
|||
}
|
||||
}}
|
||||
onChange={(e) => setTagText(e.target.value)}
|
||||
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
|
||||
placeholder={placeholderCode ? localize(placeholder as TranslationKeys) ?? placeholder : placeholder}
|
||||
className={cn('flex h-10 max-h-10 border-none bg-surface-secondary px-3 py-2')}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -182,7 +182,7 @@ function DynamicTags({
|
|||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) ?? description : description}
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
side={descriptionSide as ESide}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,78 +1,58 @@
|
|||
import throttle from 'lodash/throttle';
|
||||
import { getConfigDefaults } from 'librechat-data-provider';
|
||||
import { useState, useCallback, useMemo, memo } from 'react';
|
||||
import { useUserKeyQuery } from 'librechat-data-provider/react-query';
|
||||
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
|
||||
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
|
||||
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import { ResizableHandleAlt, ResizablePanel } from '~/components/ui/Resizable';
|
||||
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
|
||||
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import NavToggle from '~/components/Nav/NavToggle';
|
||||
import { cn, getEndpointField } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import Switcher from './Switcher';
|
||||
import Nav from './Nav';
|
||||
|
||||
interface SidePanelProps {
|
||||
defaultLayout?: number[] | undefined;
|
||||
defaultCollapsed?: boolean;
|
||||
navCollapsedSize?: number;
|
||||
fullPanelCollapse?: boolean;
|
||||
artifacts?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const defaultMinSize = 20;
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
const normalizeLayout = (layout: number[]) => {
|
||||
const sum = layout.reduce((acc, size) => acc + size, 0);
|
||||
if (Math.abs(sum - 100) < 0.01) {
|
||||
return layout.map((size) => Number(size.toFixed(2)));
|
||||
}
|
||||
|
||||
const factor = 100 / sum;
|
||||
const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2)));
|
||||
|
||||
const adjustedSum = normalizedLayout.reduce(
|
||||
(acc, size, index) => (index === layout.length - 1 ? acc : acc + size),
|
||||
0,
|
||||
);
|
||||
normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2));
|
||||
|
||||
return normalizedLayout;
|
||||
};
|
||||
|
||||
const SidePanel = ({
|
||||
defaultLayout = [97, 3],
|
||||
defaultCollapsed = false,
|
||||
fullPanelCollapse = false,
|
||||
defaultSize,
|
||||
panelRef,
|
||||
navCollapsedSize = 3,
|
||||
artifacts,
|
||||
children,
|
||||
}: SidePanelProps) => {
|
||||
hasArtifacts,
|
||||
minSize,
|
||||
setMinSize,
|
||||
collapsedSize,
|
||||
setCollapsedSize,
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
fullCollapse,
|
||||
setFullCollapse,
|
||||
interfaceConfig,
|
||||
}: {
|
||||
defaultSize?: number;
|
||||
hasArtifacts: boolean;
|
||||
navCollapsedSize?: number;
|
||||
minSize: number;
|
||||
setMinSize: React.Dispatch<React.SetStateAction<number>>;
|
||||
collapsedSize: number;
|
||||
setCollapsedSize: React.Dispatch<React.SetStateAction<number>>;
|
||||
isCollapsed: boolean;
|
||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
fullCollapse: boolean;
|
||||
setFullCollapse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
panelRef: React.RefObject<ImperativePanelHandle>;
|
||||
interfaceConfig: TInterfaceConfig;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [minSize, setMinSize] = useState(defaultMinSize);
|
||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const interfaceConfig = useMemo(
|
||||
() => (startupConfig?.interface ?? defaultInterface) as Partial<TInterfaceConfig>,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const { conversation } = useChatContext();
|
||||
const { endpoint } = conversation ?? {};
|
||||
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
|
||||
|
||||
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
const defaultActive = useMemo(() => {
|
||||
const activePanel = localStorage.getItem('side:active-panel');
|
||||
return typeof activePanel === 'string' ? activePanel : undefined;
|
||||
|
|
@ -113,46 +93,6 @@ const SidePanel = ({
|
|||
interfaceConfig,
|
||||
});
|
||||
|
||||
const calculateLayout = useCallback(() => {
|
||||
if (artifacts == null) {
|
||||
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
||||
return [100 - navSize, navSize];
|
||||
} else {
|
||||
const navSize = 0;
|
||||
const remainingSpace = 100 - navSize;
|
||||
const newMainSize = Math.floor(remainingSpace / 2);
|
||||
const artifactsSize = remainingSpace - newMainSize;
|
||||
return [newMainSize, artifactsSize, navSize];
|
||||
}
|
||||
}, [artifacts, defaultLayout]);
|
||||
|
||||
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const throttledSaveLayout = useCallback(
|
||||
throttle((sizes: number[]) => {
|
||||
const normalizedSizes = normalizeLayout(sizes);
|
||||
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
||||
}, 350),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmallScreen) {
|
||||
setIsCollapsed(true);
|
||||
setCollapsedSize(0);
|
||||
setMinSize(defaultMinSize);
|
||||
setFullCollapse(true);
|
||||
localStorage.setItem('fullPanelCollapse', 'true');
|
||||
panelRef.current?.collapse();
|
||||
return;
|
||||
} else {
|
||||
setIsCollapsed(defaultCollapsed);
|
||||
setCollapsedSize(navCollapsedSize);
|
||||
setMinSize(defaultMinSize);
|
||||
}
|
||||
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
|
||||
|
||||
const toggleNavVisible = useCallback(() => {
|
||||
if (newUser) {
|
||||
setNewUser(false);
|
||||
|
|
@ -173,127 +113,84 @@ const SidePanel = ({
|
|||
}
|
||||
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
|
||||
|
||||
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
||||
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className="relative flex w-px items-center justify-center"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[0]}
|
||||
minSize={minSizeMain}
|
||||
order={1}
|
||||
id="messages-view"
|
||||
>
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
{artifacts != null && (
|
||||
<>
|
||||
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[1]}
|
||||
minSize={minSizeMain}
|
||||
order={2}
|
||||
id="artifacts-panel"
|
||||
>
|
||||
{artifacts}
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className="relative flex w-px items-center justify-center"
|
||||
>
|
||||
<NavToggle
|
||||
navVisible={!isCollapsed}
|
||||
isHovering={isHovering}
|
||||
onToggle={toggleNavVisible}
|
||||
setIsHovering={setIsHovering}
|
||||
className={cn(
|
||||
'fixed top-1/2',
|
||||
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
||||
? 'mr-9'
|
||||
: 'mr-16',
|
||||
)}
|
||||
translateX={false}
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
|
||||
<ResizableHandleAlt withHandle className="bg-transparent text-text-primary" />
|
||||
)}
|
||||
<ResizablePanel
|
||||
tagName="nav"
|
||||
id="controls-nav"
|
||||
order={artifacts != null ? 3 : 2}
|
||||
aria-label={localize('com_ui_controls')}
|
||||
role="region"
|
||||
collapsedSize={collapsedSize}
|
||||
defaultSize={currentLayout[currentLayout.length - 1]}
|
||||
collapsible={true}
|
||||
minSize={minSize}
|
||||
maxSize={40}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
|
||||
}}
|
||||
onExpand={() => {
|
||||
setIsCollapsed(false);
|
||||
localStorage.setItem('react-resizable-panels:collapsed', 'false');
|
||||
}}
|
||||
onCollapse={() => {
|
||||
setIsCollapsed(true);
|
||||
localStorage.setItem('react-resizable-panels:collapsed', 'true');
|
||||
}}
|
||||
<NavToggle
|
||||
navVisible={!isCollapsed}
|
||||
isHovering={isHovering}
|
||||
onToggle={toggleNavVisible}
|
||||
setIsHovering={setIsHovering}
|
||||
className={cn(
|
||||
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
|
||||
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
|
||||
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
||||
? 'hidden min-w-0'
|
||||
: 'opacity-100',
|
||||
'fixed top-1/2',
|
||||
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
||||
? 'mr-9'
|
||||
: 'mr-16',
|
||||
)}
|
||||
>
|
||||
{interfaceConfig.modelSelect === true && (
|
||||
<div
|
||||
className={cn(
|
||||
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
|
||||
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||
)}
|
||||
>
|
||||
<Switcher
|
||||
isCollapsed={isCollapsed}
|
||||
endpointKeyProvided={keyProvided}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Nav
|
||||
resize={panelRef.current?.resize}
|
||||
isCollapsed={isCollapsed}
|
||||
defaultActive={defaultActive}
|
||||
links={Links}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setIsCollapsed(() => {
|
||||
localStorage.setItem('fullPanelCollapse', 'true');
|
||||
setFullCollapse(true);
|
||||
setCollapsedSize(0);
|
||||
setMinSize(0);
|
||||
return false;
|
||||
});
|
||||
panelRef.current?.collapse();
|
||||
translateX={false}
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
|
||||
<ResizableHandleAlt withHandle className="bg-transparent text-text-primary" />
|
||||
)}
|
||||
<ResizablePanel
|
||||
tagName="nav"
|
||||
id="controls-nav"
|
||||
order={hasArtifacts != null ? 3 : 2}
|
||||
aria-label={localize('com_ui_controls')}
|
||||
role="region"
|
||||
collapsedSize={collapsedSize}
|
||||
defaultSize={defaultSize}
|
||||
collapsible={true}
|
||||
minSize={minSize}
|
||||
maxSize={40}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
|
||||
}}
|
||||
/>
|
||||
onExpand={() => {
|
||||
setIsCollapsed(false);
|
||||
localStorage.setItem('react-resizable-panels:collapsed', 'false');
|
||||
}}
|
||||
onCollapse={() => {
|
||||
setIsCollapsed(true);
|
||||
localStorage.setItem('react-resizable-panels:collapsed', 'true');
|
||||
}}
|
||||
className={cn(
|
||||
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
|
||||
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
|
||||
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
||||
? 'hidden min-w-0'
|
||||
: 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{interfaceConfig.modelSelect === true && (
|
||||
<div
|
||||
className={cn(
|
||||
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
|
||||
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||
)}
|
||||
>
|
||||
<Switcher
|
||||
isCollapsed={isCollapsed}
|
||||
endpointKeyProvided={keyProvided}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Nav
|
||||
resize={panelRef.current?.resize}
|
||||
isCollapsed={isCollapsed}
|
||||
defaultActive={defaultActive}
|
||||
links={Links}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
152
client/src/components/SidePanel/SidePanelGroup.tsx
Normal file
152
client/src/components/SidePanel/SidePanelGroup.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { getConfigDefaults } from 'librechat-data-provider';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import { normalizeLayout } from '~/utils';
|
||||
import { useMediaQuery } from '~/hooks';
|
||||
import SidePanel from './SidePanel';
|
||||
import store from '~/store';
|
||||
|
||||
interface SidePanelProps {
|
||||
defaultLayout?: number[] | undefined;
|
||||
defaultCollapsed?: boolean;
|
||||
navCollapsedSize?: number;
|
||||
fullPanelCollapse?: boolean;
|
||||
artifacts?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const defaultMinSize = 20;
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
const SidePanelGroup = ({
|
||||
defaultLayout = [97, 3],
|
||||
defaultCollapsed = false,
|
||||
fullPanelCollapse = false,
|
||||
navCollapsedSize = 3,
|
||||
artifacts,
|
||||
children,
|
||||
}: SidePanelProps) => {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||
const [minSize, setMinSize] = useState(defaultMinSize);
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
||||
|
||||
const calculateLayout = useCallback(() => {
|
||||
if (artifacts == null) {
|
||||
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
||||
return [100 - navSize, navSize];
|
||||
} else {
|
||||
const navSize = 0;
|
||||
const remainingSpace = 100 - navSize;
|
||||
const newMainSize = Math.floor(remainingSpace / 2);
|
||||
const artifactsSize = remainingSpace - newMainSize;
|
||||
return [newMainSize, artifactsSize, navSize];
|
||||
}
|
||||
}, [artifacts, defaultLayout]);
|
||||
|
||||
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
||||
|
||||
const throttledSaveLayout = useCallback(
|
||||
throttle((sizes: number[]) => {
|
||||
const normalizedSizes = normalizeLayout(sizes);
|
||||
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
||||
}, 350),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmallScreen) {
|
||||
setIsCollapsed(true);
|
||||
setCollapsedSize(0);
|
||||
setMinSize(defaultMinSize);
|
||||
setFullCollapse(true);
|
||||
localStorage.setItem('fullPanelCollapse', 'true');
|
||||
panelRef.current?.collapse();
|
||||
return;
|
||||
} else {
|
||||
setIsCollapsed(defaultCollapsed);
|
||||
setCollapsedSize(navCollapsedSize);
|
||||
setMinSize(defaultMinSize);
|
||||
}
|
||||
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
|
||||
|
||||
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
||||
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[0]}
|
||||
minSize={minSizeMain}
|
||||
order={1}
|
||||
id="messages-view"
|
||||
>
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
{artifacts != null && (
|
||||
<>
|
||||
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
|
||||
<ResizablePanel
|
||||
defaultSize={currentLayout[1]}
|
||||
minSize={minSizeMain}
|
||||
order={2}
|
||||
id="artifacts-panel"
|
||||
>
|
||||
{artifacts}
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
||||
<SidePanel
|
||||
panelRef={panelRef}
|
||||
minSize={minSize}
|
||||
setMinSize={setMinSize}
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
collapsedSize={collapsedSize}
|
||||
setCollapsedSize={setCollapsedSize}
|
||||
fullCollapse={fullCollapse}
|
||||
setFullCollapse={setFullCollapse}
|
||||
defaultSize={currentLayout[currentLayout.length - 1]}
|
||||
hasArtifacts={artifacts != null}
|
||||
interfaceConfig={interfaceConfig}
|
||||
/>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setIsCollapsed(() => {
|
||||
localStorage.setItem('fullPanelCollapse', 'true');
|
||||
setFullCollapse(true);
|
||||
setCollapsedSize(0);
|
||||
setMinSize(0);
|
||||
return false;
|
||||
});
|
||||
panelRef.current?.collapse();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SidePanelGroup);
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export { default as SidePanel } from './SidePanel';
|
||||
export { default as SidePanelGroup } from './SidePanelGroup';
|
||||
export { default as SideNav } from './Nav';
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
|
|||
{tool.icon != null && tool.icon ? (
|
||||
<img
|
||||
src={tool.icon}
|
||||
alt={localize('com_ui_logo', tool.name)}
|
||||
alt={localize('com_ui_logo', { 0: tool.name })}
|
||||
className="h-full w-full rounded-[5px] bg-white"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable indent */
|
||||
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function AzureMinimalIcon({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
OGDialogHeader,
|
||||
OGDialogContent,
|
||||
OGDialogDescription,
|
||||
OGDialog,
|
||||
} from './OriginalDialog';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Spinner } from '../svg';
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function SelectDropDownPop({
|
|||
|
||||
return (
|
||||
<Root>
|
||||
<div className={'flex items-center justify-center gap-2 '}>
|
||||
<div className={'flex items-center justify-center gap-2'}>
|
||||
<div className={'relative w-full'}>
|
||||
<Trigger asChild>
|
||||
<button
|
||||
|
|
@ -64,23 +64,24 @@ function SelectDropDownPop({
|
|||
className={cn(
|
||||
'pointer-cursor relative flex flex-col rounded-lg border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
|
||||
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
|
||||
'min-w-[200px] max-w-[215px] sm:min-w-full sm:max-w-full',
|
||||
)}
|
||||
aria-label={`Select ${title}`}
|
||||
aria-haspopup="false"
|
||||
>
|
||||
{' '}
|
||||
{showLabel && (
|
||||
<label className="block text-xs text-gray-700 dark:text-gray-500 ">{title}</label>
|
||||
<label className="block text-xs text-gray-700 dark:text-gray-500">{title}</label>
|
||||
)}
|
||||
<span className="inline-flex w-full ">
|
||||
<span className="inline-flex w-full">
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-6 items-center gap-1 text-sm text-gray-800 dark:text-white',
|
||||
'flex h-6 items-center gap-1 text-sm text-text-primary',
|
||||
!showLabel ? 'text-xs' : '',
|
||||
'min-w-[75px] font-normal',
|
||||
)}
|
||||
>
|
||||
{typeof value !== 'string' && value ? value.label ?? '' : value ?? ''}
|
||||
{typeof value !== 'string' && value ? (value.label ?? '') : (value ?? '')}
|
||||
</span>
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
|
|
@ -91,7 +92,7 @@ function SelectDropDownPop({
|
|||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-gray-400"
|
||||
className="h-4 w-4 text-gray-400"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -107,7 +108,7 @@ function SelectDropDownPop({
|
|||
side="bottom"
|
||||
align="start"
|
||||
className={cn(
|
||||
'mt-2 max-h-[52vh] min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[52vh]',
|
||||
'mr-3 mt-2 max-h-[52vh] w-full max-w-[85vw] overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white sm:max-w-full lg:max-h-[52vh]',
|
||||
hasSearchRender && 'relative',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
import { TError } from 'librechat-data-provider';
|
||||
|
||||
type ProviderValue = {
|
||||
error: TError;
|
||||
error?: TError;
|
||||
setError: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
const ApiErrorBoundaryContext = React.createContext<ProviderValue | undefined>(undefined);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function useSelectAssistant(endpoint: AssistantsEndpoint) {
|
|||
|
||||
const onSelect = useCallback(
|
||||
(value: string) => {
|
||||
const assistant = assistantMap?.[endpoint]?.[value];
|
||||
const assistant = assistantMap[endpoint]?.[value];
|
||||
if (!assistant) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,10 @@ export default function useChatFunctions({
|
|||
endpointType,
|
||||
overrideConvoId,
|
||||
overrideUserMessageId,
|
||||
artifacts: getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode }),
|
||||
artifacts:
|
||||
endpoint !== EModelEndpoint.agents
|
||||
? getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode })
|
||||
: undefined,
|
||||
},
|
||||
convo,
|
||||
) as TEndpointOption;
|
||||
|
|
@ -228,7 +231,6 @@ export default function useChatFunctions({
|
|||
conversationId,
|
||||
unfinished: false,
|
||||
isCreatedByUser: false,
|
||||
isEdited: isEditOrContinue,
|
||||
iconURL: convo?.iconURL,
|
||||
model: convo?.model,
|
||||
error: false,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ export default function useAppStartup({
|
|||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const userPlugins = user.plugins ?? [];
|
||||
|
||||
if (userPlugins.length === 0) {
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ const usePresetIndexOptions: TUsePresetOptions = (_preset) => {
|
|||
}
|
||||
return tool;
|
||||
})
|
||||
?.filter((el) => !!el) || [];
|
||||
.filter((el) => !!el) || [];
|
||||
const isSelected = checkPluginSelection(newValue);
|
||||
const tool = availableTools[newValue];
|
||||
if (isSelected || remove) {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
|||
}
|
||||
return tool;
|
||||
})
|
||||
?.filter((el) => !!el) || [];
|
||||
.filter((el) => !!el) || [];
|
||||
const isSelected = checkPluginSelection(newValue);
|
||||
const tool = availableTools[newValue];
|
||||
if (isSelected || remove) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const useDelayedUploadToast = () => {
|
|||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const message = localize('com_ui_upload_delay', fileName);
|
||||
const message = localize('com_ui_upload_delay', { 0: fileName });
|
||||
showToast({
|
||||
message,
|
||||
status: 'warning',
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue