mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 18:30:15 +01:00
📂 feat: RAG Improvements (#2169)
* feat: new vector file processing strategy * chore: remove unused client files * chore: remove more unused client files * chore: remove more unused client files and move used to new dir * chore(DataIcon): add className * WIP: Model Endpoint Settings Update, draft additional context settings * feat: improve parsing for augmented prompt, add full context option * chore: remove volume mounting from rag.yml as no longer necessary
This commit is contained in:
parent
f427ad792a
commit
45a95acec2
40 changed files with 715 additions and 2046 deletions
|
|
@ -1,80 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { alternateName } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import { DropdownMenuRadioItem } from '~/components';
|
||||
import { SetKeyDialog } from '../SetKeyDialog';
|
||||
import { cn, getEndpointField } from '~/utils';
|
||||
import { Icon } from '~/components/Endpoints';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function ModelItem({
|
||||
endpoint,
|
||||
value,
|
||||
isSelected,
|
||||
}: {
|
||||
endpoint: string;
|
||||
value: string;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const icon = Icon({
|
||||
size: 20,
|
||||
endpoint,
|
||||
error: false,
|
||||
className: 'mr-2',
|
||||
message: false,
|
||||
isCreatedByUser: false,
|
||||
});
|
||||
|
||||
const userProvidesKey: boolean | null | undefined = getEndpointField(
|
||||
endpointsConfig,
|
||||
endpoint,
|
||||
'userProvide',
|
||||
);
|
||||
const localize = useLocalize();
|
||||
|
||||
// regular model
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuRadioItem
|
||||
value={value}
|
||||
className={cn(
|
||||
'group dark:font-semibold dark:text-gray-200 dark:hover:bg-gray-800',
|
||||
isSelected ? 'active bg-gray-50 dark:bg-gray-800' : '',
|
||||
)}
|
||||
id={endpoint}
|
||||
data-testid={`endpoint-item-${endpoint}`}
|
||||
>
|
||||
{icon}
|
||||
{alternateName[endpoint] || endpoint}
|
||||
{endpoint === 'gptPlugins' && (
|
||||
<span className="py-0.25 ml-1 rounded bg-blue-200 px-1 text-[10px] font-semibold text-[#4559A4]">
|
||||
Beta
|
||||
</span>
|
||||
)}
|
||||
<div className="flex w-4 flex-1" />
|
||||
{userProvidesKey ? (
|
||||
<button
|
||||
className={cn(
|
||||
'invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200',
|
||||
isSelected ? 'visible text-gray-700 dark:text-gray-200' : '',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="mr-1 inline-block w-[16px] items-center stroke-1" />
|
||||
{localize('com_endpoint_config_key')}
|
||||
</button>
|
||||
) : null}
|
||||
</DropdownMenuRadioItem>
|
||||
{userProvidesKey && (
|
||||
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import EndpointItem from './EndpointItem';
|
||||
|
||||
interface EndpointItemsProps {
|
||||
endpoints: string[];
|
||||
onSelect: (endpoint: string) => void;
|
||||
selectedEndpoint: string;
|
||||
}
|
||||
|
||||
export default function EndpointItems({ endpoints, selectedEndpoint }: EndpointItemsProps) {
|
||||
return (
|
||||
<>
|
||||
{endpoints.map((endpoint) => (
|
||||
<EndpointItem
|
||||
isSelected={selectedEndpoint === endpoint}
|
||||
key={endpoint}
|
||||
value={endpoint}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,275 +0,0 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
useDeletePresetMutation,
|
||||
useCreatePresetMutation,
|
||||
useGetEndpointsQuery,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { Icon } from '~/components/Endpoints';
|
||||
import EndpointItems from './EndpointItems';
|
||||
import PresetItems from './PresetItems';
|
||||
import FileUpload from './FileUpload';
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
TooltipProvider,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from '~/components/ui/';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import { cn, cleanupPreset, mapEndpoints } from '~/utils';
|
||||
import { useLocalize, useLocalStorage, useConversation, useDefaultConvo } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function NewConversationMenu() {
|
||||
const localize = useLocalize();
|
||||
const getDefaultConversation = useDefaultConvo();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [showPresets, setShowPresets] = useState(true);
|
||||
const [showEndpoints, setShowEndpoints] = useState(true);
|
||||
const [conversation, setConversation] = useRecoilState(store.conversation) ?? {};
|
||||
const [messages, setMessages] = useRecoilState(store.messages);
|
||||
|
||||
const { data: availableEndpoints = [] } = useGetEndpointsQuery({
|
||||
select: mapEndpoints,
|
||||
});
|
||||
|
||||
const [presets, setPresets] = useRecoilState(store.presets);
|
||||
const modularEndpoints = new Set(['gptPlugins', 'anthropic', 'google', 'openAI']);
|
||||
|
||||
const { endpoint } = conversation;
|
||||
const { newConversation } = useConversation();
|
||||
|
||||
const deletePresetsMutation = useDeletePresetMutation();
|
||||
const createPresetMutation = useCreatePresetMutation();
|
||||
|
||||
const importPreset = (jsonData) => {
|
||||
createPresetMutation.mutate(
|
||||
{ ...jsonData },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setPresets(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error uploading the preset:', error);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onFileSelected = (jsonData) => {
|
||||
const jsonPreset = { ...cleanupPreset({ preset: jsonData }), presetId: null };
|
||||
importPreset(jsonPreset);
|
||||
};
|
||||
|
||||
// save states to localStorage
|
||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
||||
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {});
|
||||
const setLastConvo = useLocalStorage('lastConversationSetup', {})[1];
|
||||
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
|
||||
useEffect(() => {
|
||||
if (endpoint && endpoint !== 'bingAI') {
|
||||
const lastModelUpdate = { ...lastModel, [endpoint]: conversation?.model };
|
||||
if (endpoint === 'gptPlugins') {
|
||||
lastModelUpdate.secondaryModel = conversation.agentOptions.model;
|
||||
}
|
||||
setLastModel(lastModelUpdate);
|
||||
} else if (endpoint === 'bingAI') {
|
||||
const { jailbreak, toneStyle } = conversation;
|
||||
setLastBingSettings({ ...lastBingSettings, jailbreak, toneStyle });
|
||||
}
|
||||
|
||||
setLastConvo(conversation);
|
||||
}, [conversation]);
|
||||
|
||||
// set the current model
|
||||
const onSelectEndpoint = (newEndpoint) => {
|
||||
setMenuOpen(false);
|
||||
if (!newEndpoint) {
|
||||
return;
|
||||
} else {
|
||||
newConversation(null, { endpoint: newEndpoint });
|
||||
}
|
||||
};
|
||||
|
||||
// set the current model
|
||||
const isModular = modularEndpoints.has(endpoint);
|
||||
const onSelectPreset = (newPreset) => {
|
||||
setMenuOpen(false);
|
||||
if (!newPreset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isModular &&
|
||||
modularEndpoints.has(newPreset?.endpoint) &&
|
||||
endpoint === newPreset?.endpoint
|
||||
) {
|
||||
const currentConvo = getDefaultConversation({
|
||||
conversation,
|
||||
preset: newPreset,
|
||||
});
|
||||
|
||||
setConversation(currentConvo);
|
||||
setMessages(messages);
|
||||
return;
|
||||
}
|
||||
|
||||
newConversation({}, newPreset);
|
||||
};
|
||||
|
||||
const clearAllPresets = () => {
|
||||
deletePresetsMutation.mutate({ arg: {} });
|
||||
};
|
||||
|
||||
const onDeletePreset = (preset) => {
|
||||
deletePresetsMutation.mutate({ arg: preset });
|
||||
};
|
||||
|
||||
const icon = Icon({
|
||||
size: 32,
|
||||
...conversation,
|
||||
error: false,
|
||||
button: true,
|
||||
});
|
||||
|
||||
const onOpenChange = (open) => {
|
||||
setMenuOpen(open);
|
||||
if (newUser) {
|
||||
setNewUser(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={250}>
|
||||
<Tooltip>
|
||||
<Dialog className="z-[100]">
|
||||
<DropdownMenu open={menuOpen} onOpenChange={onOpenChange}>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
id="new-conversation-menu"
|
||||
data-testid="new-conversation-menu"
|
||||
variant="outline"
|
||||
className={
|
||||
'group relative mb-[-12px] ml-1 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-0 md:ml-[-12px] md:pl-1'
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent forceMount={newUser} sideOffset={5}>
|
||||
{localize('com_endpoint_open_menu')}
|
||||
</TooltipContent>
|
||||
<DropdownMenuContent
|
||||
className="z-[100] w-[375px] dark:bg-gray-800 md:w-96"
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
side="top"
|
||||
>
|
||||
<DropdownMenuLabel
|
||||
className="cursor-pointer dark:text-gray-300"
|
||||
onClick={() => setShowEndpoints((prev) => !prev)}
|
||||
>
|
||||
{showEndpoints ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
|
||||
{localize('com_endpoint')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
value={endpoint}
|
||||
onValueChange={onSelectEndpoint}
|
||||
className="flex flex-col gap-1 overflow-y-auto"
|
||||
>
|
||||
{showEndpoints &&
|
||||
(availableEndpoints.length ? (
|
||||
<EndpointItems
|
||||
selectedEndpoint={endpoint}
|
||||
endpoints={availableEndpoints}
|
||||
onSelect={onSelectEndpoint}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuLabel className="dark:text-gray-300">
|
||||
{localize('com_endpoint_not_available')}
|
||||
</DropdownMenuLabel>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
<div className="mt-2 w-full" />
|
||||
|
||||
<DropdownMenuLabel className="flex items-center dark:text-gray-300">
|
||||
<span
|
||||
className="mr-auto cursor-pointer "
|
||||
onClick={() => setShowPresets((prev) => !prev)}
|
||||
>
|
||||
{showPresets ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
|
||||
{localize('com_endpoint_presets')}
|
||||
</span>
|
||||
<FileUpload onFileSelected={onFileSelected} />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-1 flex w-[22px] items-center"
|
||||
>
|
||||
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0M9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1M6.854 7.146 8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 1 1 .708-.708"></path>
|
||||
</svg>
|
||||
{localize('com_ui_clear')} {localize('com_ui_all')}
|
||||
</label>
|
||||
</DialogTrigger>
|
||||
<DialogTemplate
|
||||
title={`${localize('com_ui_clear')} ${localize('com_endpoint_presets')}`}
|
||||
description={localize('com_endpoint_presets_clear_warning')}
|
||||
selection={{
|
||||
selectHandler: clearAllPresets,
|
||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_clear'),
|
||||
}}
|
||||
className="max-w-[500px]"
|
||||
/>
|
||||
</Dialog>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={onSelectPreset}
|
||||
className={cn(
|
||||
'overflow-y-auto overflow-x-hidden',
|
||||
showEndpoints ? 'max-h-[210px]' : 'max-h-[315px]',
|
||||
)}
|
||||
>
|
||||
{showPresets &&
|
||||
(presets.length ? (
|
||||
<PresetItems
|
||||
presets={presets}
|
||||
onSelect={onSelectPreset}
|
||||
onDeletePreset={onDeletePreset}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuLabel className="dark:text-gray-300">
|
||||
{localize('com_endpoint_no_presets')}
|
||||
</DropdownMenuLabel>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Dialog>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FileUp } from 'lucide-react';
|
||||
import { cn } from '~/utils/';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
type FileUploadProps = {
|
||||
onFileSelected: (jsonData: Record<string, unknown>) => void;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
successText?: string;
|
||||
invalidText?: string;
|
||||
validator?: ((data: Record<string, unknown>) => boolean) | null;
|
||||
text?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const FileUpload: React.FC<FileUploadProps> = ({
|
||||
onFileSelected,
|
||||
className = '',
|
||||
containerClassName = '',
|
||||
successText = null,
|
||||
invalidText = null,
|
||||
validator = null,
|
||||
text = null,
|
||||
id = '1',
|
||||
}) => {
|
||||
const [statusColor, setStatusColor] = useState<string>('text-gray-600');
|
||||
const [status, setStatus] = useState<null | string>(null);
|
||||
const localize = useLocalize();
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const jsonData = JSON.parse(e.target?.result as string);
|
||||
if (validator && !validator(jsonData)) {
|
||||
setStatus('invalid');
|
||||
setStatusColor('text-red-600');
|
||||
return;
|
||||
}
|
||||
|
||||
if (validator) {
|
||||
setStatus('success');
|
||||
setStatusColor('text-green-500 dark:text-green-500');
|
||||
}
|
||||
|
||||
onFileSelected(jsonData);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
let statusText: string;
|
||||
if (!status) {
|
||||
statusText = text ?? localize('com_endpoint_import');
|
||||
} else if (status === 'success') {
|
||||
statusText = successText ?? localize('com_ui_upload_success');
|
||||
} else {
|
||||
statusText = invalidText ?? localize('com_ui_upload_invalid');
|
||||
}
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={`file-upload-${id}`}
|
||||
className={cn(
|
||||
'mr-1 flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal transition-colors hover:bg-gray-100 hover:text-green-600 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-green-500',
|
||||
statusColor,
|
||||
containerClassName,
|
||||
)}
|
||||
>
|
||||
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" />
|
||||
<span className="flex text-xs ">{statusText}</span>
|
||||
<input
|
||||
id={`file-upload-${id}`}
|
||||
value=""
|
||||
type="file"
|
||||
className={cn('hidden ', className)}
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUpload;
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import type { TPresetItemProps } from '~/common';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { DropdownMenuRadioItem, EditIcon, TrashIcon } from '~/components';
|
||||
import { Icon } from '~/components/Endpoints';
|
||||
|
||||
export default function PresetItem({
|
||||
preset = {} as TPreset,
|
||||
value,
|
||||
onChangePreset,
|
||||
onDeletePreset,
|
||||
}: TPresetItemProps) {
|
||||
const { endpoint } = preset;
|
||||
|
||||
const icon = Icon({
|
||||
size: 20,
|
||||
endpoint: preset?.endpoint,
|
||||
model: preset?.model,
|
||||
error: false,
|
||||
className: 'mr-2',
|
||||
isCreatedByUser: false,
|
||||
});
|
||||
|
||||
const getPresetTitle = () => {
|
||||
let _title = `${endpoint}`;
|
||||
const { chatGptLabel, modelLabel, model, jailbreak, toneStyle } = preset;
|
||||
|
||||
if (endpoint === EModelEndpoint.azureOpenAI || endpoint === EModelEndpoint.openAI) {
|
||||
if (model) {
|
||||
_title += `: ${model}`;
|
||||
}
|
||||
if (chatGptLabel) {
|
||||
_title += ` as ${chatGptLabel}`;
|
||||
}
|
||||
} else if (endpoint === EModelEndpoint.google) {
|
||||
if (model) {
|
||||
_title += `: ${model}`;
|
||||
}
|
||||
if (modelLabel) {
|
||||
_title += ` as ${modelLabel}`;
|
||||
}
|
||||
} else if (endpoint === EModelEndpoint.bingAI) {
|
||||
if (toneStyle) {
|
||||
_title += `: ${toneStyle}`;
|
||||
}
|
||||
if (jailbreak) {
|
||||
_title += ' as Sydney';
|
||||
}
|
||||
} else if (endpoint === EModelEndpoint.chatGPTBrowser) {
|
||||
if (model) {
|
||||
_title += `: ${model}`;
|
||||
}
|
||||
} else if (endpoint === EModelEndpoint.gptPlugins) {
|
||||
if (model) {
|
||||
_title += `: ${model}`;
|
||||
}
|
||||
} else if (endpoint === null) {
|
||||
null;
|
||||
} else {
|
||||
null;
|
||||
}
|
||||
return _title;
|
||||
};
|
||||
|
||||
// regular model
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
/* @ts-ignore, value can be an object as well */
|
||||
value={value}
|
||||
className="group flex h-10 max-h-[44px] flex-row justify-between dark:font-semibold dark:text-gray-200 dark:hover:bg-gray-800 sm:h-auto"
|
||||
>
|
||||
<div className="flex items-center justify-start">
|
||||
{icon}
|
||||
<small className="text-[11px]">{preset?.title}</small>
|
||||
<small className="invisible ml-1 flex w-0 flex-shrink text-[10px] sm:visible sm:w-auto">
|
||||
({getPresetTitle()})
|
||||
</small>
|
||||
</div>
|
||||
<div className="flex h-full items-center justify-end">
|
||||
<button
|
||||
className="m-0 mr-1 h-full rounded-md px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:p-2 sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onChangePreset(preset);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
<button
|
||||
className="m-0 h-full rounded-md px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:p-2 sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDeletePreset(preset);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import PresetItem from './PresetItem';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
|
||||
export default function PresetItems({ presets, onSelect, onChangePreset, onDeletePreset }) {
|
||||
return (
|
||||
<>
|
||||
{presets.map((preset: TPreset) => (
|
||||
<PresetItem
|
||||
key={preset?.presetId ?? Math.random()}
|
||||
value={preset}
|
||||
onSelect={onSelect}
|
||||
onChangePreset={onChangePreset}
|
||||
onDeletePreset={onDeletePreset}
|
||||
preset={preset}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as EndpointMenu } from './EndpointMenu';
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
import { Settings2 } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { tPresetSchema, EModelEndpoint } from 'librechat-data-provider';
|
||||
import { PluginStoreDialog } from '~/components';
|
||||
import {
|
||||
PopoverButtons,
|
||||
EndpointSettings,
|
||||
SaveAsPresetDialog,
|
||||
EndpointOptionsPopover,
|
||||
} from '~/components/Endpoints';
|
||||
import { Button } from '~/components/ui';
|
||||
import { cn, cardStyle } from '~/utils/';
|
||||
import { useSetOptions } from '~/hooks';
|
||||
import { ModelSelect } from './ModelSelect';
|
||||
import { GenerationButtons } from './Generations';
|
||||
import store from '~/store';
|
||||
|
||||
export default function OptionsBar() {
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
const messagesTree = useRecoilValue(store.messagesTree);
|
||||
const latestMessage = useRecoilValue(store.latestMessage);
|
||||
const setShowBingToneSetting = useSetRecoilState(store.showBingToneSetting);
|
||||
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
||||
store.showPluginStoreDialog,
|
||||
);
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
||||
const [showPopover, setShowPopover] = useRecoilState(store.showPopover);
|
||||
const [opacityClass, setOpacityClass] = useState('full-opacity');
|
||||
const { setOption } = useSetOptions();
|
||||
|
||||
const { endpoint, conversationId, jailbreak } = conversation ?? {};
|
||||
|
||||
const altConditions: { [key: string]: boolean } = {
|
||||
bingAI: !!(latestMessage && conversation?.jailbreak && endpoint === 'bingAI'),
|
||||
};
|
||||
|
||||
const altSettings: { [key: string]: () => void } = {
|
||||
bingAI: () => setShowBingToneSetting((prev) => !prev),
|
||||
};
|
||||
|
||||
const noSettings = useMemo<{ [key: string]: boolean }>(
|
||||
() => ({
|
||||
[EModelEndpoint.chatGPTBrowser]: true,
|
||||
[EModelEndpoint.bingAI]: jailbreak ? false : conversationId !== 'new',
|
||||
}),
|
||||
[jailbreak, conversationId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPopover) {
|
||||
return;
|
||||
} else if (messagesTree && messagesTree.length >= 1) {
|
||||
setOpacityClass('show');
|
||||
} else {
|
||||
setOpacityClass('full-opacity');
|
||||
}
|
||||
}, [messagesTree, showPopover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (endpoint && noSettings[endpoint]) {
|
||||
setShowPopover(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [endpoint, noSettings]);
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const triggerAdvancedMode = altConditions[endpoint]
|
||||
? altSettings[endpoint]
|
||||
: () => setShowPopover((prev) => !prev);
|
||||
return (
|
||||
<div className="relative py-2 last:mb-2 md:mx-4 md:mb-[-16px] md:py-4 md:pt-2 md:last:mb-6 lg:mx-auto lg:mb-[-32px] lg:max-w-2xl lg:pt-6 xl:max-w-3xl">
|
||||
<GenerationButtons
|
||||
endpoint={endpoint}
|
||||
showPopover={showPopover}
|
||||
opacityClass={opacityClass}
|
||||
/>
|
||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'options-bar z-[61] flex w-full flex-wrap items-center justify-center gap-2',
|
||||
showPopover ? '' : opacityClass,
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
if (showPopover) {
|
||||
return;
|
||||
}
|
||||
setOpacityClass('full-opacity');
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (showPopover) {
|
||||
return;
|
||||
}
|
||||
if (!messagesTree || messagesTree.length === 0) {
|
||||
return;
|
||||
}
|
||||
setOpacityClass('show');
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (showPopover) {
|
||||
return;
|
||||
}
|
||||
setOpacityClass('full-opacity');
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (showPopover) {
|
||||
return;
|
||||
}
|
||||
if (!messagesTree || messagesTree.length === 0) {
|
||||
return;
|
||||
}
|
||||
setOpacityClass('show');
|
||||
}}
|
||||
>
|
||||
<ModelSelect conversation={conversation} setOption={setOption} />
|
||||
{!noSettings[endpoint] && (
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
|
||||
)}
|
||||
onClick={triggerAdvancedMode}
|
||||
>
|
||||
<Settings2 className="w-4 text-gray-600 dark:text-white" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<EndpointOptionsPopover
|
||||
visible={showPopover}
|
||||
saveAsPreset={saveAsPreset}
|
||||
closePopover={() => setShowPopover(false)}
|
||||
PopoverButtons={<PopoverButtons endpoint={endpoint} />}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<EndpointSettings conversation={conversation} setOption={setOption} />
|
||||
</div>
|
||||
</EndpointOptionsPopover>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={tPresetSchema.parse({ ...conversation })}
|
||||
/>
|
||||
<PluginStoreDialog isOpen={showPluginStoreDialog} setIsOpen={setShowPluginStoreDialog} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { object, string } from 'zod';
|
||||
import { AuthKeys } from 'librechat-data-provider';
|
||||
import type { TConfigProps } from '~/common';
|
||||
import FileUpload from '~/components/Input/EndpointMenu/FileUpload';
|
||||
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
|
||||
import { useLocalize, useMultipleKeys } from '~/hooks';
|
||||
import InputWithLabel from './InputWithLabel';
|
||||
import { Label } from '~/components/ui';
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { StopGeneratingIcon } from '~/components';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { SetKeyDialog } from './SetKeyDialog';
|
||||
import { useUserKey, useLocalize, useMediaQuery } from '~/hooks';
|
||||
import { SendMessageIcon } from '~/components/svg';
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui/';
|
||||
|
||||
export default function SubmitButton({
|
||||
conversation,
|
||||
submitMessage,
|
||||
handleStopGenerating,
|
||||
disabled,
|
||||
isSubmitting,
|
||||
userProvidesKey,
|
||||
hasText,
|
||||
}) {
|
||||
const { endpoint } = conversation;
|
||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||
const { checkExpiry } = useUserKey(endpoint);
|
||||
const [isKeyProvided, setKeyProvided] = useState(userProvidesKey ? checkExpiry() : true);
|
||||
const isKeyActive = checkExpiry();
|
||||
const localize = useLocalize();
|
||||
const dots = ['·', '··', '···'];
|
||||
const [dotIndex, setDotIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDotIndex((prevDotIndex) => (prevDotIndex + 1) % dots.length);
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [dots.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userProvidesKey) {
|
||||
setKeyProvided(isKeyActive);
|
||||
} else {
|
||||
setKeyProvided(true);
|
||||
}
|
||||
}, [checkExpiry, endpoint, userProvidesKey, isKeyActive]);
|
||||
|
||||
const clickHandler = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
submitMessage();
|
||||
},
|
||||
[submitMessage],
|
||||
);
|
||||
|
||||
const [isSquareGreen, setIsSquareGreen] = useState(false);
|
||||
|
||||
const setKey = useCallback(() => {
|
||||
setDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
const iconContainerClass = `m-1 mr-0 rounded-md pb-[5px] pl-[6px] pr-[4px] pt-[5px] ${
|
||||
hasText ? (isSquareGreen ? 'bg-green-500' : '') : ''
|
||||
} group-hover:bg-19C37D group-disabled:hover:bg-transparent dark:${
|
||||
hasText ? (isSquareGreen ? 'bg-green-500' : '') : ''
|
||||
} dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent`;
|
||||
|
||||
useEffect(() => {
|
||||
setIsSquareGreen(hasText);
|
||||
}, [hasText]);
|
||||
|
||||
if (isSubmitting && isSmallScreen) {
|
||||
return (
|
||||
<button onClick={handleStopGenerating} type="button">
|
||||
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-200 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-800 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<StopGeneratingIcon />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
} else if (isSubmitting) {
|
||||
return (
|
||||
<div className="relative flex h-full">
|
||||
<div
|
||||
className="absolute text-2xl"
|
||||
style={{ top: '50%', transform: 'translateY(-20%) translateX(-33px)' }}
|
||||
>
|
||||
{dots[dotIndex]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (!isKeyProvided) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={setKey}
|
||||
type="button"
|
||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-auto items-center justify-center bg-transparent pr-1 text-gray-500"
|
||||
>
|
||||
<div className="flex items-center justify-center rounded-md text-xs group-hover:bg-gray-200 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-800 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<div className="m-0 mr-0 flex items-center justify-center rounded-md p-2 sm:p-2">
|
||||
<Settings className="mr-1 inline-block h-auto w-[18px]" />
|
||||
{localize('com_endpoint_config_key_name_placeholder')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{userProvidesKey && (
|
||||
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TooltipProvider delayDuration={250}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
disabled={disabled}
|
||||
data-testid="submit-button"
|
||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className={iconContainerClass}>
|
||||
{hasText ? (
|
||||
<div className="bg-19C37D flex h-[24px] w-[24px] items-center justify-center rounded-full text-white">
|
||||
<SendMessageIcon />
|
||||
</div>
|
||||
) : (
|
||||
<SendMessageIcon />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={-5}>
|
||||
{localize('com_nav_send_message')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import React, { useEffect, useContext, useRef, useState, useCallback } from 'react';
|
||||
|
||||
import { EndpointMenu } from './EndpointMenu';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import OptionsBar from './OptionsBar';
|
||||
import Footer from './Footer';
|
||||
|
||||
import { useMessageHandler, ThemeContext } from '~/hooks';
|
||||
import { cn, getEndpointField } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
interface TextChatProps {
|
||||
isSearchView?: boolean;
|
||||
}
|
||||
|
||||
export default function TextChat({ isSearchView = false }: TextChatProps) {
|
||||
const { ask, isSubmitting, handleStopGenerating, latestMessage, endpointsConfig } =
|
||||
useMessageHandler();
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
const setShowBingToneSetting = useSetRecoilState(store.showBingToneSetting);
|
||||
const [text, setText] = useRecoilState(store.text);
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const isComposing = useRef(false);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [hasText, setHasText] = useState(false);
|
||||
|
||||
// TODO: do we need this?
|
||||
const disabled = false;
|
||||
|
||||
const isNotAppendable = (latestMessage?.unfinished && !isSubmitting) || latestMessage?.error;
|
||||
const { conversationId, jailbreak } = conversation || {};
|
||||
|
||||
// auto focus to input, when entering a conversation.
|
||||
useEffect(() => {
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevents Settings from not showing on a new conversation, also prevents showing toneStyle change without jailbreak
|
||||
if (conversationId === 'new' || !jailbreak) {
|
||||
setShowBingToneSetting(false);
|
||||
}
|
||||
|
||||
if (conversationId !== 'search') {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
// setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [conversationId, jailbreak]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [isSubmitting]);
|
||||
|
||||
const submitMessage = () => {
|
||||
ask({ text });
|
||||
setText('');
|
||||
setHasText(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isComposing?.current) {
|
||||
submitMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.keyCode === 8 && e.currentTarget.value.trim() === '') {
|
||||
setText(e.currentTarget.value);
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && e.shiftKey) {
|
||||
return console.log('Enter + Shift');
|
||||
}
|
||||
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompositionStart = () => {
|
||||
isComposing.current = true;
|
||||
};
|
||||
|
||||
const handleCompositionEnd = () => {
|
||||
isComposing.current = false;
|
||||
};
|
||||
|
||||
const changeHandler = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const { value } = e.target;
|
||||
|
||||
setText(value);
|
||||
updateHasText(value);
|
||||
};
|
||||
|
||||
const updateHasText = useCallback(
|
||||
(text: string) => {
|
||||
setHasText(!!text.trim() || !!latestMessage?.error);
|
||||
},
|
||||
[setHasText, latestMessage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateHasText(text);
|
||||
}, [text, latestMessage, updateHasText]);
|
||||
|
||||
const getPlaceholderText = () => {
|
||||
if (isSearchView) {
|
||||
return 'Click a message title to open its conversation.';
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return 'Choose another model or customize GPT again';
|
||||
}
|
||||
|
||||
if (isNotAppendable) {
|
||||
return 'Edit your message or Regenerate.';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
if (isSearchView) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let isDark = theme === 'dark';
|
||||
|
||||
if (theme === 'system') {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="no-gradient-sm fixed bottom-0 left-0 w-full pt-6 sm:bg-gradient-to-b md:absolute md:w-[calc(100%-.5rem)]"
|
||||
style={{
|
||||
background: `linear-gradient(to bottom,
|
||||
${isDark ? 'rgba(23, 23, 23, 0)' : 'rgba(255, 255, 255, 0)'},
|
||||
${isDark ? 'rgba(23, 23, 23, 0.08)' : 'rgba(255, 255, 255, 0.08)'},
|
||||
${isDark ? 'rgba(23, 23, 23, 0.38)' : 'rgba(255, 255, 255, 0.38)'},
|
||||
${isDark ? 'rgba(23, 23, 23, 1)' : 'rgba(255, 255, 255, 1)'},
|
||||
${isDark ? '#171717' : '#ffffff'})`,
|
||||
}}
|
||||
>
|
||||
<OptionsBar />
|
||||
<div className="input-panel md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient relative w-full border-t bg-white py-2 dark:border-white/20 dark:bg-gray-800 md:border-t-0 md:border-transparent md:bg-transparent md:dark:border-transparent md:dark:bg-transparent">
|
||||
<form className="stretch z-[60] mx-2 flex flex-row gap-3 last:mb-2 md:mx-4 md:pt-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
|
||||
<div className="relative flex h-full flex-1 md:flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-grow flex-row rounded-xl border border-black/10 py-[10px] md:py-4 md:pl-4',
|
||||
'shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]',
|
||||
'dark:border-gray-800/50 dark:text-white',
|
||||
disabled ? 'bg-gray-200 dark:bg-gray-800' : 'bg-white dark:bg-gray-700',
|
||||
)}
|
||||
>
|
||||
<EndpointMenu />
|
||||
<TextareaAutosize
|
||||
// set test id for e2e testing
|
||||
data-testid="text-input"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
|
||||
rows={1}
|
||||
value={disabled || isNotAppendable ? '' : text}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={changeHandler}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
placeholder={getPlaceholderText()}
|
||||
disabled={disabled || isNotAppendable}
|
||||
className="m-0 flex h-auto max-h-52 flex-1 resize-none overflow-auto border-0 bg-transparent p-0 pl-2 pr-12 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-2"
|
||||
/>
|
||||
<SubmitButton
|
||||
conversation={conversation}
|
||||
submitMessage={submitMessage}
|
||||
handleStopGenerating={handleStopGenerating}
|
||||
disabled={disabled || isNotAppendable}
|
||||
isSubmitting={isSubmitting}
|
||||
userProvidesKey={
|
||||
conversation?.endpoint
|
||||
? getEndpointField(endpointsConfig, conversation.endpoint, 'userProvide')
|
||||
: undefined
|
||||
}
|
||||
hasText={hasText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue