feat: ChatGPT Plugins/OpenAPI specs for Plugins Endpoint (#620)

* wip: proof of concept for openapi chain

* chore(api): update langchain dependency to version 0.0.105

* feat(Plugins): use ChatGPT Plugins/OpenAPI specs (first pass)

* chore(manifest.json): update pluginKey for "Browser" tool to "web-browser"
chore(handleTools.js): update customConstructor key for "web-browser" tool

* fix(handleSubmit.js): set unfinished property to false for all endpoints

* fix(handlers.js): remove unnecessary capitalizeWords function and use action.tool directly
refactor(endpoints.js): rename availableTools to tools and transform it into a map

* feat(endpoints): add plugins selector to endpoints file
refactor(CodeBlock.tsx): refactor to typescript
refactor(Plugin.tsx): use recoil Map for plugin name and refactor to typescript
chore(Message.jsx): linting
chore(PluginsOptions/index.jsx): remove comment/linting
chore(svg): export Clipboard and CheckMark components from SVG index and refactor to typescript

* fix(OpenAPIPlugin.js): rename readYamlFile function to readSpecFile
fix(OpenAPIPlugin.js): handle JSON files in readSpecFile function
fix(OpenAPIPlugin.js): handle JSON URLs in getSpec function
fix(OpenAPIPlugin.js): handle JSON variables in createOpenAPIPlugin function
fix(OpenAPIPlugin.js): add description for variables in createOpenAPIPlugin function
fix(OpenAPIPlugin.js): add optional flag for is_user_authenticated and has_user_authentication in ManifestDefinition
fix(loadSpecs.js): add optional flag for is_user_authenticated and has_user_authentication in ManifestDefinition
fix(Plugin.tsx): remove unnecessary callback parameter in getPluginName function
fix(getDefaultConversation.js): fix browser console error: handle null value for lastConversationSetup in getDefaultConversation function

* feat(api): add new tools

Add Ai PDF tool for super-fast, interactive chats with PDFs of any size, complete with page references for fact checking.
Add VoxScript tool for searching through YouTube transcripts, financial data sources, Google Search results, and more.
Add WebPilot tool for browsing and QA of webpages, PDFs, and data. Generate articles from one or more URLs.

feat(api): update OpenAPIPlugin.js

- Add support for bearer token authorization in the OpenAPIPlugin.
- Add support for custom headers in the OpenAPIPlugin.

fix(api): fix loadTools.js

- Pass the user parameter to the loadSpecs function.

* feat(PluginsClient.js): import findMessageContent function from utils
feat(PluginsClient.js): add message parameter to options object in initializeCustomAgent function
feat(PluginsClient.js): add content to errorMessage if message content is found
feat(PluginsClient.js): break out of loop if message content is found
feat(PluginsClient.js): add delay option with value of 8 to generateTextStream function
feat(PluginsClient.js): add support for process.env.PORT environment variable in app.listen function
feat(askyourpdf.json): add askyourpdf plugin configuration
feat(metar.json): add metar plugin configuration
feat(askyourpdf.yaml): add askyourpdf plugin OpenAPI specification
feat(OpenAPIPlugin.js): add message parameter to createOpenAPIPlugin function
feat(OpenAPIPlugin.js): add description_for_model to chain run message
feat(addOpenAPISpecs.js): remove verbose option from loadSpecs function call

fix(loadSpecs.js): add 'message' parameter to the loadSpecs function
feat(findMessageContent.js): add utility function to find message content in JSON objects

* fix(PluginStoreDialog.tsx): update z-index value for the dialog container

The z-index value for the dialog container was updated to "102" to ensure it appears above other elements on the page.

* chore(web_pilot.json): add "params" field with "user_has_request" parameter set to true

* chore(eslintrc.js): update eslint rules
fix(Login.tsx): add missing semicolon after import statement

* fix(package-lock.json): update langchain dependency to version ^0.0.105

* fix(OpenAPIPlugin.js): change header key from 'id' to 'librechat_user_id' for consistency and clarity

feat(plugins): add documentation for using official ChatGPT Plugins with OpenAPI specs

This commit adds a new file `chatgpt_plugins_openapi.md` to the `docs/features/plugins` directory. The file provides detailed information on how to use official ChatGPT Plugins with OpenAPI specifications. It explains the components of a plugin, including the Plugin Manifest file and the OpenAPI spec. It also covers the process of adding a plugin, editing manifest files, and customizing OpenAPI spec files. Additionally, the commit includes disclaimers about the limitations and compatibility of plugins with LibreChat. The documentation also clarifies that the use of ChatGPT Plugins with LibreChat does not violate OpenAI's Terms of Service.

The purpose of this commit is to provide comprehensive documentation for developers who want to integrate ChatGPT Plugins into their projects using OpenAPI specs. It aims to guide them through the process of adding and configuring plugins, as well as addressing potential issues and

chore(introduction.md): update link to ChatGPT Plugins documentation
docs(introduction.md): clarify the purpose of the plugins endpoint and its capabilities

* fix(OpenAPIPlugin.js): update SUFFIX variable to provide a clearer description
docs(chatgpt_plugins_openapi.md): update information about adding plugins via url on the frontend

* feat(PluginsClient.js): sendIntermediateMessage on successful Agent load
fix(PluginsClient.js, server/index.js, gptPlugins.js): linting fixes
docs(chatgpt_plugins_openapi.md): update links and add additional information

* Update chatgpt_plugins_openapi.md

* chore: rebuild package-lock file

* chore: format/lint all files with new rules

* chore: format all files

* chore(README.md): update AI model selection list

The AI model selection list in the README.md file has been updated to reflect the current options available. The "Anthropic" model has been added as an alternative name for the "Claude" model.

* fix(Plugin.tsx): type issue

* feat(tools): add new tool WebPilot

feat(tools): remove tool Weather Report

feat(tools): add new tool Prompt Perfect

feat(tools): add new tool Scholarly Graph Link

* feat(OpenAPIPlugin.js): add getSpec and readSpecFile functions
feat(OpenAPIPlugin.spec.js): add tests for readSpecFile, getSpec, and createOpenAPIPlugin functions

* chore(agent-demo-1.js): remove unused code and dependencies
chore(agent-demo-2.js): remove unused code and dependencies
chore(demo.js): remove unused code and dependencies

* feat(addOpenAPISpecs): add function to transform OpenAPI specs into desired format
feat(addOpenAPISpecs.spec): add tests for transformSpec function
fix(loadSpecs): remove debugging code

* feat(loadSpecs.spec.js): add unit tests for ManifestDefinition, validateJson, and loadSpecs functions

* fix: package file resolution bug

* chore: move scholarly_graph_link manifest to 'has-issues'

* refactor(client/hooks): convert to TS and export from index

* Update introduction.md

* Update chatgpt_plugins_openapi.md
This commit is contained in:
Danny Avila 2023-07-16 12:19:47 -04:00 committed by GitHub
parent 39ac8d3858
commit 514f625b8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
165 changed files with 3002 additions and 712 deletions

View file

@ -7,7 +7,7 @@ import { useRecoilValue } from 'recoil';
import store from '~/store';
import { localize } from '~/localization/Translation';
import { useGetStartupConfig } from '@librechat/data-provider';
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components'
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
function Login() {
const { login, error, isAuthenticated } = useAuthContext();
@ -26,7 +26,9 @@ function Login() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
<h1 className="mb-4 text-center text-3xl font-semibold">{localize(lang, 'com_auth_welcome_back')}</h1>
<h1 className="mb-4 text-center text-3xl font-semibold">
{localize(lang, 'com_auth_welcome_back')}
</h1>
{error && (
<div
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
@ -55,12 +57,12 @@ function Login() {
)}
{startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with Google"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/google`}>
href={`${startupConfig.serverDomain}/oauth/google`}
>
<GoogleIcon />
<p>{localize(lang, 'com_auth_google_login')}</p>
</a>
@ -87,12 +89,12 @@ function Login() {
)}
{startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with GitHub"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/github`}>
href={`${startupConfig.serverDomain}/oauth/github`}
>
<GithubIcon />
<p>{localize(lang, 'com_auth_github_login')}</p>
</a>
@ -101,12 +103,12 @@ function Login() {
)}
{startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with Discord"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/discord`}>
href={`${startupConfig.serverDomain}/oauth/discord`}
>
<DiscordIcon />
<p>{localize(lang, 'com_auth_discord_login')}</p>
</a>
@ -116,6 +118,6 @@ function Login() {
</div>
</div>
);
};
}
export default Login;

View file

@ -9,7 +9,7 @@ import {
TRegisterUser,
useGetStartupConfig,
} from '@librechat/data-provider';
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components'
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
function Registration() {
const navigate = useNavigate();
@ -235,7 +235,8 @@ function Registration() {
// return false;
// }}
{...register('confirm_password', {
validate: (value) => value === password || localize(lang, 'com_auth_password_not_match'),
validate: (value) =>
value === password || localize(lang, 'com_auth_password_not_match'),
})}
aria-invalid={!!errors.confirm_password}
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
@ -294,12 +295,12 @@ function Registration() {
)}
{startupConfig?.googleLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with Google"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/google`}>
href={`${startupConfig.serverDomain}/oauth/google`}
>
<GoogleIcon />
<p>{localize(lang, 'com_auth_google_login')}</p>
</a>
@ -326,13 +327,12 @@ function Registration() {
)}
{startupConfig?.githubLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with GitHub"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/github`}>
href={`${startupConfig.serverDomain}/oauth/github`}
>
<GithubIcon />
<p>{localize(lang, 'com_auth_github_login')}</p>
</a>
@ -341,12 +341,12 @@ function Registration() {
)}
{startupConfig?.discordLoginEnabled && startupConfig?.socialLoginEnabled && (
<>
<div className="mt-2 flex gap-x-2">
<a
aria-label="Login with Discord"
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
href={`${startupConfig.serverDomain}/oauth/discord`}>
href={`${startupConfig.serverDomain}/oauth/discord`}
>
<DiscordIcon />
<p>{localize(lang, 'com_auth_discord_login')}</p>
</a>

View file

@ -39,7 +39,9 @@ function RequestPasswordReset() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
<h1 className="mb-4 text-center text-3xl font-semibold">{localize(lang, 'com_auth_reset_password')}</h1>
<h1 className="mb-4 text-center text-3xl font-semibold">
{localize(lang, 'com_auth_reset_password')}
</h1>
{success && (
<div
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700"

View file

@ -32,7 +32,9 @@ function ResetPassword() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
<h1 className="mb-4 text-center text-3xl font-semibold">{localize(lang, 'com_auth_reset_password_success')}</h1>
<h1 className="mb-4 text-center text-3xl font-semibold">
{localize(lang, 'com_auth_reset_password_success')}
</h1>
<div
className="relative mb-8 mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700"
role="alert"
@ -53,7 +55,9 @@ function ResetPassword() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
<h1 className="mb-4 text-center text-3xl font-semibold">{localize(lang, 'com_auth_reset_password')}</h1>
<h1 className="mb-4 text-center text-3xl font-semibold">
{localize(lang, 'com_auth_reset_password')}
</h1>
{resetError && (
<div
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
@ -135,7 +139,8 @@ function ResetPassword() {
return false;
}}
{...register('confirm_password', {
validate: (value) => value === password || localize(lang, 'com_auth_password_not_match'),
validate: (value) =>
value === password || localize(lang, 'com_auth_password_not_match'),
})}
aria-invalid={!!errors.confirm_password}
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"

View file

@ -11,7 +11,7 @@ test('renders login form', () => {
});
test('submits login form', async () => {
const { getByLabelText, getByRole } = render(<Login onSubmit={mockLogin}/>);
const { getByLabelText, getByRole } = render(<Login onSubmit={mockLogin} />);
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
@ -24,7 +24,7 @@ test('submits login form', async () => {
});
test('displays validation error messages', async () => {
const { getByLabelText, getByRole, getByText } = render(<Login onSubmit={mockLogin}/>);
const { getByLabelText, getByRole, getByText } = render(<Login onSubmit={mockLogin} />);
const emailInput = getByLabelText(/email/i);
const passwordInput = getByLabelText(/password/i);
const submitButton = getByRole('button', { name: /Sign in/i });
@ -36,4 +36,3 @@ test('displays validation error messages', async () => {
expect(getByText(/You must enter a valid email address/i)).toBeInTheDocument();
expect(getByText(/Password must be at least 8 characters/i)).toBeInTheDocument();
});

View file

@ -15,7 +15,9 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
useEffect(() => {
if (deleteConvoMutation.isSuccess) {
if (currentConversation?.conversationId == conversationId) newConversation();
if (currentConversation?.conversationId == conversationId) {
newConversation();
}
refreshConversations();
retainView();

View file

@ -12,10 +12,7 @@ const types = {
function OptionHover({ type, side }) {
return (
<HoverCardPortal>
<HoverCardContent
side={side}
className="w-80 "
>
<HoverCardContent side={side} className="w-80 ">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
</div>

View file

@ -168,7 +168,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`${title || 'Edit Preset'} - ${preset?.title}`}
className="max-w-full sm:max-w-4xl h-[675px] "
className="h-[675px] max-w-full sm:max-w-4xl "
main={
<div className="flex w-full flex-col items-center gap-2 md:h-[475px]">
<div className="grid w-full gap-6 sm:grid-cols-2">
@ -227,7 +227,9 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
<div className="w-full p-0">
{shouldShowSettings && <Settings preset={preset} setOption={setOption} />}
{preset?.endpoint === 'google' && showExamples && !preset?.model?.startsWith('codechat-') && (
{preset?.endpoint === 'google' &&
showExamples &&
!preset?.model?.startsWith('codechat-') && (
<Examples
examples={preset.examples}
setExample={setExample}

View file

@ -19,7 +19,7 @@ function EndpointOptionsPopover({
<>
<div
className={
' endpointOptionsPopover-container absolute bottom-[-10px] flex w-full flex-col items-center md:px-4 z-0' +
' endpointOptionsPopover-container absolute bottom-[-10px] z-0 flex w-full flex-col items-center md:px-4' +
(visible ? ' show' : '')
}
>
@ -42,7 +42,10 @@ function EndpointOptionsPopover({
{additionalButton && (
<Button
type="button"
className={cn(additionalButton.buttonClass, 'ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0')}
className={cn(
additionalButton.buttonClass,
'ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0',
)}
onClick={additionalButton.handler}
>
{additionalButton.icon}

View file

@ -44,7 +44,7 @@ function Settings(props) {
const codeChat = model.startsWith('codechat-');
return (
<div className={'md:h-[350px] h-[490px] overflow-y-auto'}>
<div className={'h-[490px] overflow-y-auto md:h-[350px]'}>
<div className="grid gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
@ -55,7 +55,7 @@ function Settings(props) {
disabled={readonly}
className={cn(
defaultTextProps,
'flex w-full z-50 resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
'z-50 flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
)}
containerClassName="flex w-full resize-none"
/>
@ -141,7 +141,7 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
Top P <small className="opacity-40">(default: 0.95)</small>
Top P <small className="opacity-40">(default: 0.95)</small>
</Label>
<InputNumber
id="top-p-int"
@ -179,7 +179,7 @@ function Settings(props) {
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
Top K <small className="opacity-40">(default: 40)</small>
Top K <small className="opacity-40">(default: 40)</small>
</Label>
<InputNumber
id="top-k-int"
@ -212,14 +212,13 @@ function Settings(props) {
</HoverCardTrigger>
<OptionHover type="topk" side="left" />
</HoverCard>
</>
)}
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
</Label>
<InputNumber
id="max-tokens-int"

View file

@ -10,13 +10,9 @@ const types = {
};
function OptionHover({ type, side }) {
return (
<HoverCardPortal>
<HoverCardContent
side={side}
className="w-80 "
>
<HoverCardContent side={side} className="w-80 ">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
</div>

View file

@ -44,7 +44,7 @@ function Settings(props) {
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
return (
<div className="md:h-[350px] h-[490px] overflow-y-auto">
<div className="h-[490px] overflow-y-auto md:h-[350px]">
<div className="grid gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
@ -103,9 +103,7 @@ function Settings(props) {
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
Temperature{' '}
<small className="opacity-40">
(default: {isOpenAI ? '1' : '0'})
</small>
<small className="opacity-40">(default: {isOpenAI ? '1' : '0'})</small>
</Label>
<InputNumber
id="temp-int"

View file

@ -19,14 +19,7 @@ const optionText =
import store from '~/store';
function Settings(props) {
const {
readonly,
agent,
skipCompletion,
model,
temperature,
setOption,
} = props;
const { readonly, agent, skipCompletion, model, temperature, setOption } = props;
const endpoint = 'gptPlugins';
const endpointsConfig = useRecoilValue(store.endpointsConfig);
@ -45,7 +38,7 @@ function Settings(props) {
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
return (
<div className="md:h-[350px] h-[490px] overflow-y-auto">
<div className="h-[490px] overflow-y-auto md:h-[350px]">
<div className="grid gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
@ -62,28 +55,40 @@ function Settings(props) {
containerClassName="flex w-full resize-none"
/>
</div>
<div className="grid w-full items-center gap-2 grid-cols-2">
<div className="grid w-full grid-cols-2 items-center gap-2">
<HoverCard openDelay={500}>
<HoverCardTrigger className='w-[100px]'>
<HoverCardTrigger className="w-[100px]">
<label
htmlFor="functions-agent"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
<small>Use Functions</small>
</label>
<Switch id="functions-agent" checked={agent === 'functions'} onCheckedChange={onCheckedChangeAgent} disabled={readonly} className="mt-2 ml-4"/>
<Switch
id="functions-agent"
checked={agent === 'functions'}
onCheckedChange={onCheckedChangeAgent}
disabled={readonly}
className="ml-4 mt-2"
/>
</HoverCardTrigger>
<OptionHover type="func" side="right" />
</HoverCard>
<HoverCard openDelay={500}>
<HoverCardTrigger className='w-[100px] ml-[-60px]'>
<HoverCardTrigger className="ml-[-60px] w-[100px]">
<label
htmlFor="skip-completion"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
<small>Skip Completion</small>
</label>
<Switch id="skip-completion" checked={skipCompletion === true} onCheckedChange={onCheckedChangeSkip} disabled={readonly} className="mt-2 ml-4"/>
<Switch
id="skip-completion"
checked={skipCompletion === true}
onCheckedChange={onCheckedChangeSkip}
disabled={readonly}
className="ml-4 mt-2"
/>
</HoverCardTrigger>
<OptionHover type="skip" side="right" />
</HoverCard>

View file

@ -47,7 +47,7 @@ function Settings(props) {
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
return (
<div className="md:h-[350px] h-[490px] overflow-y-auto">
<div className="h-[490px] overflow-y-auto md:h-[350px]">
<div className="grid gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
@ -67,7 +67,8 @@ function Settings(props) {
<>
<div className="grid w-full items-center gap-2">
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
Custom Name <small className="opacity-40">(default: empty | disabled with tools)</small>
Custom Name{' '}
<small className="opacity-40">(default: empty | disabled with tools)</small>
</Label>
<Input
id="chatGptLabel"
@ -85,7 +86,8 @@ function Settings(props) {
</div>
<div className="grid w-full items-center gap-2">
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
Prompt Prefix <small className="opacity-40">(default: empty | disabled with tools)</small>
Prompt Prefix{' '}
<small className="opacity-40">(default: empty | disabled with tools)</small>
</Label>
<TextareaAutosize
id="promptPrefix"

View file

@ -1,3 +1,3 @@
export { default as AgentSettings } from './AgentSettings';
export { default as OptionHover } from './OptionHover';
export { default as Settings } from './Settings';
export { default as Settings } from './Settings';

View file

@ -61,7 +61,7 @@ const Settings = ({ preset, ...props }) => {
{...props}
/>
);
} else if (endpoint === 'gptPlugins') {
} else if (endpoint === 'gptPlugins') {
return (
<PluginsSettings
model={preset?.model}

View file

@ -18,8 +18,12 @@ function BingAIOptions({ show }) {
const { endpoint, conversationId } = conversation;
const { toneStyle, context, systemMessage, jailbreak } = conversation;
if (endpoint !== 'bingAI') return null;
if (conversationId !== 'new' && !show) return null;
if (endpoint !== 'bingAI') {
return null;
}
if (conversationId !== 'new' && !show) {
return null;
}
const triggerAdvancedMode = () => setAdvancedMode((prev) => !prev);

View file

@ -11,8 +11,12 @@ function ChatGPTOptions() {
const endpointsConfig = useRecoilValue(store.endpointsConfig);
if (endpoint !== 'chatGPTBrowser') return null;
if (conversationId !== 'new') return null;
if (endpoint !== 'chatGPTBrowser') {
return null;
}
if (conversationId !== 'new') {
return null;
}
const models = endpointsConfig?.['chatGPTBrowser']?.['availableModels'] || [];

View file

@ -29,7 +29,7 @@ export default function ModelItem({ endpoint, value, isSelected }) {
value={value}
className={cn(
'group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800',
isSelected && 'dark:bg-gray-800 bg-gray-50 active',
isSelected && 'active bg-gray-50 dark:bg-gray-800',
)}
id={endpoint}
>

View file

@ -26,7 +26,9 @@ const FileUpload: React.FC<FileUploadProps> = ({
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
if (!file) return;
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {

View file

@ -19,22 +19,38 @@ export default function PresetItem({ preset = {}, value, onChangePreset, onDelet
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
const { chatGptLabel, model } = preset;
if (model) _title += `: ${model}`;
if (chatGptLabel) _title += ` as ${chatGptLabel}`;
if (model) {
_title += `: ${model}`;
}
if (chatGptLabel) {
_title += ` as ${chatGptLabel}`;
}
} else if (endpoint === 'google') {
const { modelLabel, model } = preset;
if (model) _title += `: ${model}`;
if (modelLabel) _title += ` as ${modelLabel}`;
if (model) {
_title += `: ${model}`;
}
if (modelLabel) {
_title += ` as ${modelLabel}`;
}
} else if (endpoint === 'bingAI') {
const { jailbreak, toneStyle } = preset;
if (toneStyle) _title += `: ${toneStyle}`;
if (jailbreak) _title += ' as Sydney';
if (toneStyle) {
_title += `: ${toneStyle}`;
}
if (jailbreak) {
_title += ' as Sydney';
}
} else if (endpoint === 'chatGPTBrowser') {
const { model } = preset;
if (model) _title += `: ${model}`;
if (model) {
_title += `: ${model}`;
}
} else if (endpoint === 'gptPlugins') {
const { model } = preset;
if (model) _title += `: ${model}`;
if (model) {
_title += `: ${model}`;
}
} else if (endpoint === null) {
null;
} else {

View file

@ -155,7 +155,9 @@ export default function NewConversationMenu() {
<Button
id="new-conversation-menu"
variant="outline"
className={'group relative mb-[-12px] ml-0 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-[-12px] md:pl-1'}
className={
'group relative mb-[-12px] ml-0 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-[-12px] md:pl-1'
}
>
{icon}
<span className="max-w-0 overflow-hidden whitespace-nowrap px-0 text-slate-600 transition-all group-hover:max-w-[80px] group-hover:px-2 group-data-[state=open]:max-w-[80px] group-data-[state=open]:px-2 dark:text-slate-300">

View file

@ -59,9 +59,10 @@ function PluginsOptions() {
const triggerAgentSettings = () => setShowAgentSettings((prev) => !prev);
const { endpoint, agentOptions } = conversation;
if (endpoint !== 'gptPlugins') return null;
if (endpoint !== 'gptPlugins') {
return null;
}
const models = endpointsConfig?.['gptPlugins']?.['availableModels'] || [];
// const availableTools = endpointsConfig?.['gptPlugins']?.['availableTools'] || [];
const triggerAdvancedMode = () => setAdvancedMode((prev) => !prev);
@ -74,7 +75,9 @@ function PluginsOptions() {
};
function checkIfSelected(value) {
if (!conversation.tools) return false;
if (!conversation.tools) {
return false;
}
return conversation.tools.find((el) => el.pluginKey === value) ? true : false;
}
@ -130,12 +133,18 @@ function PluginsOptions() {
(!advancedMode ? opacityClass : '')
}
onMouseEnter={() => {
if (advancedMode) return;
if (advancedMode) {
return;
}
setOpacityClass('full-opacity');
}}
onMouseLeave={() => {
if (advancedMode) return;
if (!messagesTree || messagesTree.length === 0) return;
if (advancedMode) {
return;
}
if (!messagesTree || messagesTree.length === 0) {
return;
}
setOpacityClass('show');
}}
>

View file

@ -1,7 +1,7 @@
import React from 'react';
import FileUpload from '../NewConversationMenu/FileUpload';
const GoogleConfig = ({ setToken } : { setToken: React.Dispatch<React.SetStateAction<string>> }) => {
const GoogleConfig = ({ setToken }: { setToken: React.Dispatch<React.SetStateAction<string>> }) => {
return (
<FileUpload
id="googleKey"
@ -16,24 +16,24 @@ const GoogleConfig = ({ setToken } : { setToken: React.Dispatch<React.SetStateAc
if (
!credentials.client_email ||
typeof credentials.client_email !== 'string' ||
credentials.client_email.length <= 2
typeof credentials.client_email !== 'string' ||
credentials.client_email.length <= 2
) {
return false;
}
if (
!credentials.project_id ||
typeof credentials.project_id !== 'string' ||
credentials.project_id.length <= 2
typeof credentials.project_id !== 'string' ||
credentials.project_id.length <= 2
) {
return false;
}
if (
!credentials.private_key ||
typeof credentials.private_key !== 'string' ||
credentials.private_key.length <= 600
typeof credentials.private_key !== 'string' ||
credentials.private_key.length <= 600
) {
return false;
}

View file

@ -1,6 +1,6 @@
import React from 'react';
function HelpText({ endpoint } : { endpoint: string }) {
function HelpText({ endpoint }: { endpoint: string }) {
const textMap = {
bingAI: (
<small className="break-all text-gray-600">
@ -11,7 +11,7 @@ function HelpText({ endpoint } : { endpoint: string }) {
rel="noreferrer"
className="text-blue-600 underline"
>
https://www.bing.com
https://www.bing.com
</a>
{`. Use dev tools or an extension while logged into the site to copy the content of the _U cookie.
If this fails, follow these `}
@ -21,7 +21,7 @@ function HelpText({ endpoint } : { endpoint: string }) {
rel="noreferrer"
className="text-blue-600 underline"
>
instructions
instructions
</a>
{' to provide the full cookie strings.'}
</small>
@ -35,48 +35,47 @@ function HelpText({ endpoint } : { endpoint: string }) {
rel="noreferrer"
className="text-blue-600 underline"
>
https://chat.openai.com
https://chat.openai.com
</a>
, then visit{' '}
, then visit{' '}
<a
target="_blank"
href="https://chat.openai.com/api/auth/session"
rel="noreferrer"
className="text-blue-600 underline"
>
https://chat.openai.com/api/auth/session
https://chat.openai.com/api/auth/session
</a>
. Copy access token.
. Copy access token.
</small>
),
google: (
<small className="break-all text-gray-600">
You need to{' '}
You need to{' '}
<a
target="_blank"
href="https://console.cloud.google.com/vertex-ai"
rel="noreferrer"
className="text-blue-600 underline"
>
Enable Vertex AI
Enable Vertex AI
</a>{' '}
API on Google Cloud, then{' '}
API on Google Cloud, then{' '}
<a
target="_blank"
href="https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1"
rel="noreferrer"
className="text-blue-600 underline"
>
Create a Service Account
Create a Service Account
</a>
{`. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
Lastly, create a JSON key to import here.`}
</small>
),
};
return textMap[endpoint] || null;
};
}
export default React.memo(HelpText);
export default React.memo(HelpText);

View file

@ -32,6 +32,6 @@ const InputWithLabel: FC<InputWithLabelProps> = ({ value, onChange, label, id })
/>
</>
);
}
};
export default InputWithLabel;

View file

@ -21,7 +21,7 @@ type OpenAIConfigProps = {
endpoint: string;
};
const OpenAIConfig = ({ token, setToken, endpoint } : OpenAIConfigProps) => {
const OpenAIConfig = ({ token, setToken, endpoint }: OpenAIConfigProps) => {
const [showPanel, setShowPanel] = useState(endpoint === 'azureOpenAI');
const { getToken } = store.useToken(endpoint);
@ -64,7 +64,7 @@ const OpenAIConfig = ({ token, setToken, endpoint } : OpenAIConfigProps) => {
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e: { target: { value: any; }; }) => setToken(e.target.value || '')}
onChange={(e: { target: { value: any } }) => setToken(e.target.value || '')}
label={'OpenAI API Key'}
/>
</>
@ -73,28 +73,36 @@ const OpenAIConfig = ({ token, setToken, endpoint } : OpenAIConfigProps) => {
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('azureOpenAIApiInstanceName') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiInstanceName', e.target.value || '')}
onChange={(e: { target: { value: any } }) =>
setAzure('azureOpenAIApiInstanceName', e.target.value || '')
}
label={'Azure OpenAI Instance Name'}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('azureOpenAIApiDeploymentName') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiDeploymentName', e.target.value || '')}
onChange={(e: { target: { value: any } }) =>
setAzure('azureOpenAIApiDeploymentName', e.target.value || '')
}
label={'Azure OpenAI Deployment Name'}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('azureOpenAIApiVersion') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiVersion', e.target.value || '')}
onChange={(e: { target: { value: any } }) =>
setAzure('azureOpenAIApiVersion', e.target.value || '')
}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('azureOpenAIApiKey') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiKey', e.target.value || '')}
onChange={(e: { target: { value: any } }) =>
setAzure('azureOpenAIApiKey', e.target.value || '')
}
label={'Azure OpenAI API Key'}
/>
</>

View file

@ -6,7 +6,7 @@ type ConfigProps = {
setToken: React.Dispatch<React.SetStateAction<string>>;
};
const OtherConfig = ({ token, setToken } : ConfigProps) => {
const OtherConfig = ({ token, setToken }: ConfigProps) => {
return (
<InputWithLabel
id={'chatGPTLabel'}

View file

@ -17,11 +17,11 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
};
const endpointComponents = {
'google': GoogleConfig,
'openAI': OpenAIConfig,
'azureOpenAI': OpenAIConfig,
'gptPlugins': OpenAIConfig,
'default': OtherConfig,
google: GoogleConfig,
openAI: OpenAIConfig,
azureOpenAI: OpenAIConfig,
gptPlugins: OpenAIConfig,
default: OtherConfig,
};
const EndpointComponent = endpointComponents[endpoint] || endpointComponents['default'];
@ -32,11 +32,11 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
title={`Set Token for ${alternateName[endpoint] ?? endpoint}`}
main={
<div className="grid w-full items-center gap-2">
<EndpointComponent token={token} setToken={setToken} endpoint={endpoint}/>
<EndpointComponent token={token} setToken={setToken} endpoint={endpoint} />
<small className="text-red-600">
Your token will be sent to the server, but not saved.
Your token will be sent to the server, but not saved.
</small>
<HelpText endpoint={endpoint}/>
<HelpText endpoint={endpoint} />
</div>
}
selection={{

View file

@ -1 +1 @@
export { default as SetTokenDialog } from './SetTokenDialog';
export { default as SetTokenDialog } from './SetTokenDialog';

View file

@ -39,7 +39,7 @@ export default function SubmitButton({
</div>
</button>
);
} else if (!isTokenProvided && (!endpointsToHideSetTokens.has(endpoint))) {
} else if (!isTokenProvided && !endpointsToHideSetTokens.has(endpoint)) {
return (
<>
<button

View file

@ -131,7 +131,9 @@ export default function TextChat({ isSearchView = false }) {
setShowBingToneSetting((show) => !show);
};
if (isSearchView) return <></>;
if (isSearchView) {
return <></>;
}
return (
<>

View file

@ -81,7 +81,7 @@ export default function MessageHandler() {
const createdHandler = (data, submission) => {
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
if (isRegenerate) {
setMessages([
...messages,
{
@ -91,7 +91,7 @@ export default function MessageHandler() {
submitting: true,
},
]);
else
} else {
setMessages([
...messages,
message,
@ -102,6 +102,7 @@ export default function MessageHandler() {
submitting: true,
},
]);
}
const { conversationId } = message;
setConversation((prevState) => ({
@ -184,8 +185,12 @@ export default function MessageHandler() {
};
useEffect(() => {
if (submission === null) return;
if (Object.keys(submission).length === 0) return;
if (submission === null) {
return;
}
if (Object.keys(submission).length === 0) {
return;
}
let { message } = submission;
@ -213,7 +218,9 @@ export default function MessageHandler() {
} else {
let text = data.text || data.response;
let { initial, plugin } = data;
if (initial) console.log(data);
if (initial) {
console.log(data);
}
if (data.message) {
messageHandler(text, { ...submission, plugin, message });

View file

@ -1,10 +1,15 @@
import React, { useRef, useState } from 'react';
import Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
import React, { useRef, useState, RefObject } from 'react';
import { Clipboard, CheckMark } from '~/components';
import { InfoIcon } from 'lucide-react';
import { cn } from '~/utils/';
const CodeBar = React.memo(({ lang, codeRef, plugin = null }) => {
interface CodeBarProps {
lang: string;
codeRef: RefObject<HTMLElement>;
plugin?: boolean;
}
const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, plugin = null }) => {
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-800 px-4 py-2 font-sans text-xs text-gray-200">
@ -16,11 +21,12 @@ const CodeBar = React.memo(({ lang, codeRef, plugin = null }) => {
className="ml-auto flex gap-2"
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString)
if (codeString) {
navigator.clipboard.writeText(codeString).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 3000);
});
}
}}
>
{isCopied ? (
@ -40,13 +46,25 @@ const CodeBar = React.memo(({ lang, codeRef, plugin = null }) => {
);
});
const CodeBlock = ({ lang, codeChildren, classProp = '', plugin = null }) => {
const codeRef = useRef(null);
interface CodeBlockProps {
lang: string;
codeChildren: string;
classProp?: string;
plugin?: boolean;
}
const CodeBlock: React.FC<CodeBlockProps> = ({
lang,
codeChildren,
classProp = '',
plugin = null,
}) => {
const codeRef = useRef<HTMLElement>(null);
const language = plugin ? 'json' : lang;
return (
<div className="rounded-md bg-black">
<CodeBar lang={lang} codeRef={codeRef} plugin={plugin} />
<CodeBar lang={lang} codeRef={codeRef} plugin={!!plugin} />
<div className={cn(classProp, 'overflow-y-auto p-4')}>
<code ref={codeRef} className={`hljs !whitespace-pre language-${language}`}>
{codeChildren}

View file

@ -4,7 +4,7 @@ import ReactMarkdown from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub'
import supersub from 'remark-supersub';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import CodeBlock from './CodeBlock';

View file

@ -19,9 +19,15 @@ export default function HoverButtons({
const branchingSupported =
// azureOpenAI, openAI, chatGPTBrowser support branching, so edit enabled // 5/21/23: Bing is allowing editing and Message regenerating
!!['azureOpenAI', 'openAI', 'chatGPTBrowser', 'google', 'bingAI', 'gptPlugins', 'anthropic'].find(
(e) => e === endpoint,
);
!![
'azureOpenAI',
'openAI',
'chatGPTBrowser',
'google',
'bingAI',
'gptPlugins',
'anthropic',
].find((e) => e === endpoint);
// Sydney in bingAI supports branching, so edit enabled
const editEnabled =

View file

@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import copy from 'copy-to-clipboard';
import Plugin from './Plugin.jsx';
import Plugin from './Plugin';
import SubRow from './Content/SubRow';
import Content from './Content/Content';
import MultiMessage from './MultiMessage';
@ -78,9 +78,10 @@ export default function Message({
model: message?.model || conversation?.model,
});
if (!isCreatedByUser)
if (!isCreatedByUser) {
props.className =
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-gray-1000';
}
if (message.bg && searchResult) {
props.className = message.bg.split('hover')[0];
@ -101,7 +102,9 @@ export default function Message({
};
const regenerateMessage = () => {
if (!isSubmitting && !message?.isCreatedByUser) regenerate(message);
if (!isSubmitting && !message?.isCreatedByUser) {
regenerate(message);
}
};
const copyToClipboard = (setIsCopied) => {
@ -114,7 +117,9 @@ export default function Message({
};
const clickSearchResult = async () => {
if (!searchResult) return;
if (!searchResult) {
return;
}
getConversationQuery.refetch(message.conversationId).then((response) => {
switchToConversation(response.data);
});

View file

@ -15,8 +15,7 @@ const MessageHeader = ({ isSearchView = false }) => {
const { model } = conversation;
const plugins = (
<>
<Plugin />{' '}
<span className="px-1"></span>
<Plugin /> <span className="px-1"></span>
<span className="py-0.25 ml-1 rounded bg-blue-200 px-1 text-[10px] font-semibold uppercase text-[#4559A4]">
beta
</span>
@ -26,25 +25,40 @@ const MessageHeader = ({ isSearchView = false }) => {
);
const getConversationTitle = () => {
if (isSearchView) return `Search: ${searchQuery}`;
else {
if (isSearchView) {
return `Search: ${searchQuery}`;
} else {
let _title = `${alternateName[endpoint] ?? endpoint}`;
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
const { chatGptLabel } = conversation;
if (model) _title += `: ${model}`;
if (chatGptLabel) _title += ` as ${chatGptLabel}`;
if (model) {
_title += `: ${model}`;
}
if (chatGptLabel) {
_title += ` as ${chatGptLabel}`;
}
} else if (endpoint === 'google') {
_title = 'PaLM';
const { modelLabel, model } = conversation;
if (model) _title += `: ${model}`;
if (modelLabel) _title += ` as ${modelLabel}`;
if (model) {
_title += `: ${model}`;
}
if (modelLabel) {
_title += ` as ${modelLabel}`;
}
} else if (endpoint === 'bingAI') {
const { jailbreak, toneStyle } = conversation;
if (toneStyle) _title += `: ${toneStyle}`;
if (jailbreak) _title += ' as Sydney';
if (toneStyle) {
_title += `: ${toneStyle}`;
}
if (jailbreak) {
_title += ' as Sydney';
}
} else if (endpoint === 'chatGPTBrowser') {
if (model) _title += `: ${model}`;
if (model) {
_title += `: ${model}`;
}
} else if (endpoint === 'gptPlugins') {
return plugins;
} else if (endpoint === 'anthropic') {
@ -62,7 +76,7 @@ const MessageHeader = ({ isSearchView = false }) => {
<>
<div
className={cn(
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-500 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:hover:bg-opacity-100 dark:text-gray-500',
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-500 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:text-gray-500 dark:hover:bg-gray-600 dark:hover:bg-opacity-100',
isNotClickable ? '' : 'cursor-pointer ',
)}
onClick={() => (isNotClickable ? null : setSaveAsDialogShow(true))}

View file

@ -37,7 +37,7 @@ export default function MultiMessage({
}
const message = messagesTree[messagesTree.length - siblingIdx - 1];
if (isSearchView)
if (isSearchView) {
return (
<>
{messagesTree
@ -57,6 +57,7 @@ export default function MultiMessage({
: null}
</>
);
}
return (
<Message
key={message.messageId}

View file

@ -1,92 +0,0 @@
import { useState } from 'react';
import { Spinner } from '~/components';
import CodeBlock from './Content/CodeBlock.jsx';
import { Disclosure } from '@headlessui/react';
import { ChevronDownIcon } from 'lucide-react';
import { cn } from '~/utils/';
function formatInputs(inputs) {
let output = '';
for (let i = 0; i < inputs.length; i++) {
output += `${inputs[i].inputStr}`;
if (inputs.length > 1 && i !== inputs.length - 1) {
output += ',\n';
}
}
return output;
}
export default function Plugin({ plugin }) {
const [loading, setLoading] = useState(plugin.loading);
const finished = plugin.outputs && plugin.outputs.length > 0;
if (!plugin.latest || (plugin.latest && plugin.latest.toLowerCase() === 'n/a')) {
return null;
}
if (finished && loading) {
setLoading(false);
}
const generateStatus = () => {
if (!loading && plugin.latest === 'Self Reflection') {
return 'Finished';
} else if (plugin.latest === 'Self Reflection') {
return 'I\'m thinking...';
} else {
return (
<>
{loading ? 'Using' : 'Used'} <b>{plugin.latest}</b>
{loading ? '...' : ''}
</>
);
}
};
return (
<div className="flex flex-col items-start">
<Disclosure>
{({ open }) => (
<>
<div
className={cn(
loading ? 'bg-green-100' : 'bg-[#ECECF1]',
'flex items-center rounded p-3 text-sm text-gray-900',
)}
>
<div>
<div className="flex items-center gap-3">
<div>{generateStatus()}</div>
</div>
</div>
{loading && <Spinner className="ml-1" />}
<Disclosure.Button className="ml-12 flex items-center gap-2">
<ChevronDownIcon className={cn(open ? 'rotate-180 transform' : '', 'h-4 w-4')} />
</Disclosure.Button>
</div>
<Disclosure.Panel className="my-3 flex max-w-full flex-col gap-3">
<CodeBlock
lang={plugin.latest?.toUpperCase() || 'INPUTS'}
codeChildren={formatInputs(plugin.inputs)}
plugin={true}
classProp="max-h-[450px]"
/>
{finished && (
<CodeBlock
lang="OUTPUTS"
codeChildren={plugin.outputs}
plugin={true}
classProp="max-h-[450px]"
/>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
);
}

View file

@ -0,0 +1,146 @@
import React, { useState, useCallback, memo, ReactNode } from 'react';
import { Spinner } from '~/components';
import { useRecoilValue } from 'recoil';
import CodeBlock from './Content/CodeBlock.jsx';
import { Disclosure } from '@headlessui/react';
import { ChevronDownIcon, LucideProps } from 'lucide-react';
import { cn } from '~/utils/';
import store from '~/store';
interface Input {
inputStr: string;
}
interface PluginProps {
plugin: {
plugin: string;
input: string;
thought: string;
loading?: boolean;
outputs?: string;
latest?: string;
inputs?: Input[];
};
}
type PluginsMap = {
[pluginKey: string]: string;
};
type PluginIconProps = LucideProps & {
className?: string;
};
function formatInputs(inputs: Input[]) {
let output = '';
for (let i = 0; i < inputs.length; i++) {
output += `${inputs[i].inputStr}`;
if (inputs.length > 1 && i !== inputs.length - 1) {
output += ',\n';
}
}
return output;
}
const Plugin: React.FC<PluginProps> = ({ plugin }) => {
const [loading, setLoading] = useState(plugin.loading);
const finished = plugin.outputs && plugin.outputs.length > 0;
const plugins: PluginsMap = useRecoilValue(store.plugins);
const getPluginName = useCallback(
(pluginKey: string) => {
if (!pluginKey) {
return null;
}
if (pluginKey === 'n/a' || pluginKey === 'self reflection') {
return pluginKey;
}
return plugins[pluginKey] ?? 'self reflection';
},
[plugins],
);
if (!plugin || !plugin.latest) {
return null;
}
const latestPlugin = getPluginName(plugin.latest);
if (!latestPlugin || (latestPlugin && latestPlugin === 'n/a')) {
return null;
}
if (finished && loading) {
setLoading(false);
}
const generateStatus = (): ReactNode => {
if (!loading && latestPlugin === 'self reflection') {
return 'Finished';
} else if (latestPlugin === 'self reflection') {
return 'I\'m thinking...';
} else {
return (
<>
{loading ? 'Using' : 'Used'} <b>{latestPlugin}</b>
{loading ? '...' : ''}
</>
);
}
};
return (
<div className="flex flex-col items-start">
<Disclosure>
{({ open }) => {
const iconProps: PluginIconProps = {
className: cn(open ? 'rotate-180 transform' : '', 'h-4 w-4'),
};
return (
<>
<div
className={cn(
loading ? 'bg-green-100' : 'bg-[#ECECF1]',
'flex items-center rounded p-3 text-sm text-gray-900',
)}
>
<div>
<div className="flex items-center gap-3">
<div>{generateStatus()}</div>
</div>
</div>
{loading && <Spinner className="ml-1" />}
<Disclosure.Button className="ml-12 flex items-center gap-2">
<ChevronDownIcon {...iconProps} />
</Disclosure.Button>
</div>
<Disclosure.Panel className="my-3 flex max-w-full flex-col gap-3">
<CodeBlock
lang={latestPlugin?.toUpperCase() || 'INPUTS'}
codeChildren={formatInputs(plugin.inputs ?? [])}
plugin={true}
classProp="max-h-[450px]"
/>
{finished && (
<CodeBlock
lang="OUTPUTS"
codeChildren={plugin.outputs ?? ''}
plugin={true}
classProp="max-h-[450px]"
/>
)}
</Disclosure.Panel>
</>
);
}}
</Disclosure>
</div>
);
};
export default memo(Plugin);

View file

@ -89,7 +89,7 @@ export default function Messages({ isSearchView = false }) {
<div className="dark:gpt-dark-gray flex h-auto flex-col items-center text-sm">
<MessageHeader isSearchView={isSearchView} />
{_messagesTree === null ? (
<div className="h-screen flex items-center justify-center">
<div className="flex h-screen items-center justify-center">
<Spinner />
</div>
) : _messagesTree?.length == 0 && isSearchView ? (

View file

@ -3,7 +3,15 @@ import { useRecoilValue, useRecoilCallback } from 'recoil';
import filenamify from 'filenamify';
import exportFromJSON from 'export-from-json';
import download from 'downloadjs';
import { Dialog, DialogButton, DialogTemplate, Input, Label, Checkbox, Dropdown } from '~/components/ui/';
import {
Dialog,
DialogButton,
DialogTemplate,
Input,
Label,
Checkbox,
Dropdown,
} from '~/components/ui/';
import { cn } from '~/utils/';
import { useScreenshot } from '~/utils/screenshotContext';
@ -70,9 +78,9 @@ export default function ExportModel({ open, onOpenChange }) {
recursive = false,
}) => {
let children = [];
if (messages?.length)
if (branches)
for (const message of messages)
if (messages?.length) {
if (branches) {
for (const message of messages) {
children.push(
await buildMessageTree({
messageId: message?.messageId,
@ -82,7 +90,8 @@ export default function ExportModel({ open, onOpenChange }) {
recursive,
}),
);
else {
}
} else {
let message = messages[0];
if (messages?.length > 1) {
const siblingIdx = await getSiblingIdx(messageId);
@ -99,16 +108,20 @@ export default function ExportModel({ open, onOpenChange }) {
}),
];
}
}
if (recursive) return { ...message, children: children };
else {
if (recursive) {
return { ...message, children: children };
} else {
let ret = [];
if (message) {
let _message = { ...message };
delete _message.children;
ret = [_message];
}
for (const child of children) ret = ret.concat(child);
for (const child of children) {
ret = ret.concat(child);
}
return ret;
}
};
@ -204,9 +217,15 @@ export default function ExportModel({ open, onOpenChange }) {
data += '\n## History\n';
for (const message of messages) {
data += `**${message?.sender}:**\n${message?.text}\n`;
if (message.error) data += '*(This is an error message)*\n';
if (message.unfinished) data += '*(This is an unfinished message)*\n';
if (message.cancelled) data += '*(This is a cancelled message)*\n';
if (message.error) {
data += '*(This is an error message)*\n';
}
if (message.unfinished) {
data += '*(This is an unfinished message)*\n';
}
if (message.cancelled) {
data += '*(This is a cancelled message)*\n';
}
data += '\n\n';
}
@ -247,9 +266,15 @@ export default function ExportModel({ open, onOpenChange }) {
data += '\nHistory\n########################\n';
for (const message of messages) {
data += `>> ${message?.sender}:\n${message?.text}\n`;
if (message.error) data += '(This is an error message)\n';
if (message.unfinished) data += '(This is an unfinished message)\n';
if (message.cancelled) data += '(This is a cancelled message)\n';
if (message.error) {
data += '(This is an error message)\n';
}
if (message.unfinished) {
data += '(This is an unfinished message)\n';
}
if (message.cancelled) {
data += '(This is a cancelled message)\n';
}
data += '\n\n';
}
@ -271,7 +296,9 @@ export default function ExportModel({ open, onOpenChange }) {
recursive: recursive,
};
if (includeOptions) data.options = cleanupPreset({ preset: conversation, endpointsConfig });
if (includeOptions) {
data.options = cleanupPreset({ preset: conversation, endpointsConfig });
}
const messages = await buildMessageTree({
messageId: conversation?.conversationId,
@ -281,8 +308,11 @@ export default function ExportModel({ open, onOpenChange }) {
recursive: recursive,
});
if (recursive) data.messagesTree = messages.children;
else data.messages = messages;
if (recursive) {
data.messagesTree = messages.children;
} else {
data.messages = messages;
}
exportFromJSON({
data: data,
@ -293,11 +323,17 @@ export default function ExportModel({ open, onOpenChange }) {
};
const exportConversation = () => {
if (type === 'json') exportJSON();
else if (type == 'text') exportText();
else if (type == 'markdown') exportMarkdown();
else if (type == 'csv') exportCSV();
else if (type == 'screenshot') exportScreenshot();
if (type === 'json') {
exportJSON();
} else if (type == 'text') {
exportText();
} else if (type == 'markdown') {
exportMarkdown();
} else if (type == 'csv') {
exportCSV();
} else if (type == 'screenshot') {
exportScreenshot();
}
};
const defaultTextProps =

View file

@ -18,7 +18,9 @@ const ExportConversation = forwardRef(() => {
conversation?.conversationId !== 'search';
const clickHandler = () => {
if (exportable) setOpen(true);
if (exportable) {
setOpen(true);
}
};
return (
@ -39,4 +41,4 @@ const ExportConversation = forwardRef(() => {
);
});
export default ExportConversation;
export default ExportConversation;

View file

@ -28,7 +28,9 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
conversation?.conversationId !== 'search';
const clickHandler = () => {
if (exportable) setShowExports(true);
if (exportable) {
setShowExports(true);
}
};
return (
@ -47,7 +49,10 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
<img
className="rounded-sm"
src={
user?.avatar || `https://api.dicebear.com/6.x/initials/svg?seed=${user?.name || 'User'}&fontFamily=Verdana&fontSize=36`
user?.avatar ||
`https://api.dicebear.com/6.x/initials/svg?seed=${
user?.name || 'User'
}&fontFamily=Verdana&fontSize=36`
}
alt=""
/>
@ -77,7 +82,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
<Menu.Item as="div">
<NavLink
className={cn(
'flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none',
'flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700',
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-white/50',
)}
svg={() => <Download size={16} />}
@ -88,7 +93,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
<div className="my-1.5 h-px bg-white/20" role="none" />
<Menu.Item as="div">
<NavLink
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none"
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <TrashIcon />}
text="Clear conversations"
clickHandler={() => setShowClearConvos(true)}
@ -96,7 +101,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
</Menu.Item>
<Menu.Item as="div">
<NavLink
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none"
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <LinkIcon />}
text="Help & FAQ"
clickHandler={() => window.open('https://docs.librechat.ai/', '_blank')}
@ -104,7 +109,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
</Menu.Item>
<Menu.Item as="div">
<NavLink
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none"
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <GearIcon />}
text="Settings"
clickHandler={() => setShowSettings(true)}

View file

@ -16,7 +16,7 @@ export default function NewChat() {
return (
<a
onClick={clickHandler}
className="mb-2 flex flex-grow flex-shrink-0 h-11 cursor-pointer items-center gap-3 rounded-md border border-white/20 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
className="mb-2 flex h-11 flex-shrink-0 flex-grow cursor-pointer items-center gap-3 rounded-md border border-white/20 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
>
<svg
stroke="currentColor"

View file

@ -28,17 +28,17 @@ const SearchBar = forwardRef((props, ref) => {
} else {
setShowClearIcon(true);
}
}, [searchQuery])
}, [searchQuery]);
return (
<div
ref={ref}
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 relative"
className="relative flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
>
{<Search className="h-4 w-4 absolute left-3" />}
{<Search className="absolute left-3 h-4 w-4" />}
<input
type="text"
className="m-0 mr-0 w-full border-none bg-transparent p-0 text-sm leading-tight outline-none pl-7"
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight outline-none"
value={searchQuery}
onChange={onChange}
onKeyDown={(e) => {
@ -48,7 +48,9 @@ const SearchBar = forwardRef((props, ref) => {
onKeyUp={handleKeyUp}
/>
<X
className={`h-5 w-5 absolute right-3 cursor-pointer ${showClearIcon ? 'opacity-100' : 'opacity-0'} transition-opacity duration-1000`}
className={`absolute right-3 h-5 w-5 cursor-pointer ${
showClearIcon ? 'opacity-100' : 'opacity-0'
} transition-opacity duration-1000`}
onClick={() => {
setSearchQuery('');
clearSearch();
@ -58,4 +60,4 @@ const SearchBar = forwardRef((props, ref) => {
);
});
export default SearchBar;
export default SearchBar;

View file

@ -20,9 +20,7 @@ describe('ThemeSelector', () => {
});
it('calls onChange when the select value changes', () => {
const { getByDisplayValue } = render(
<ThemeSelector theme="system" onChange={mockOnChange} />,
);
const { getByDisplayValue } = render(<ThemeSelector theme="system" onChange={mockOnChange} />);
fireEvent.change(getByDisplayValue('System'), { target: { value: 'dark' } });

View file

@ -1,2 +1,2 @@
export { default as General } from './General';
export { ClearChatsButton } from './General';
export { ClearChatsButton } from './General';

View file

@ -8,9 +8,8 @@ import NewChat from './NewChat';
import Pages from '../Conversations/Pages';
import { Panel, Spinner } from '~/components';
import { cn } from '~/utils/';
import { useAuthContext, useDebounce } from '~/hooks';
import store from '~/store';
import { useAuthContext } from '~/hooks/AuthContext';
import useDebounce from '~/hooks/useDebounce';
export default function Nav({ navVisible, setNavVisible }) {
const [isHovering, setIsHovering] = useState(false);
@ -192,7 +191,7 @@ export default function Nav({ navVisible, setNavVisible }) {
<div className="absolute left-2 top-2 z-10 hidden md:inline-block">
<button
type="button"
className="nav-open-button flex h-11 cursor-pointer items-center gap-3 rounded-md border border-black/10 bg-white p-3 text-sm text-black transition-colors duration-200 hover:bg-gray-50 dark:border-white/20 dark:bg-gray-800 dark:hover:bg-gray-700"
className="nav-open-button flex h-11 cursor-pointer items-center gap-3 rounded-md border border-black/10 bg-white p-3 text-sm text-black transition-colors duration-200 hover:bg-gray-50 dark:border-white/20 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700"
onClick={toggleNavVisible}
>
<div className="flex items-center justify-center">

View file

@ -138,7 +138,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
};
return (
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-[102]">
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-gray-500/90 transition-opacity dark:bg-gray-800/90" />
{/* Full-screen container to center the panel */}

View file

@ -43,4 +43,4 @@ describe('PluginAuthForm', () => {
},
});
});
});
});

View file

@ -47,4 +47,4 @@ describe('PluginPagination', () => {
await userEvent.click(pageNumbers[3]);
expect(onChangePage).toHaveBeenCalledWith(4);
});
});
});

View file

@ -24,8 +24,15 @@ describe('PluginStoreItem', () => {
it('calls onUninstall when the uninstall button is clicked', async () => {
const onUninstall = jest.fn();
render(<PluginStoreItem plugin={mockPlugin} onInstall={() => {}} onUninstall={onUninstall} isInstalled />);
render(
<PluginStoreItem
plugin={mockPlugin}
onInstall={() => {}}
onUninstall={onUninstall}
isInstalled
/>,
);
await userEvent.click(screen.getByText('Uninstall'));
expect(onUninstall).toHaveBeenCalled();
});
});
});

View file

@ -3,4 +3,4 @@ export { default as PluginStoreItem } from './PluginStoreItem';
export { default as PluginPagination } from './PluginPagination';
export { default as PluginStoreLinkButton } from './PluginStoreLinkButton';
export { default as PluginAuthForm } from './PluginAuthForm';
export { default as PluginTooltip } from './PluginTooltip';
export { default as PluginTooltip } from './PluginTooltip';

View file

@ -0,0 +1,282 @@
import React from 'react';
export default function BingIcon() {
return (
<svg width={32} height={32} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="a" x1={22} x2={22} y1={2} y2={38} gradientUnits="userSpaceOnUse">
<stop stopColor="#F9F9F9" />
<stop
offset={1}
stopColor="#EDF0F9"
style={{
stopColor: '#000',
stopOpacity: 1,
}}
/>
</linearGradient>
<linearGradient
id="b"
x1={4.137}
x2={44.564}
y1={44.75}
y2={38.792}
gradientUnits="userSpaceOnUse"
>
<stop offset={0.108} stopColor="#1D6CF2" />
<stop offset={0.871} stopColor="#1B4AEF" />
</linearGradient>
<linearGradient
id="g"
x1={21.172}
x2={30.46}
y1={18.646}
y2={23.99}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#37BDFF" />
<stop offset={0.183} stopColor="#33BFFD" />
<stop offset={0.358} stopColor="#28C5F5" />
<stop offset={0.528} stopColor="#15D0E9" />
<stop offset={0.547} stopColor="#12D1E7" />
<stop offset={0.59} stopColor="#1CD2E5" />
<stop offset={0.768} stopColor="#42D8DC" />
<stop offset={0.911} stopColor="#59DBD6" />
<stop offset={1} stopColor="#62DCD4" />
</linearGradient>
<linearGradient
id="h"
x1={15.739}
x2={29.233}
y1={26.703}
y2={26.703}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#39D2FF" />
<stop offset={0.15} stopColor="#38CEFE" />
<stop offset={0.293} stopColor="#35C3FA" />
<stop offset={0.433} stopColor="#2FB0F3" />
<stop offset={0.547} stopColor="#299AEB" />
<stop offset={0.583} stopColor="#2692EC" />
<stop offset={0.763} stopColor="#1A6CF1" />
<stop offset={0.909} stopColor="#1355F4" />
<stop offset={1} stopColor="#104CF5" />
</linearGradient>
<linearGradient
id="i"
x1={18.23}
x2={18.23}
y1={27.894}
y2={9.79}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#1B48EF" />
<stop offset={0.122} stopColor="#1C51F0" />
<stop offset={0.321} stopColor="#1E69F5" />
<stop offset={0.568} stopColor="#2190FB" />
<stop offset={1} stopColor="#26B8F4" />
</linearGradient>
<linearGradient
id="j"
x1={18.421}
x2={26.776}
y1={30.045}
y2={21.718}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
<stop offset={0.373} stopColor="#FDFDFD" />
<stop offset={0.507} stopColor="#F6F6F6" />
<stop offset={0.603} stopColor="#EBEBEB" />
<stop offset={0.68} stopColor="#DADADA" />
<stop offset={0.746} stopColor="#C4C4C4" />
<stop offset={0.805} stopColor="#A8A8A8" />
<stop offset={0.858} stopColor="#888" />
<stop offset={0.907} stopColor="#626262" />
<stop offset={0.952} stopColor="#373737" />
<stop offset={0.993} stopColor="#090909" />
<stop offset={1} />
</linearGradient>
<linearGradient
id="k"
x1={18.23}
x2={18.23}
y1={9.469}
y2={27.707}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
<stop offset={0.373} stopColor="#FDFDFD" />
<stop offset={0.507} stopColor="#F6F6F6" />
<stop offset={0.603} stopColor="#EBEBEB" />
<stop offset={0.68} stopColor="#DADADA" />
<stop offset={0.746} stopColor="#C4C4C4" />
<stop offset={0.805} stopColor="#A8A8A8" />
<stop offset={0.858} stopColor="#888" />
<stop offset={0.907} stopColor="#626262" />
<stop offset={0.952} stopColor="#373737" />
<stop offset={0.993} stopColor="#090909" />
<stop offset={1} />
</linearGradient>
<radialGradient
id="c"
cx={0}
cy={0}
r={1}
gradientTransform="rotate(14.036 -132.013 71.177) scale(31.8068)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#0B31A3" />
<stop offset={1} stopColor="#39A0ED" />
</radialGradient>
<radialGradient
id="d"
cx={0}
cy={0}
r={1}
gradientTransform="rotate(-140.774 10.754 4.54) scale(20.3315)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#00FFF3" stopOpacity={0.77} />
<stop offset={0.423} stopColor="#00FFF3" stopOpacity={0.72} />
<stop offset={1} stopColor="#5BDCD6" stopOpacity={0} />
</radialGradient>
<clipPath id="e">
<path fill="#fff" d="M11.2 9.2h21.6v21.6H11.2Z" />
</clipPath>
<filter
id="f"
width={21.6}
height={24.3}
x={11.2}
y={9.2}
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix
in="SourceAlpha"
result="hardAlpha"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
/>
<feOffset dy={2.7} />
<feGaussianBlur stdDeviation={4.388} />
<feComposite in2="hardAlpha" k2={-1} k3={1} operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.81875 0 0 0 0 0.824081 0 0 0 0 1 0 0 0 0.37 0" />
<feBlend in2="shape" result="effect1_innerShadow_1360_20280" />
</filter>
</defs>
<path
fill="url(#a)"
d="M16 .005C24.836.005 32 7.166 32 16s-7.164 15.996-16 15.996c-1.779 0-3.516-.292-5.158-.85-1.509-.513-3.127-.754-4.669-.353l-4.468 1.163a1.36 1.36 0 0 1-1.66-1.659l1.162-4.46c.402-1.543.16-3.163-.355-4.673A15.982 15.98 0 0 1 0 16C0 7.166 7.164.005 16 .005Z"
style={{
display: 'inline',
fill: '#000',
fillOpacity: 0.849858,
strokeWidth: 0.888825,
}}
/>
<path
fill="url(#a)"
d="M16.073.44c8.554 0 15.488 6.943 15.488 15.509s-6.934 15.51-15.488 15.51a15.47 15.47 0 0 1-4.993-.824c-1.46-.498-3.027-.731-4.519-.342l-4.325 1.128a1.316 1.319 0 0 1-1.607-1.609l1.125-4.324c.389-1.497.155-3.068-.344-4.532a15.47 15.495 0 0 1-.825-5.007C.585 7.383 7.52.439 16.073.439z"
style={{
display: 'inline',
fill: '#fff',
fillOpacity: 1,
strokeWidth: 0.861109,
}}
/>
<g
style={{
display: 'inline',
}}
strokeWidth={1.5}
>
<path
stroke="url(#b)"
d="M5.729 37.224a.78.78 0 0 1-.951-.951l1.306-5.018c.503-1.932.19-3.915-.415-5.69a17.23 17.23 0 0 1-.919-5.568C4.75 10.472 12.473 2.75 22 2.75c9.527 0 17.25 7.722 17.25 17.247 0 9.526-7.723 17.248-17.25 17.248-1.919 0-3.792-.314-5.561-.916-1.774-.604-3.754-.915-5.683-.413z"
style={{
display: 'inline',
fill: 'none',
stroke: 'url(#b)',
}}
transform="matrix(.88887 0 0 .88875 -3.556 -1.773)"
/>
<path
stroke="url(#c)"
strokeOpacity={0.6}
d="M5.729 37.224a.78.78 0 0 1-.951-.951l1.306-5.018c.503-1.932.19-3.915-.415-5.69a17.23 17.23 0 0 1-.919-5.568C4.75 10.472 12.473 2.75 22 2.75c9.527 0 17.25 7.722 17.25 17.247 0 9.526-7.723 17.248-17.25 17.248-1.919 0-3.792-.314-5.561-.916-1.774-.604-3.754-.915-5.683-.413z"
style={{
display: 'inline',
fill: 'none',
stroke: 'url(#c)',
}}
transform="matrix(.88887 0 0 .88875 -3.556 -1.773)"
/>
<path
stroke="url(#d)"
strokeOpacity={0.8}
d="M5.729 37.224a.78.78 0 0 1-.951-.951l1.306-5.018c.503-1.932.19-3.915-.415-5.69a17.23 17.23 0 0 1-.919-5.568C4.75 10.472 12.473 2.75 22 2.75c9.527 0 17.25 7.722 17.25 17.247 0 9.526-7.723 17.248-17.25 17.248-1.919 0-3.792-.314-5.561-.916-1.774-.604-3.754-.915-5.683-.413z"
style={{
display: 'inline',
fill: 'none',
stroke: 'url(#d)',
}}
transform="matrix(.88887 0 0 .88875 -3.556 -1.773)"
/>
</g>
<g
clipPath="url(#e)"
filter="url(#f)"
transform="matrix(.88889 0 0 .88876 -3.555 -1.773)"
style={{
fill: 'none',
}}
>
<path
fill="url(#g)"
d="M30.1 23.08a5.706 5.706 0 0 1-1.529 3.9 2.582 2.582 0 0 0 .562-1.009c.01-.037.02-.074.028-.112a.244.244 0 0 0 .004-.013c.01-.037.016-.074.023-.111.007-.039.015-.078.02-.116v-.003a2.48 2.48 0 0 0 .026-.364 2.563 2.563 0 0 0-.786-1.856 2.55 2.55 0 0 0-1.143-.643l-.006-.001-.046-.016-.665-.23-1.74-.6c-.006-.002-.013-.002-.017-.004l-.109-.04a1.616 1.616 0 0 1-.82-.723l-.636-1.626-.727-1.862-.14-.359-.036-.073a.826.826 0 0 1-.061-.314c0-.028 0-.057.003-.083a.814.814 0 0 1 1.123-.666l3.242 1.668.64.328c.338.202.654.44.942.708a5.715 5.715 0 0 1 1.847 4.22z"
style={{
display: 'inline',
fill: 'url(#g)',
}}
/>
<path
fill="url(#h)"
d="M29.233 25.252c0 .166-.016.326-.045.483a2.747 2.747 0 0 1-.13.452c-.014.037-.03.073-.045.11a2.61 2.61 0 0 1-.443.683c-.472.525-2.076 1.46-2.668 1.84l-1.313.805c-.961.594-1.87 1.015-3.017 1.044l-.16.003a5.71 5.71 0 0 1-4.83-2.67 5.666 5.666 0 0 1-.843-2.387 2.53 2.53 0 0 0 3.703 1.783l.129-.078.522-.31.666-.394v-.019l.085-.051 5.952-3.538.458-.272.045.015.006.002a2.55 2.55 0 0 1 1.389.917 2.577 2.577 0 0 1 .54 1.582z"
style={{
display: 'inline',
fill: 'url(#h)',
}}
/>
<path
fill="url(#i)"
d="m20.76 13.446-.002 13.17-.665.395-.523.309-.129.078-.008.004a2.528 2.528 0 0 1-3.734-2.263V10.318a.849.849 0 0 1 1.32-.705l2.59 1.697c.013.012.029.022.045.032a2.543 2.543 0 0 1 1.106 2.104z"
style={{
display: 'inline',
fill: 'url(#i)',
}}
/>
<path
fill="url(#j)"
d="M29.233 25.252c0 .166-.016.326-.045.483a2.747 2.747 0 0 1-.13.452c-.014.037-.03.073-.045.11a2.587 2.587 0 0 1-.443.683c-.472.525-2.076 1.46-2.668 1.84l-1.313.805c-.961.594-1.87 1.015-3.017 1.044l-.16.003a5.71 5.71 0 0 1-4.83-2.67 5.666 5.666 0 0 1-.843-2.387 2.53 2.53 0 0 0 3.703 1.783l.129-.078.522-.31.666-.394v-.019l.085-.051 5.952-3.538.458-.272.045.015.006.002a2.55 2.55 0 0 1 1.389.917 2.577 2.577 0 0 1 .54 1.582z"
opacity={0.15}
style={{
display: 'inline',
fill: 'url(#j)',
}}
/>
<path
fill="url(#k)"
d="m20.76 13.446-.002 13.17-.665.395-.523.309-.129.078-.008.004a2.528 2.528 0 0 1-3.734-2.263V10.318a.849.849 0 0 1 1.32-.705l2.59 1.697c.013.012.029.022.045.032a2.543 2.543 0 0 1 1.106 2.104z"
opacity={0.1}
style={{
display: 'inline',
fill: 'url(#k)',
}}
/>
</g>
</svg>
);
}

View file

@ -0,0 +1,267 @@
import React from 'react';
export default function BingIcon() {
return (
<svg width={32} height={32} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="a" x1={22} x2={22} y1={2} y2={38} gradientUnits="userSpaceOnUse">
<stop stopColor="#F9F9F9" />
<stop
offset={1}
stopColor="#EDF0F9"
style={{
stopColor: '#000',
stopOpacity: 1,
}}
/>
</linearGradient>
<linearGradient
id="b"
x1={4.137}
x2={44.564}
y1={44.75}
y2={38.792}
gradientUnits="userSpaceOnUse"
>
<stop offset={0.108} stopColor="#1D6CF2" />
<stop offset={0.871} stopColor="#1B4AEF" />
</linearGradient>
<linearGradient
id="g"
x1={21.172}
x2={30.46}
y1={18.646}
y2={23.99}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#37BDFF" />
<stop offset={0.183} stopColor="#33BFFD" />
<stop offset={0.358} stopColor="#28C5F5" />
<stop offset={0.528} stopColor="#15D0E9" />
<stop offset={0.547} stopColor="#12D1E7" />
<stop offset={0.59} stopColor="#1CD2E5" />
<stop offset={0.768} stopColor="#42D8DC" />
<stop offset={0.911} stopColor="#59DBD6" />
<stop offset={1} stopColor="#62DCD4" />
</linearGradient>
<linearGradient
id="h"
x1={15.739}
x2={29.233}
y1={26.703}
y2={26.703}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#39D2FF" />
<stop offset={0.15} stopColor="#38CEFE" />
<stop offset={0.293} stopColor="#35C3FA" />
<stop offset={0.433} stopColor="#2FB0F3" />
<stop offset={0.547} stopColor="#299AEB" />
<stop offset={0.583} stopColor="#2692EC" />
<stop offset={0.763} stopColor="#1A6CF1" />
<stop offset={0.909} stopColor="#1355F4" />
<stop offset={1} stopColor="#104CF5" />
</linearGradient>
<linearGradient
id="i"
x1={18.23}
x2={18.23}
y1={27.894}
y2={9.79}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#1B48EF" />
<stop offset={0.122} stopColor="#1C51F0" />
<stop offset={0.321} stopColor="#1E69F5" />
<stop offset={0.568} stopColor="#2190FB" />
<stop offset={1} stopColor="#26B8F4" />
</linearGradient>
<linearGradient
id="j"
x1={18.421}
x2={26.776}
y1={30.045}
y2={21.718}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
<stop offset={0.373} stopColor="#FDFDFD" />
<stop offset={0.507} stopColor="#F6F6F6" />
<stop offset={0.603} stopColor="#EBEBEB" />
<stop offset={0.68} stopColor="#DADADA" />
<stop offset={0.746} stopColor="#C4C4C4" />
<stop offset={0.805} stopColor="#A8A8A8" />
<stop offset={0.858} stopColor="#888" />
<stop offset={0.907} stopColor="#626262" />
<stop offset={0.952} stopColor="#373737" />
<stop offset={0.993} stopColor="#090909" />
<stop offset={1} />
</linearGradient>
<linearGradient
id="k"
x1={18.23}
x2={18.23}
y1={9.469}
y2={27.707}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
<stop offset={0.373} stopColor="#FDFDFD" />
<stop offset={0.507} stopColor="#F6F6F6" />
<stop offset={0.603} stopColor="#EBEBEB" />
<stop offset={0.68} stopColor="#DADADA" />
<stop offset={0.746} stopColor="#C4C4C4" />
<stop offset={0.805} stopColor="#A8A8A8" />
<stop offset={0.858} stopColor="#888" />
<stop offset={0.907} stopColor="#626262" />
<stop offset={0.952} stopColor="#373737" />
<stop offset={0.993} stopColor="#090909" />
<stop offset={1} />
</linearGradient>
<radialGradient
id="c"
cx={0}
cy={0}
r={1}
gradientTransform="rotate(14.036 -132.013 71.177) scale(31.8068)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#0B31A3" />
<stop offset={1} stopColor="#39A0ED" />
</radialGradient>
<radialGradient
id="d"
cx={0}
cy={0}
r={1}
gradientTransform="rotate(-140.774 10.754 4.54) scale(20.3315)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#00FFF3" stopOpacity={0.77} />
<stop offset={0.423} stopColor="#00FFF3" stopOpacity={0.72} />
<stop offset={1} stopColor="#5BDCD6" stopOpacity={0} />
</radialGradient>
<clipPath id="e">
<path fill="#fff" d="M11.2 9.2h21.6v21.6H11.2Z" />
</clipPath>
<filter
id="f"
width={21.6}
height={24.3}
x={11.2}
y={9.2}
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feColorMatrix
in="SourceAlpha"
result="hardAlpha"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
/>
<feOffset dy={2.7} />
<feGaussianBlur stdDeviation={4.388} />
<feComposite in2="hardAlpha" k2={-1} k3={1} operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.81875 0 0 0 0 0.824081 0 0 0 0 1 0 0 0 0.37 0" />
<feBlend in2="shape" result="effect1_innerShadow_1360_20280" />
</filter>
</defs>
<path
fill="url(#a)"
d="M16 .005C24.836.005 32 7.166 32 16s-7.164 15.996-16 15.996c-1.779 0-3.516-.292-5.158-.85-1.509-.513-3.127-.754-4.669-.353l-4.468 1.163a1.36 1.36 0 0 1-1.66-1.659l1.162-4.46c.402-1.543.16-3.163-.355-4.673A15.982 15.98 0 0 1 0 16C0 7.166 7.164.005 16 .005Z"
style={{
display: 'inline',
fill: '#000',
fillOpacity: 0.849858,
strokeWidth: 0.888825,
}}
/>
<g strokeWidth={1.5}>
<path
stroke="url(#b)"
d="M5.729 37.224a.78.78 0 0 1-.951-.951l1.306-5.018c.503-1.932.19-3.915-.415-5.69a17.23 17.23 0 0 1-.919-5.568C4.75 10.472 12.473 2.75 22 2.75c9.527 0 17.25 7.722 17.25 17.247 0 9.526-7.723 17.248-17.25 17.248-1.919 0-3.792-.314-5.561-.916-1.774-.604-3.754-.915-5.683-.413z"
style={{
display: 'inline',
fill: 'none',
stroke: 'url(#b)',
}}
transform="matrix(.88887 0 0 .88875 -3.556 -1.773)"
/>
<path
stroke="url(#c)"
strokeOpacity={0.6}
d="M5.729 37.224a.78.78 0 0 1-.951-.951l1.306-5.018c.503-1.932.19-3.915-.415-5.69a17.23 17.23 0 0 1-.919-5.568C4.75 10.472 12.473 2.75 22 2.75c9.527 0 17.25 7.722 17.25 17.247 0 9.526-7.723 17.248-17.25 17.248-1.919 0-3.792-.314-5.561-.916-1.774-.604-3.754-.915-5.683-.413z"
style={{
display: 'inline',
fill: 'none',
stroke: 'url(#c)',
}}
transform="matrix(.88887 0 0 .88875 -3.556 -1.773)"
/>
<path
stroke="url(#d)"
strokeOpacity={0.8}
d="M5.729 37.224a.78.78 0 0 1-.951-.951l1.306-5.018c.503-1.932.19-3.915-.415-5.69a17.23 17.23 0 0 1-.919-5.568C4.75 10.472 12.473 2.75 22 2.75c9.527 0 17.25 7.722 17.25 17.247 0 9.526-7.723 17.248-17.25 17.248-1.919 0-3.792-.314-5.561-.916-1.774-.604-3.754-.915-5.683-.413z"
style={{
display: 'inline',
fill: 'none',
stroke: 'url(#d)',
}}
transform="matrix(.88887 0 0 .88875 -3.556 -1.773)"
/>
</g>
<g
clipPath="url(#e)"
filter="url(#f)"
transform="matrix(.88889 0 0 .88876 -3.555 -1.773)"
style={{
fill: 'none',
}}
>
<path
fill="url(#g)"
d="M30.1 23.08a5.706 5.706 0 0 1-1.529 3.9 2.582 2.582 0 0 0 .562-1.009c.01-.037.02-.074.028-.112a.244.244 0 0 0 .004-.013c.01-.037.016-.074.023-.111.007-.039.015-.078.02-.116v-.003a2.48 2.48 0 0 0 .026-.364 2.563 2.563 0 0 0-.786-1.856 2.55 2.55 0 0 0-1.143-.643l-.006-.001-.046-.016-.665-.23-1.74-.6c-.006-.002-.013-.002-.017-.004l-.109-.04a1.616 1.616 0 0 1-.82-.723l-.636-1.626-.727-1.862-.14-.359-.036-.073a.826.826 0 0 1-.061-.314c0-.028 0-.057.003-.083a.814.814 0 0 1 1.123-.666l3.242 1.668.64.328c.338.202.654.44.942.708a5.715 5.715 0 0 1 1.847 4.22z"
style={{
display: 'inline',
fill: 'url(#g)',
}}
/>
<path
fill="url(#h)"
d="M29.233 25.252c0 .166-.016.326-.045.483a2.747 2.747 0 0 1-.13.452c-.014.037-.03.073-.045.11a2.61 2.61 0 0 1-.443.683c-.472.525-2.076 1.46-2.668 1.84l-1.313.805c-.961.594-1.87 1.015-3.017 1.044l-.16.003a5.71 5.71 0 0 1-4.83-2.67 5.666 5.666 0 0 1-.843-2.387 2.53 2.53 0 0 0 3.703 1.783l.129-.078.522-.31.666-.394v-.019l.085-.051 5.952-3.538.458-.272.045.015.006.002a2.55 2.55 0 0 1 1.389.917 2.577 2.577 0 0 1 .54 1.582z"
style={{
display: 'inline',
fill: 'url(#h)',
}}
/>
<path
fill="url(#i)"
d="m20.76 13.446-.002 13.17-.665.395-.523.309-.129.078-.008.004a2.528 2.528 0 0 1-3.734-2.263V10.318a.849.849 0 0 1 1.32-.705l2.59 1.697c.013.012.029.022.045.032a2.543 2.543 0 0 1 1.106 2.104z"
style={{
display: 'inline',
fill: 'url(#i)',
}}
/>
<path
fill="url(#j)"
d="M29.233 25.252c0 .166-.016.326-.045.483a2.747 2.747 0 0 1-.13.452c-.014.037-.03.073-.045.11a2.587 2.587 0 0 1-.443.683c-.472.525-2.076 1.46-2.668 1.84l-1.313.805c-.961.594-1.87 1.015-3.017 1.044l-.16.003a5.71 5.71 0 0 1-4.83-2.67 5.666 5.666 0 0 1-.843-2.387 2.53 2.53 0 0 0 3.703 1.783l.129-.078.522-.31.666-.394v-.019l.085-.051 5.952-3.538.458-.272.045.015.006.002a2.55 2.55 0 0 1 1.389.917 2.577 2.577 0 0 1 .54 1.582z"
opacity={0.15}
style={{
display: 'inline',
fill: 'url(#j)',
}}
/>
<path
fill="url(#k)"
d="m20.76 13.446-.002 13.17-.665.395-.523.309-.129.078-.008.004a2.528 2.528 0 0 1-3.734-2.263V10.318a.849.849 0 0 1 1.32-.705l2.59 1.697c.013.012.029.022.045.032a2.543 2.543 0 0 1 1.106 2.104z"
opacity={0.1}
style={{
display: 'inline',
fill: 'url(#k)',
}}
/>
</g>
</svg>
);
}

View file

@ -1,4 +1,4 @@
import * as React from 'react'
import * as React from 'react';
export default function CogIcon() {
return (

View file

@ -3,10 +3,16 @@ import React from 'react';
export default function DiscordIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" id="discord" className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
id="discord"
className="h-6 w-6"
>
<circle cx="512" cy="512" r="512" fill="#5865f2"/>
<path fill="#fff" d="M689.43 349a422.21 422.21 0 0 0-104.22-32.32 1.58 1.58 0 0 0-1.68.79 294.11 294.11 0 0 0-13 26.66 389.78 389.78 0 0 0-117.05 0 269.75 269.75 0 0 0-13.18-26.66 1.64 1.64 0 0 0-1.68-.79A421 421 0 0 0 334.44 349a1.49 1.49 0 0 0-.69.59c-66.37 99.17-84.55 195.9-75.63 291.41a1.76 1.76 0 0 0 .67 1.2 424.58 424.58 0 0 0 127.85 64.63 1.66 1.66 0 0 0 1.8-.59 303.45 303.45 0 0 0 26.15-42.54 1.62 1.62 0 0 0-.89-2.25 279.6 279.6 0 0 1-39.94-19 1.64 1.64 0 0 1-.16-2.72c2.68-2 5.37-4.1 7.93-6.22a1.58 1.58 0 0 1 1.65-.22c83.79 38.26 174.51 38.26 257.31 0a1.58 1.58 0 0 1 1.68.2c2.56 2.11 5.25 4.23 8 6.24a1.64 1.64 0 0 1-.14 2.72 262.37 262.37 0 0 1-40 19 1.63 1.63 0 0 0-.87 2.28 340.72 340.72 0 0 0 26.13 42.52 1.62 1.62 0 0 0 1.8.61 423.17 423.17 0 0 0 128-64.63 1.64 1.64 0 0 0 .67-1.18c10.68-110.44-17.88-206.38-75.7-291.42a1.3 1.3 0 0 0-.63-.63zM427.09 582.85c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.02 28.44-20.37 51.6-46 51.6zm170.13 0c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.01 28.44-20.17 51.6-46 51.6z"></path>
<circle cx="512" cy="512" r="512" fill="#5865f2" />
<path
fill="#fff"
d="M689.43 349a422.21 422.21 0 0 0-104.22-32.32 1.58 1.58 0 0 0-1.68.79 294.11 294.11 0 0 0-13 26.66 389.78 389.78 0 0 0-117.05 0 269.75 269.75 0 0 0-13.18-26.66 1.64 1.64 0 0 0-1.68-.79A421 421 0 0 0 334.44 349a1.49 1.49 0 0 0-.69.59c-66.37 99.17-84.55 195.9-75.63 291.41a1.76 1.76 0 0 0 .67 1.2 424.58 424.58 0 0 0 127.85 64.63 1.66 1.66 0 0 0 1.8-.59 303.45 303.45 0 0 0 26.15-42.54 1.62 1.62 0 0 0-.89-2.25 279.6 279.6 0 0 1-39.94-19 1.64 1.64 0 0 1-.16-2.72c2.68-2 5.37-4.1 7.93-6.22a1.58 1.58 0 0 1 1.65-.22c83.79 38.26 174.51 38.26 257.31 0a1.58 1.58 0 0 1 1.68.2c2.56 2.11 5.25 4.23 8 6.24a1.64 1.64 0 0 1-.14 2.72 262.37 262.37 0 0 1-40 19 1.63 1.63 0 0 0-.87 2.28 340.72 340.72 0 0 0 26.13 42.52 1.62 1.62 0 0 0 1.8.61 423.17 423.17 0 0 0 128-64.63 1.64 1.64 0 0 0 .67-1.18c10.68-110.44-17.88-206.38-75.7-291.42a1.3 1.3 0 0 0-.63-.63zM427.09 582.85c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.02 28.44-20.37 51.6-46 51.6zm170.13 0c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.01 28.44-20.17 51.6-46 51.6z"
></path>
</svg>
);
}

View file

@ -2,12 +2,7 @@ import React from 'react';
export default function GoogleIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
id="google"
className="h-5 w-5"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="google" className="h-5 w-5">
<path
fill="#fbbb00"
d="M113.47 309.408 95.648 375.94l-65.139 1.378C11.042 341.211 0 299.9 0 256c0-42.451 10.324-82.483 28.624-117.732h.014L86.63 148.9l25.404 57.644c-5.317 15.501-8.215 32.141-8.215 49.456.002 18.792 3.406 36.797 9.651 53.408z"
@ -26,4 +21,4 @@ export default function GoogleIcon() {
></path>
</svg>
);
}
}

View file

@ -5,7 +5,8 @@ export default function OpenIDIcon() {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" id="openid" className="h-5 w-5">
<path
fill="currentColor"
d="M271.5 432l-68 32C88.5 453.7 0 392.5 0 318.2c0-71.5 82.5-131 191.7-144.3v43c-71.5 12.5-124 53-124 101.3 0 51 58.5 93.3 135.7 103v-340l68-33.2v384zM448 291l-131.3-28.5 36.8-20.7c-19.5-11.5-43.5-20-70-24.8v-43c46.2 5.5 87.7 19.5 120.3 39.3l35-19.8L448 291z"></path>
d="M271.5 432l-68 32C88.5 453.7 0 392.5 0 318.2c0-71.5 82.5-131 191.7-144.3v43c-71.5 12.5-124 53-124 101.3 0 51 58.5 93.3 135.7 103v-340l68-33.2v384zM448 291l-131.3-28.5 36.8-20.7c-19.5-11.5-43.5-20-70-24.8v-43c46.2 5.5 87.7 19.5 120.3 39.3l35-19.8L448 291z"
></path>
</svg>
);
}

View file

@ -3,6 +3,8 @@ export { default as GPTIcon } from './GPTIcon';
export { default as CogIcon } from './CogIcon';
export { default as Panel } from './Panel';
export { default as Spinner } from './Spinner';
export { default as Clipboard } from './Clipboard';
export { default as CheckMark } from './CheckMark';
export { default as MessagesSquared } from './MessagesSquared';
export { default as StopGeneratingIcon } from './StopGeneratingIcon';
export { default as GoogleIcon } from './GoogleIcon';

View file

@ -28,7 +28,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'animate-in fade-in fixed inset-0 z-50 bg-gray-500/90 dark:bg-gray-800/90 transition-opacity',
'animate-in fade-in fixed inset-0 z-50 bg-gray-500/90 transition-opacity dark:bg-gray-800/90',
className,
)}
{...props}

View file

@ -24,7 +24,7 @@ const DialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
'data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out fixed inset-0 z-[999] bg-gray-500/90 dark:bg-gray-800/90 transition-all duration-100',
'data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out fixed inset-0 z-[999] bg-gray-500/90 transition-all duration-100 dark:bg-gray-800/90',
className,
)}
{...props}
@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-[999] grid w-full gap-4 rounded-b-lg bg-white pb-6 sm:rounded-lg md:w-[680px] overflow-y-auto',
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-[999] grid w-full gap-4 overflow-y-auto rounded-b-lg bg-white pb-6 sm:rounded-lg md:w-[680px]',
'dark:bg-slate-900',
className,
)}
@ -71,7 +71,10 @@ DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-between sm:space-x-2 px-6', className)}
className={cn(
'flex flex-col-reverse px-6 sm:flex-row sm:justify-between sm:space-x-2',
className,
)}
{...props}
/>
);

View file

@ -37,9 +37,7 @@ describe('DialogTemplate', () => {
it('renders correctly without optional props', () => {
const { getByText, queryByText } = render(
<Dialog open onOpenChange={() => {}}>
<DialogTemplate
title="Test Dialog"
/>
<DialogTemplate title="Test Dialog" />
</Dialog>,
);

View file

@ -34,7 +34,9 @@ const DialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDivE
return (
<DialogContent ref={ref} className={cn('shadow-2xl dark:bg-gray-900', className || '')}>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">{title}</DialogTitle>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{title}
</DialogTitle>
{description && (
<DialogDescription className="text-gray-600 dark:text-gray-300">
{description}

View file

@ -21,14 +21,18 @@ const ModelSelect = ({ model, onChange, availableModels, ...props }) => {
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button {...props}>
<span className="w-full text-center text-xs font-medium font-normal">{localize(lang, 'com_ui_model')}: {model}</span>
<span className="w-full text-center text-xs font-medium font-normal">
{localize(lang, 'com_ui_model')}: {model}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 dark:bg-gray-700"
onCloseAutoFocus={(event) => event.preventDefault()}
>
<DropdownMenuLabel className="dark:text-gray-300">{localize(lang, 'com_ui_select_model')}</DropdownMenuLabel>
<DropdownMenuLabel className="dark:text-gray-300">
{localize(lang, 'com_ui_select_model')}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value={model} onValueChange={onChange} className="overflow-y-auto">
{availableModels.map((model) => (

View file

@ -1,5 +1,5 @@
import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '../../utils';
@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-600 data-[state=unchecked]:bg-gray-200',
'focus-visible:ring-ring focus-visible:ring-offset-background peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-600 data-[state=unchecked]:bg-gray-200',
className,
)}
{...props}
@ -21,7 +21,7 @@ const Switch = React.forwardRef<
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };

View file

@ -16,9 +16,13 @@ export default function Templates({ showTemplates }) {
<div className="flex flex-1 flex-col items-center gap-3.5">
<span className="text-sm text-gray-700 dark:text-gray-400">
{localize(lang, 'com_ui_showing')} <span className="font-semibold text-gray-900 dark:text-white">1</span> {localize(lang, 'com_ui_of')}{' '}
{localize(lang, 'com_ui_showing')}{' '}
<span className="font-semibold text-gray-900 dark:text-white">1</span>{' '}
{localize(lang, 'com_ui_of')}{' '}
<a id="prompt-link">
<span className="font-semibold text-gray-900 dark:text-white">1 {localize(lang, 'com_ui_entries')}</span>
<span className="font-semibold text-gray-900 dark:text-white">
1 {localize(lang, 'com_ui_entries')}
</span>
</a>
</span>
<button

View file

@ -12,13 +12,13 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
<textarea
className={cn(
'flex h-20 w-full resize-none rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
className
className,
)}
ref={ref}
{...props}
/>
);
}
},
);
Textarea.displayName = 'Textarea';