feat: add plugin search functionality (#1007)

* feat: add plugin search functionality

* Delete env/conda-meta/history

File deleted

* UI fix and 3 new translations

* fix(PluginStoreDialog) can't select pages

* fix(PluginStoreDialog) select pages fixed. Layout fixed

* update test

* fix(PluginStoreDialog) Fixed count pages

---------

Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
This commit is contained in:
walbercardoso 2023-10-11 17:38:43 -03:00 committed by GitHub
parent bc7a079208
commit 4ac0c04e83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 122 additions and 26 deletions

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Dialog } from '@headlessui/react'; import { Dialog } from '@headlessui/react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { X } from 'lucide-react'; import { Search, X } from 'lucide-react';
import store from '~/store'; import store from '~/store';
import PluginStoreItem from './PluginStoreItem'; import PluginStoreItem from './PluginStoreItem';
import PluginPagination from './PluginPagination'; import PluginPagination from './PluginPagination';
@ -15,6 +15,7 @@ import {
TError, TError,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { useAuthContext } from '~/hooks/AuthContext'; import { useAuthContext } from '~/hooks/AuthContext';
import { useLocalize } from '~/hooks';
type TPluginStoreDialogProps = { type TPluginStoreDialogProps = {
isOpen: boolean; isOpen: boolean;
@ -22,6 +23,7 @@ type TPluginStoreDialogProps = {
}; };
function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) { function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
const localize = useLocalize();
const { data: availablePlugins } = useAvailablePluginsQuery(); const { data: availablePlugins } = useAvailablePluginsQuery();
const { user } = useAuthContext(); const { user } = useAuthContext();
const updateUserPlugins = useUpdateUserPluginsMutation(); const updateUserPlugins = useUpdateUserPluginsMutation();
@ -121,17 +123,20 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
}, },
[itemsPerPage], [itemsPerPage],
); );
const [searchValue, setSearchValue] = useState<string>('');
const filteredPlugins = availablePlugins?.filter((plugin) =>
plugin.name.toLowerCase().includes(searchValue.toLowerCase()),
);
useEffect(() => { useEffect(() => {
if (user) { if (user && user.plugins) {
if (user.plugins) { setUserPlugins(user.plugins);
setUserPlugins(user.plugins);
}
} }
if (availablePlugins) {
setMaxPage(Math.ceil(availablePlugins.length / itemsPerPage)); if (filteredPlugins) {
setMaxPage(Math.ceil(filteredPlugins.length / itemsPerPage));
setCurrentPage(1); // Reset the current page to 1 whenever the filtered list changes
} }
}, [availablePlugins, itemsPerPage, user]); }, [availablePlugins, itemsPerPage, user, searchValue]); // Add searchValue to the dependency list
const handleChangePage = (page: number) => { const handleChangePage = (page: number) => {
setCurrentPage(page); setCurrentPage(page);
@ -143,12 +148,15 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
<div className="fixed inset-0 bg-gray-500/90 transition-opacity dark:bg-gray-800/90" /> <div className="fixed inset-0 bg-gray-500/90 transition-opacity dark:bg-gray-800/90" />
{/* Full-screen container to center the panel */} {/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4"> <div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-900 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"> <Dialog.Panel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-900 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6"> <div className="flex items-center justify-between border-b-[1px] border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
<div className="flex items-center"> <div className="flex items-center">
<div className="text-center sm:text-left"> <div className="text-center sm:text-left">
<Dialog.Title className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200"> <Dialog.Title className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
Plugin store {localize('com_nav_plugin_store')}
</Dialog.Title> </Dialog.Title>
</div> </div>
</div> </div>
@ -169,8 +177,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
className="relative m-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700" className="relative m-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
role="alert" role="alert"
> >
There was an error attempting to authenticate this plugin. Please try again.{' '} {localize('com_nav_plugin_auth_error')} {errorMessage}
{errorMessage}
</div> </div>
)} )}
{showPluginAuthForm && ( {showPluginAuthForm && (
@ -183,12 +190,37 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
)} )}
<div className="p-4 sm:p-6 sm:pt-4"> <div className="p-4 sm:p-6 sm:pt-4">
<div className="mt-4 flex flex-col gap-4"> <div className="mt-4 flex flex-col gap-4">
<div style={{ position: 'relative', display: 'inline-block', width: '250px' }}>
<input
type="text"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder={localize('com_nav_plugin_search')}
style={{
width: '100%',
paddingLeft: '30px',
border: '1px solid #ccc',
borderRadius: '4px', // This rounds the corners
}}
/>
<Search
style={{
position: 'absolute',
left: '10px',
top: '50%',
transform: 'translateY(-50%)',
width: '16px',
height: '16px',
}}
/>
</div>
<div <div
ref={gridRef} ref={gridRef}
className="grid grid-cols-1 grid-rows-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" className="grid grid-cols-1 grid-rows-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
style={{ minHeight: '410px' }}
> >
{availablePlugins && {filteredPlugins &&
availablePlugins filteredPlugins
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage) .slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
.map((plugin, index) => ( .map((plugin, index) => (
<PluginStoreItem <PluginStoreItem
@ -202,14 +234,14 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
</div> </div>
</div> </div>
<div className="mt-2 flex flex-col items-center gap-2 sm:flex-row sm:justify-between"> <div className="mt-2 flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
{maxPage > 1 && ( {maxPage > 0 ? (
<div> <PluginPagination
<PluginPagination currentPage={currentPage}
currentPage={currentPage} maxPage={maxPage}
maxPage={maxPage} onChangePage={handleChangePage}
onChangePage={handleChangePage} />
/> ) : (
</div> <div style={{ height: '21px' }}></div>
)} )}
{/* API not yet implemented: */} {/* API not yet implemented: */}
{/* <div className="flex flex-col items-center gap-2 sm:flex-row"> {/* <div className="flex flex-col items-center gap-2 sm:flex-row">

View file

@ -1,4 +1,4 @@
import { render } from 'test/layout-test-utils'; import { render, screen, fireEvent } from 'test/layout-test-utils';
import PluginStoreDialog from '../PluginStoreDialog'; import PluginStoreDialog from '../PluginStoreDialog';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import * as mockDataProvider from 'librechat-data-provider'; import * as mockDataProvider from 'librechat-data-provider';
@ -202,3 +202,20 @@ test('allows the user to navigate between pages', async () => {
expect(getByText('Wolfram')).toBeInTheDocument(); expect(getByText('Wolfram')).toBeInTheDocument();
expect(getByText('Plugin 1')).toBeInTheDocument(); expect(getByText('Plugin 1')).toBeInTheDocument();
}); });
test('allows the user to search for plugins', async () => {
setup();
const searchInput = screen.getByPlaceholderText('Search plugins');
fireEvent.change(searchInput, { target: { value: 'Google' } });
expect(screen.getByText('Google')).toBeInTheDocument();
expect(screen.queryByText('Wolfram')).not.toBeInTheDocument();
expect(screen.queryByText('Plugin 1')).not.toBeInTheDocument();
fireEvent.change(searchInput, { target: { value: 'Plugin 1' } });
expect(screen.getByText('Plugin 1')).toBeInTheDocument();
expect(screen.queryByText('Google')).not.toBeInTheDocument();
expect(screen.queryByText('Wolfram')).not.toBeInTheDocument();
});

View file

@ -225,6 +225,10 @@ export default {
com_endpoint_config_key_google_service_account: 'Criar uma Conta de Serviço', com_endpoint_config_key_google_service_account: 'Criar uma Conta de Serviço',
com_endpoint_config_key_google_vertex_api_role: com_endpoint_config_key_google_vertex_api_role:
'Certifique-se de clicar em \'Criar e Continuar\' para atribuir pelo menos a função de \'Usuário Vertex AI\'. Por fim, crie uma chave JSON para importar aqui.', 'Certifique-se de clicar em \'Criar e Continuar\' para atribuir pelo menos a função de \'Usuário Vertex AI\'. Por fim, crie uma chave JSON para importar aqui.',
com_nav_plugin_store: 'Loja de plugins',
com_nav_plugin_search: 'Buscar plugins',
com_nav_plugin_auth_error:
'Ocorreu um erro ao tentar autenticar este plugin. Por favor, tente novamente.',
com_nav_export_filename: 'Nome do Arquivo', com_nav_export_filename: 'Nome do Arquivo',
com_nav_export_filename_placeholder: 'Defina o nome do arquivo', com_nav_export_filename_placeholder: 'Defina o nome do arquivo',
com_nav_export_type: 'Tipo', com_nav_export_type: 'Tipo',

View file

@ -168,6 +168,10 @@ export default {
com_endpoint_func_hover: 'Aktiviere die Plugin Funktion für ChatGPT', com_endpoint_func_hover: 'Aktiviere die Plugin Funktion für ChatGPT',
com_endpoint_skip_hover: com_endpoint_skip_hover:
'Aktivieren Sie das Überspringen des Abschlussschritts, der die endgültige Antwort und die generierten Schritte überprüft.', 'Aktivieren Sie das Überspringen des Abschlussschritts, der die endgültige Antwort und die generierten Schritte überprüft.',
com_nav_plugin_store: 'Plugin-Store',
com_nav_plugin_search: 'Suche nach Plugins',
com_nav_plugin_auth_error:
'Beim Versuch, dieses Plugin zu authentifizieren, ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.',
com_nav_export_filename: 'Dateiname', com_nav_export_filename: 'Dateiname',
com_nav_export_filename_placeholder: 'Lege einen Dateinamen fest', com_nav_export_filename_placeholder: 'Lege einen Dateinamen fest',
com_nav_export_type: 'Typ', com_nav_export_type: 'Typ',

View file

@ -226,6 +226,10 @@ export default {
com_endpoint_config_key_google_service_account: 'Create a Service Account', com_endpoint_config_key_google_service_account: 'Create a Service Account',
com_endpoint_config_key_google_vertex_api_role: com_endpoint_config_key_google_vertex_api_role:
'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.', 'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.',
com_nav_plugin_store: 'Plugin store',
com_nav_plugin_search: 'Search plugins',
com_nav_plugin_auth_error:
'There was an error attempting to authenticate this plugin. Please try again.',
com_nav_close_menu: 'Close sidebar', com_nav_close_menu: 'Close sidebar',
com_nav_open_menu: 'Open sidebar', com_nav_open_menu: 'Open sidebar',
com_nav_export_filename: 'Filename', com_nav_export_filename: 'Filename',

View file

@ -230,6 +230,10 @@ export default {
com_endpoint_config_key_google_service_account: 'Crear una Cuenta de Servicio', com_endpoint_config_key_google_service_account: 'Crear una Cuenta de Servicio',
com_endpoint_config_key_google_vertex_api_role: com_endpoint_config_key_google_vertex_api_role:
'Asegúrese de hacer clic en \'Crear y Continuar\' para asignar al menos la función de \'Usuario de Vertex AI\'. Finalmente, cree una clave JSON para importar aquí.', 'Asegúrese de hacer clic en \'Crear y Continuar\' para asignar al menos la función de \'Usuario de Vertex AI\'. Finalmente, cree una clave JSON para importar aquí.',
com_nav_plugin_store: 'Tienda de complementos',
com_nav_plugin_search: 'Buscar complementos',
com_nav_plugin_auth_error:
'Se produjo un error al intentar autenticar este complemento. Por favor, inténtalo de nuevo.',
com_nav_export_filename: 'Nombre del Archivo', com_nav_export_filename: 'Nombre del Archivo',
com_nav_export_filename_placeholder: 'Establece el nombre del archivo', com_nav_export_filename_placeholder: 'Establece el nombre del archivo',
com_nav_export_type: 'Tipo', com_nav_export_type: 'Tipo',

View file

@ -170,6 +170,10 @@ export default {
com_endpoint_func_hover: 'Activer l\'utilisation des plugins comme fonctions OpenAI', com_endpoint_func_hover: 'Activer l\'utilisation des plugins comme fonctions OpenAI',
com_endpoint_skip_hover: com_endpoint_skip_hover:
'Activer le saut de l\'étape de complétion, qui examine la réponse finale et les étapes générées', 'Activer le saut de l\'étape de complétion, qui examine la réponse finale et les étapes générées',
com_nav_plugin_store: 'Boutique de plugins',
com_nav_plugin_search: 'Rechercher des plugins',
com_nav_plugin_auth_error:
'Une erreur s\'est produite lors de la tentative d\'authentification de ce plugin. Veuillez réessayer.',
com_nav_export_filename: 'Nom du fichier', com_nav_export_filename: 'Nom du fichier',
com_nav_export_filename_placeholder: 'Définir le nom du fichier', com_nav_export_filename_placeholder: 'Définir le nom du fichier',
com_nav_export_type: 'Type', com_nav_export_type: 'Type',

View file

@ -226,6 +226,10 @@ export default {
com_endpoint_config_key_google_service_account: 'Crea un account di servizio', com_endpoint_config_key_google_service_account: 'Crea un account di servizio',
com_endpoint_config_key_google_vertex_api_role: com_endpoint_config_key_google_vertex_api_role:
'Assicurati di fare clic su \'Crea e continua\' per dare almeno il ruolo \'Utente Vertex AI\'. Infine, crea una chiave JSON da importare qui.', 'Assicurati di fare clic su \'Crea e continua\' per dare almeno il ruolo \'Utente Vertex AI\'. Infine, crea una chiave JSON da importare qui.',
com_nav_plugin_store: 'Negozio dei plugin',
com_nav_plugin_search: 'Cerca plugin',
com_nav_plugin_auth_error:
'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore, riprova.',
com_nav_close_menu: 'Apri la barra laterale', com_nav_close_menu: 'Apri la barra laterale',
com_nav_open_menu: 'Chiudi la barra laterale', com_nav_open_menu: 'Chiudi la barra laterale',
com_nav_export_filename: 'Nome del file', com_nav_export_filename: 'Nome del file',

View file

@ -223,6 +223,10 @@ export default {
com_endpoint_config_key_google_service_account: 'Service Accountを作成する', com_endpoint_config_key_google_service_account: 'Service Accountを作成する',
com_endpoint_config_key_google_vertex_api_role: com_endpoint_config_key_google_vertex_api_role:
'必ず「作成して続行」をクリックして、最低でも「Vertex AI ユーザー」ロールを与えてください。最後にここにインポートするJSONキーを作成してください。', '必ず「作成して続行」をクリックして、最低でも「Vertex AI ユーザー」ロールを与えてください。最後にここにインポートするJSONキーを作成してください。',
com_nav_plugin_store: 'プラグインストア',
com_nav_plugin_search: 'プラグイン検索',
com_nav_plugin_auth_error:
'このプラグインの認証中にエラーが発生しました。もう一度お試しください。',
com_nav_export_filename: 'ファイル名', com_nav_export_filename: 'ファイル名',
com_nav_export_filename_placeholder: 'ファイル名を入力してください', com_nav_export_filename_placeholder: 'ファイル名を入力してください',
com_nav_export_type: 'タイプ', com_nav_export_type: 'タイプ',

View file

@ -103,9 +103,9 @@ export default {
com_endpoint_bing_to_enable_sydney: '시드니를 활성화하려면', com_endpoint_bing_to_enable_sydney: '시드니를 활성화하려면',
com_endpoint_bing_jailbreak: 'Jailbreak', com_endpoint_bing_jailbreak: 'Jailbreak',
com_endpoint_bing_context_placeholder: com_endpoint_bing_context_placeholder:
"Bing은 '컨텍스트'로 최대 7,000개의 토큰을 사용할 수 있으며, 대화에서 참조할 수 있습니다. 구체적인 제한은 알려져 있지 않지만, 7,000개의 토큰을 초과하면 오류가 발생할 수 있습니다.", 'Bing은 \'컨텍스트\'로 최대 7,000개의 토큰을 사용할 수 있으며, 대화에서 참조할 수 있습니다. 구체적인 제한은 알려져 있지 않지만, 7,000개의 토큰을 초과하면 오류가 발생할 수 있습니다.',
com_endpoint_bing_system_message_placeholder: com_endpoint_bing_system_message_placeholder:
"경고: 이 기능의 오용으로 인해 Bing의 사용이 '금지'될 수 있습니다. 모든 내용을 보려면 '시스템 메시지'를 클릭하세요. 생략된 경우 '시드니' 프리셋이 사용됩니다.", '경고: 이 기능의 오용으로 인해 Bing의 사용이 \'금지\'될 수 있습니다. 모든 내용을 보려면 \'시스템 메시지\'를 클릭하세요. 생략된 경우 \'시드니\' 프리셋이 사용됩니다.',
com_endpoint_system_message: '시스템 메시지', com_endpoint_system_message: '시스템 메시지',
com_endpoint_default_blank: '기본값: 공백', com_endpoint_default_blank: '기본값: 공백',
com_endpoint_default_false: '기본값: false', com_endpoint_default_false: '기본값: false',
@ -209,6 +209,10 @@ export default {
'로그인한 상태에서 개발 도구 또는 확장 프로그램을 사용하여 _U 쿠키의 내용을 복사합니다. 실패하는 경우 다음', '로그인한 상태에서 개발 도구 또는 확장 프로그램을 사용하여 _U 쿠키의 내용을 복사합니다. 실패하는 경우 다음',
com_endpoint_config_key_edge_instructions: '지침', com_endpoint_config_key_edge_instructions: '지침',
com_endpoint_config_key_edge_full_key_string: '전체 쿠키 문자열을 제공하세요', com_endpoint_config_key_edge_full_key_string: '전체 쿠키 문자열을 제공하세요',
com_nav_plugin_store: '플러그인 스토어',
com_nav_plugin_search: '플러그인 검색',
com_nav_plugin_auth_error:
'이 플러그인을 인증하려는 중에 오류가 발생했습니다. 다시 시도해주세요.',
com_nav_export_filename: '파일 이름', com_nav_export_filename: '파일 이름',
com_nav_export_filename_placeholder: '파일 이름을 설정하세요', com_nav_export_filename_placeholder: '파일 이름을 설정하세요',
com_nav_export_type: '유형', com_nav_export_type: '유형',

View file

@ -171,6 +171,10 @@ export default {
com_endpoint_skip_hover: com_endpoint_skip_hover:
'Omijaj etap uzupełnienia sprawdzający ostateczną odpowiedź i generowane kroki', 'Omijaj etap uzupełnienia sprawdzający ostateczną odpowiedź i generowane kroki',
com_endpoint_config_token: 'Token konfiguracji', com_endpoint_config_token: 'Token konfiguracji',
com_nav_plugin_store: 'Sklep z wtyczkami',
com_nav_plugin_search: 'Wyszukiwanie wtyczek',
com_nav_plugin_auth_error:
'Wystąpił błąd podczas próby uwierzytelnienia tej wtyczki. Proszę spróbować ponownie.',
com_nav_export_filename: 'Nazwa pliku', com_nav_export_filename: 'Nazwa pliku',
com_nav_export_filename_placeholder: 'Podaj nazwę pliku', com_nav_export_filename_placeholder: 'Podaj nazwę pliku',
com_nav_export_type: 'Typ', com_nav_export_type: 'Typ',

View file

@ -171,6 +171,10 @@ export default {
com_endpoint_skip_hover: com_endpoint_skip_hover:
'Пропустить этап завершения, который проверяет окончательный ответ и сгенерированные шаги', 'Пропустить этап завершения, который проверяет окончательный ответ и сгенерированные шаги',
com_endpoint_config_token: 'Токен конфигурации', com_endpoint_config_token: 'Токен конфигурации',
com_nav_plugin_store: 'Магазин плагинов',
com_nav_plugin_search: 'Поиск плагинов',
com_nav_plugin_auth_error:
'При попытке аутентификации этого плагина произошла ошибка. Пожалуйста, попробуйте еще раз.',
com_nav_export_filename: 'Имя файла', com_nav_export_filename: 'Имя файла',
com_nav_export_filename_placeholder: 'Установите имя файла', com_nav_export_filename_placeholder: 'Установите имя файла',
com_nav_export_type: 'Тип', com_nav_export_type: 'Тип',

View file

@ -224,6 +224,10 @@ export default {
com_endpoint_config_key_google_service_account: 'Skapa ett tjänstekonto', // Create a Service Account com_endpoint_config_key_google_service_account: 'Skapa ett tjänstekonto', // Create a Service Account
com_endpoint_config_key_google_vertex_api_role: com_endpoint_config_key_google_vertex_api_role:
'Se till att klicka på "Skapa och fortsätt" för att ge åtminstone rollen "Vertex AI-användare". Skapa slutligen en JSON-nyckel att importera här.', // Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role. Lastly, create a JSON key to import here. 'Se till att klicka på "Skapa och fortsätt" för att ge åtminstone rollen "Vertex AI-användare". Skapa slutligen en JSON-nyckel att importera här.', // Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role. Lastly, create a JSON key to import here.
com_nav_plugin_store: 'Pluginbutik',
com_nav_plugin_search: 'Sök efter plugins',
com_nav_plugin_auth_error:
'Det uppstod ett fel när försöket att autentisera denna plugin gjordes. Försök igen.',
com_nav_export_filename: 'Filnamn', // Filename com_nav_export_filename: 'Filnamn', // Filename
com_nav_export_filename_placeholder: 'Ange filnamnet', // Set the filename com_nav_export_filename_placeholder: 'Ange filnamnet', // Set the filename
com_nav_export_type: 'Typ', // Type com_nav_export_type: 'Typ', // Type

View file

@ -167,6 +167,9 @@ export default {
com_endpoint_completion_model: '补全模型 (推荐: GPT-4)', com_endpoint_completion_model: '补全模型 (推荐: GPT-4)',
com_endpoint_func_hover: '将插件当做OpenAI函数使用', com_endpoint_func_hover: '将插件当做OpenAI函数使用',
com_endpoint_skip_hover: '跳过补全步骤, 用于检查最终答案和生成步骤', com_endpoint_skip_hover: '跳过补全步骤, 用于检查最终答案和生成步骤',
com_nav_plugin_store: '插件商店',
com_nav_plugin_search: '搜索插件',
com_nav_plugin_auth_error: '尝试验证此插件时出错。请重试。',
com_endpoint_import: '导入', com_endpoint_import: '导入',
com_endpoint_preset: '预设', com_endpoint_preset: '预设',
com_endpoint_presets: '预设', com_endpoint_presets: '预设',