Compare commits

..

10 Commits

Author SHA1 Message Date
Danny Avila
d69465ea3d refactor(useHandleKeyUp): enter from beginning only 2024-07-11 13:16:42 -04:00
Danny Avila
5ef71a7a36 🪙 fix: Streaming Response Token Issue (#3323)
* chore: use NEW_CONVO constant

* fix: token object assign issue
2024-07-10 23:41:21 -04:00
Peter Dave Hello
326069d7a6 🌏 i18n: Improve Traditional Chinese translation (#3311) 2024-07-10 16:54:12 -04:00
ylioja
785430daf5 🌍 i18n: Added Finnish Translations (#3316) 2024-07-10 16:53:45 -04:00
Anirudh
03fe361917 🔧 fix+chore: Resolve Overflow in Settings Modal & Upgrade to Headless UI 2.0 (#2661)
* fix: dropdown overflow

* fix: make dropdown work on mobile

* feat: update headlessui to 2.0 and use portal

* feat: rewrite modal using headlessui

* fix: applying of maxHeight

* fix: optimize backdrop for dark mode

* fix: rendering dropdown width

* feat: match small screen layout to radix-ui dialog

* revert: mobile modifications

* fix: modal animations

* fix: z-index

* chore: Migrate from HeadlessUI 1.0 to 2.0

* fix: h2 nesting

* fix: use lighter border for PopoverButtons

* feat: Move modal to the top if using a small screen

* fix: mobile position

* fix: frontend tests

* feat: use row layout in mobile instead of col

* fix: remove config path from tsconfig

* fix: fix dropdown tests (gpt4o ftw!)

* feat: Upgrade to latest headlessui version

* fix:test1

* fix: ThemeSelector test

* fix: re-add speech tab

* style: use pl and pr-3

* fix: speech tab dropdowns

* style: use maxHeight for language

* feat: convert DropdownNoState to v2.0

* fix: use v2 params for voiceDropdown

* style: reduce maxHeight for VoiceDropdown and set fixed width

* chore: rebuild package-lock

* style(fix): copy over the same styles for the settingsTab

* style(fix): use -top-1 for speech tabs

* style(fix): use max-w-[400px]

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-07-10 16:45:58 -04:00
Marco Beretta
b34a4ddac1 🌩️ feat: cloud-based browser voices (#3297)
* initial voice support

* feat: local voices; feat: switch cloud-based voices

* feat: apply voice to hook
2024-07-10 16:44:12 -04:00
Mert Doğruca
7d5b03dd98 🗨️ fix: Reset prompt groups query when changing filters (#3294) 2024-07-10 16:43:02 -04:00
Marco Beretta
f959ee302c 🗣️ fix: get speechTab config; feature: not overriding variables selected by user (#3282)
* fix(Speech): speechTab settings update

* fix: get speech config; refactor: moved everything to types and removed file types; feature: not overriding variables selected by user
2024-07-10 16:38:36 -04:00
Marco Beretta
cd00df69bb 👤 feat: zoom and rotate avatar (#3264) 2024-07-10 16:38:02 -04:00
Marco Beretta
a05e2c1dcc 🗣️ feat: Azure OpenAI speech (#2985)
* feat: Azure STT

* feat: Azure TTS

* refactor: use enums

* fix: frontend tests

* fix(config): wrong key provider
2024-07-10 16:33:06 -04:00
59 changed files with 1922 additions and 737 deletions

View File

@@ -1,2 +0,0 @@
# When running devcontainers, you can specify if docker & docker-compose should be installed in your environment
INSTALL_DOCKER=false

View File

@@ -1,33 +1,5 @@
# .devcontainer/Dockerfile
FROM node:18-bullseye
ARG INSTALL_DOCKER="false"
ENV INSTALL_DOCKER=${INSTALL_DOCKER}
# Install Docker and Docker Compose only if INSTALL_DOCKER is "true"
RUN if [ "$INSTALL_DOCKER" = "true" ]; then \
apt-get update && \
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release && \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
apt-get update && \
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin; \
fi
# Install sudo
RUN apt-get update && apt-get install -y sudo
# Set up non-root user
RUN useradd -m -s /bin/bash vscode
RUN if [ "$INSTALL_DOCKER" = "true" ]; then usermod -aG docker vscode; fi
# Add vscode user to sudoers
RUN echo "vscode ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/vscode && \
chmod 0440 /etc/sudoers.d/vscode
USER vscode
WORKDIR /workspaces/LibreChat
# Set the default command
CMD ["/bin/bash"]
RUN mkdir -p /workspaces && chown -R vscode:vscode /workspaces
WORKDIR /workspaces

View File

@@ -1,23 +1,18 @@
{
"name": "LibreChat Development",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/LibreChat",
"workspaceFolder": "/workspaces",
"customizations": {
"vscode": {
"extensions": ["ms-azuretools.vscode-docker"]
"extensions": [],
"settings": {
"terminal.integrated.profiles.linux": {
"bash": null
}
}
}
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": false,
"dockerDashComposeVersion": "v2"
}
},
"remoteUser": "vscode",
"postCreateCommand": "sudo chown root:docker /var/run/docker.sock && sudo chmod 660 /var/run/docker.sock && npm run reinstall && npm run pull:rag && npm run copy-ex && MEILI_MASTER_KEY=$(docker-compose -f .devcontainer/docker-compose.yml exec -T meilisearch printenv MEILI_MASTER_KEY) && sed -i \"s/^MEILI_MASTER_KEY=.*/MEILI_MASTER_KEY=$MEILI_MASTER_KEY/\" .env",
"remoteEnv": {
"INSTALL_DOCKER": "${localEnv:INSTALL_DOCKER:false}"
}
"postCreateCommand": "",
"features": { "ghcr.io/devcontainers/features/git:1": {} },
"remoteUser": "vscode"
}

View File

@@ -1,16 +1,10 @@
# .devcontainer/docker-compose.yml
version: "3.8"
services:
app:
group_add:
- docker
build:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
- INSTALL_DOCKER=${INSTALL_DOCKER:-false}
# restart: always
links:
- mongodb
@@ -23,7 +17,6 @@ services:
volumes:
# This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
- ..:/workspaces:cached
- /var/run/docker.sock:/var/run/docker.sock
# Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
# - /var/run/docker.sock:/var/run/docker.sock
environment:
@@ -43,12 +36,14 @@ services:
user: vscode
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"
command: /bin/sh -c "while sleep 1000; do :; done"
mongodb:
container_name: chat-mongodb
expose:
- 27017
# ports:
# - 27018:27017
image: mongo
# restart: always
volumes:
@@ -60,8 +55,11 @@ services:
# restart: always
expose:
- 7700
# Uncomment this to access meilisearch from outside docker
# ports:
# - 7700:7700 # if exposing these ports, make sure your master key is not the default value
environment:
- MEILI_NO_ANALYTICS=true
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY:-$(openssl rand -hex 16)}
- MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63
volumes:
- ./meili_data_v1.5:/meili_data

View File

@@ -436,6 +436,3 @@ HELP_AND_FAQ_URL=https://librechat.ai
# E2E_USER_EMAIL=
# E2E_USER_PASSWORD=
# RAG_PORT
RAG_PORT=8000

View File

@@ -112,7 +112,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
progressCallback,
progressOptions: {
res,
text,
// parentMessageId: overrideParentMessageId || userMessageId,
},
};

View File

@@ -119,7 +119,6 @@ const EditController = async (req, res, next, initializeClient) => {
progressCallback,
progressOptions: {
res,
text,
// parentMessageId: overrideParentMessageId || userMessageId,
},
});

View File

@@ -69,8 +69,10 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
*/
const onStart = (userMessage, responseMessageId) => {
sendMessage(res, { message: userMessage, created: true });
const abortKey = userMessage?.conversationId ?? req.user.id;
const prevRequest = abortControllers.get(abortKey);
if (prevRequest && prevRequest?.abortController) {
const data = prevRequest.abortController.getAbortData();
getReqData({ userMessage: data?.userMessage });
@@ -81,6 +83,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
});
return;
}
abortControllers.set(abortKey, { abortController, ...endpointOption });
res.on('finish', function () {

View File

@@ -195,7 +195,6 @@ router.post(
progressCallback,
progressOptions: {
res,
text,
// parentMessageId: overrideParentMessageId || userMessageId,
plugins,
},

View File

@@ -168,7 +168,6 @@ router.post(
progressCallback,
progressOptions: {
res,
text,
plugin,
// parentMessageId: overrideParentMessageId || userMessageId,
},

View File

@@ -26,6 +26,7 @@ async function getCustomConfigSpeech(req, res) {
if (ttsSchema.advancedMode !== undefined) {
settings.advancedMode = ttsSchema.advancedMode;
}
if (ttsSchema.speechToText) {
for (const key in ttsSchema.speechToText) {
if (ttsSchema.speechToText[key] !== undefined) {
@@ -33,6 +34,7 @@ async function getCustomConfigSpeech(req, res) {
}
}
}
if (ttsSchema.textToSpeech) {
for (const key in ttsSchema.textToSpeech) {
if (ttsSchema.textToSpeech[key] !== undefined) {
@@ -41,7 +43,7 @@ async function getCustomConfigSpeech(req, res) {
}
}
res.json(settings);
return res.status(200).send(settings);
} catch (error) {
res.status(200).send();
}

View File

@@ -1,3 +1,4 @@
const { TTSProviders } = require('librechat-data-provider');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { getProvider } = require('./textToSpeech');
@@ -24,13 +25,16 @@ async function getVoices(req, res) {
let voices;
switch (provider) {
case 'openai':
case TTSProviders.OPENAI:
voices = ttsSchema.openai?.voices;
break;
case 'elevenlabs':
case TTSProviders.AZURE_OPENAI:
voices = ttsSchema.azureOpenAI?.voices;
break;
case TTSProviders.ELEVENLABS:
voices = ttsSchema.elevenlabs?.voices;
break;
case 'localai':
case TTSProviders.LOCALAI:
voices = ttsSchema.localai?.voices;
break;
default:

View File

@@ -1,8 +1,9 @@
const axios = require('axios');
const { Readable } = require('stream');
const { logger } = require('~/config');
const axios = require('axios');
const { extractEnvVariable, STTProviders } = require('librechat-data-provider');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { extractEnvVariable } = require('librechat-data-provider');
const { genAzureEndpoint } = require('~/utils');
const { logger } = require('~/config');
/**
* Handle the response from the STT API
@@ -24,12 +25,34 @@ async function handleResponse(response) {
return response.data.text.trim();
}
function getProvider(sttSchema) {
if (sttSchema?.openai) {
return 'openai';
/**
* getProviderSchema function
* This function takes the customConfig object and returns the name of the provider and its schema
* If more than one provider is set or no provider is set, it throws an error
*
* @param {Object} customConfig - The custom configuration containing the STT schema
* @returns {Promise<[string, Object]>} The name of the provider and its schema
* @throws {Error} Throws an error if multiple providers are set or no provider is set
*/
async function getProviderSchema(customConfig) {
const sttSchema = customConfig.speech.stt;
if (!sttSchema) {
throw new Error(`No STT schema is set. Did you configure STT in the custom config (librechat.yaml)?
https://www.librechat.ai/docs/configuration/stt_tts#stt`);
}
throw new Error('Invalid provider');
const providers = Object.entries(sttSchema).filter(([, value]) => Object.keys(value).length > 0);
if (providers.length > 1) {
throw new Error('Multiple providers are set. Please set only one provider.');
} else if (providers.length === 0) {
throw new Error('No provider is set. Please set a provider.');
} else {
const provider = providers[0][0];
return [provider, sttSchema[provider]];
}
}
function removeUndefined(obj) {
@@ -83,72 +106,63 @@ function openAIProvider(sttSchema, audioReadStream) {
}
/**
* This function prepares the necessary data and headers for making a request to the Azure API
* It uses the provided request and audio stream to create the request
* Prepares the necessary data and headers for making a request to the Azure API.
* It uses the provided Speech-to-Text (STT) schema and audio file to create the request.
*
* @param {Object} req - The request object, which should contain the endpoint in its body
* @param {Stream} audioReadStream - The audio data to be transcribed
* @param {Object} sttSchema - The STT schema object, which should contain instanceName, deploymentName, apiVersion, and apiKey.
* @param {Buffer} audioBuffer - The audio data to be transcribed
* @param {Object} audioFile - The audio file object, which should contain originalname, mimetype, and size.
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* If an error occurs, it returns an array with three null values and logs the error with logger
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request.
* If an error occurs, it logs the error with logger and returns an array with three null values.
*/
function azureProvider(req, audioReadStream) {
function azureOpenAIProvider(sttSchema, audioBuffer, audioFile) {
try {
const { endpoint } = req.body;
const azureConfig = req.app.locals[endpoint];
const instanceName = sttSchema?.instanceName;
const deploymentName = sttSchema?.deploymentName;
const apiVersion = sttSchema?.apiVersion;
if (!azureConfig) {
throw new Error(`No configuration found for endpoint: ${endpoint}`);
const url =
genAzureEndpoint({
azureOpenAIApiInstanceName: instanceName,
azureOpenAIApiDeploymentName: deploymentName,
}) +
'/audio/transcriptions?api-version=' +
apiVersion;
const apiKey = sttSchema.apiKey ? extractEnvVariable(sttSchema.apiKey) : '';
if (audioBuffer.byteLength > 25 * 1024 * 1024) {
throw new Error('The audio file size exceeds the limit of 25MB');
}
const acceptedFormats = ['flac', 'mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'ogg', 'wav', 'webm'];
const fileFormat = audioFile.mimetype.split('/')[1];
if (!acceptedFormats.includes(fileFormat)) {
throw new Error(`The audio file format ${fileFormat} is not accepted`);
}
const { apiKey, instanceName, whisperModel, apiVersion } = Object.entries(
azureConfig.groupMap,
).reduce((acc, [, value]) => {
if (acc) {
return acc;
}
const formData = new FormData();
const whisperKey = Object.keys(value.models).find((modelKey) =>
modelKey.startsWith('whisper'),
);
const audioBlob = new Blob([audioBuffer], { type: audioFile.mimetype });
if (whisperKey) {
return {
apiVersion: value.version,
apiKey: value.apiKey,
instanceName: value.instanceName,
whisperModel: value.models[whisperKey]['deploymentName'],
};
}
formData.append('file', audioBlob, audioFile.originalname);
return null;
}, null);
let data = formData;
if (!apiKey || !instanceName || !whisperModel || !apiVersion) {
throw new Error('Required Azure configuration values are missing');
}
const baseURL = `https://${instanceName}.openai.azure.com`;
const url = `${baseURL}/openai/deployments/${whisperModel}/audio/transcriptions?api-version=${apiVersion}`;
let data = {
file: audioReadStream,
filename: 'audio.wav',
contentType: 'audio/wav',
knownLength: audioReadStream.length,
};
const headers = {
...data.getHeaders(),
let headers = {
'Content-Type': 'multipart/form-data',
'api-key': apiKey,
};
[headers].forEach(removeUndefined);
if (apiKey) {
headers['api-key'] = apiKey;
}
return [url, data, headers];
} catch (error) {
logger.error('An error occurred while preparing the Azure API STT request: ', error);
return [null, null, null];
logger.error('An error occurred while preparing the Azure OpenAI API STT request: ', error);
throw error;
}
}
@@ -176,16 +190,16 @@ async function speechToText(req, res) {
const audioReadStream = Readable.from(audioBuffer);
audioReadStream.path = 'audio.wav';
const provider = getProvider(customConfig.speech.stt);
const [provider, sttSchema] = await getProviderSchema(customConfig);
let [url, data, headers] = [];
switch (provider) {
case 'openai':
[url, data, headers] = openAIProvider(customConfig.speech.stt, audioReadStream);
case STTProviders.OPENAI:
[url, data, headers] = openAIProvider(sttSchema, audioReadStream);
break;
case 'azure':
[url, data, headers] = azureProvider(req, audioReadStream);
case STTProviders.AZURE_OPENAI:
[url, data, headers] = azureOpenAIProvider(sttSchema, audioBuffer, req.file);
break;
default:
throw new Error('Invalid provider');

View File

@@ -1,8 +1,9 @@
const axios = require('axios');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
const { extractEnvVariable } = require('librechat-data-provider');
const { extractEnvVariable, TTSProviders } = require('librechat-data-provider');
const { logger } = require('~/config');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { genAzureEndpoint } = require('~/utils');
const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
/**
* getProvider function
@@ -91,6 +92,59 @@ function openAIProvider(ttsSchema, input, voice) {
return [url, data, headers];
}
/**
* Generates the necessary parameters for making a request to Azure's OpenAI Text-to-Speech API.
*
* @param {TCustomConfig['tts']['azureOpenAI']} ttsSchema - The TTS schema containing the AzureOpenAI configuration
* @param {string} input - The text to be converted to speech
* @param {string} voice - The voice to be used for the speech
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* If an error occurs, it throws an error with a message indicating that the selected voice is not available
*/
function azureOpenAIProvider(ttsSchema, input, voice) {
const instanceName = ttsSchema?.instanceName;
const deploymentName = ttsSchema?.deploymentName;
const apiVersion = ttsSchema?.apiVersion;
const url =
genAzureEndpoint({
azureOpenAIApiInstanceName: instanceName,
azureOpenAIApiDeploymentName: deploymentName,
}) +
'/audio/speech?api-version=' +
apiVersion;
const apiKey = ttsSchema.apiKey ? extractEnvVariable(ttsSchema.apiKey) : '';
if (
ttsSchema?.voices &&
ttsSchema.voices.length > 0 &&
!ttsSchema.voices.includes(voice) &&
!ttsSchema.voices.includes('ALL')
) {
throw new Error(`Voice ${voice} is not available.`);
}
let data = {
model: ttsSchema?.model,
input,
voice: ttsSchema?.voices && ttsSchema.voices.length > 0 ? voice : undefined,
};
let headers = {
'Content-Type': 'application/json',
};
[data, headers].forEach(removeUndefined);
if (apiKey) {
headers['api-key'] = apiKey;
}
return [url, data, headers];
}
/**
* elevenLabsProvider function
* This function prepares the necessary data and headers for making a request to the Eleven Labs TTS
@@ -225,13 +279,16 @@ async function getVoice(providerSchema, requestVoice) {
async function ttsRequest(provider, ttsSchema, { input, voice, stream = true } = { stream: true }) {
let [url, data, headers] = [];
switch (provider) {
case 'openai':
case TTSProviders.OPENAI:
[url, data, headers] = openAIProvider(ttsSchema, input, voice);
break;
case 'elevenlabs':
case TTSProviders.AZURE_OPENAI:
[url, data, headers] = azureOpenAIProvider(ttsSchema, input, voice);
break;
case TTSProviders.ELEVENLABS:
[url, data, headers] = elevenLabsProvider(ttsSchema, input, voice, stream);
break;
case 'localai':
case TTSProviders.LOCALAI:
[url, data, headers] = localAIProvider(ttsSchema, input, voice);
break;
default:

View File

@@ -19,8 +19,7 @@ const createOnProgress = ({ generation = '', onProgress: _onProgress }) => {
const basePayload = Object.assign({}, base, { text: tokens || '' });
const progressCallback = (partial, { res, text, ...rest }) => {
let chunk = partial === text ? '' : partial;
const progressCallback = (chunk, { res, ...rest }) => {
basePayload.text = basePayload.text + chunk;
const payload = Object.assign({}, basePayload, rest);

View File

@@ -31,7 +31,7 @@
"@ariakit/react": "^0.4.5",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^1.7.13",
"@headlessui/react": "^2.1.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.3",
@@ -70,6 +70,7 @@
"match-sorter": "^6.3.4",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",
"react-avatar-editor": "^13.0.2",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",

View File

@@ -122,7 +122,7 @@ export default function PopoverButtons({
type="button"
className={cn(
button.buttonClass,
'border-2 border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'border border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'ml-1 h-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
buttonClass ?? '',
)}
@@ -141,7 +141,7 @@ export default function PopoverButtons({
type="button"
className={cn(
button.buttonClass,
'flex justify-center border-2 border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'flex justify-center border border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'h-full w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
buttonClass ?? '',
)}

View File

@@ -178,7 +178,7 @@ const SetKeyDialog = ({
value={expiresAtLabel}
onChange={handleExpirationChange}
options={expirationOptions.map((option) => option.display)}
width={185}
sizeClasses="w-[185px]"
/>
<FormProvider {...methods}>
<EndpointComponent

View File

@@ -1,4 +1,4 @@
import { Disclosure } from '@headlessui/react';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import { useCallback, memo, ReactNode } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TResPlugin, TInput } from 'librechat-data-provider';
@@ -98,12 +98,12 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
</div>
</div>
{plugin.loading && <Spinner className="ml-1 text-black" />}
<Disclosure.Button className="ml-12 flex items-center gap-2">
<DisclosureButton className="ml-12 flex items-center gap-2">
<ChevronDownIcon {...iconProps} />
</Disclosure.Button>
</DisclosureButton>
</div>
<Disclosure.Panel className="mt-3 flex max-w-full flex-col gap-3">
<DisclosurePanel className="mt-3 flex max-w-full flex-col gap-3">
<CodeBlock
lang={latestPlugin ? `REQUEST TO ${latestPlugin?.toUpperCase()}` : 'REQUEST'}
codeChildren={formatInputs(plugin.inputs ?? [])}
@@ -120,7 +120,7 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
classProp="max-h-[450px]"
/>
)}
</Disclosure.Panel>
</DisclosurePanel>
</>
);
}}

View File

@@ -1,7 +1,7 @@
import { FileText } from 'lucide-react';
import { useRecoilState } from 'recoil';
import { Fragment, useState, memo } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { Menu, MenuItem, MenuButton, MenuItems, Transition } from '@headlessui/react';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext';
@@ -32,7 +32,7 @@ function NavLinks() {
<Menu as="div" className="group relative">
{({ open }) => (
<>
<Menu.Button
<MenuButton
className={cn(
'group-ui-open:bg-gray-100 dark:group-ui-open:bg-gray-700 duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800',
open ? 'bg-gray-100 dark:bg-gray-800' : '',
@@ -64,7 +64,7 @@ function NavLinks() {
>
{user?.name || user?.username || localize('com_nav_user')}
</div>
</Menu.Button>
</MenuButton>
<Transition
as={Fragment}
@@ -75,7 +75,7 @@ function NavLinks() {
leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0"
>
<Menu.Items className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-gray-300 bg-white p-1.5 opacity-100 shadow-lg outline-none dark:border-gray-600 dark:bg-gray-700">
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-gray-300 bg-white p-1.5 opacity-100 shadow-lg outline-none dark:border-gray-600 dark:bg-gray-700">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
{user?.email || localize('com_nav_user')}
</div>
@@ -90,34 +90,34 @@ function NavLinks() {
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
</>
)}
<Menu.Item as="div">
<MenuItem as="div">
<NavLink
svg={() => <FileText className="icon-md" />}
text={localize('com_nav_my_files')}
clickHandler={() => setShowFiles(true)}
/>
</Menu.Item>
</MenuItem>
{startupConfig?.helpAndFaqURL !== '/' && (
<Menu.Item as="div">
<MenuItem as="div">
<NavLink
svg={() => <LinkIcon />}
text={localize('com_nav_help_faq')}
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
/>
</Menu.Item>
</MenuItem>
)}
<Menu.Item as="div">
<MenuItem as="div">
<NavLink
svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')}
clickHandler={() => setShowSettings(true)}
/>
</Menu.Item>
</MenuItem>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
<Menu.Item as="div">
<MenuItem as="div">
<Logout />
</Menu.Item>
</Menu.Items>
</MenuItem>
</MenuItems>
</Transition>
</>
)}

View File

@@ -2,7 +2,14 @@ import * as Tabs from '@radix-ui/react-tabs';
import { MessageSquare } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
import type { TDialogProps } from '~/common';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import {
Button,
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { GearIcon, DataIcon, SpeechIcon, UserIcon, ExperimentIcon } from '~/components/svg';
import { General, Messages, Speech, Beta, Data, Account } from './SettingsTabs';
import { useMediaQuery, useLocalize } from '~/hooks';
@@ -13,132 +20,183 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
const localize = useLocalize();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
disableScroll={isSmallScreen}
className={cn(
'max-h-[90vh] overflow-auto shadow-2xl md:min-h-[500px] md:w-[680px]',
isSmallScreen ? 'min-h-[200px]' : '',
)}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_nav_settings')}
</DialogTitle>
</DialogHeader>
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<Tabs.Root
defaultValue={SettingsTabValues.GENERAL}
className="flex flex-col gap-3 md:flex-row"
orientation="horizontal"
<Transition appear show={open}>
<Dialog as="div" className="relative z-50 focus:outline-none" onClose={onOpenChange}>
<TransitionChild
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/50 dark:bg-black/80" aria-hidden="true" />
</TransitionChild>
<TransitionChild
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className={cn(
'fixed inset-0 flex w-screen items-center justify-center p-4',
isSmallScreen ? '' : '',
)}
>
<Tabs.List
aria-label="Settings"
role="tablist"
aria-orientation="horizontal"
<DialogPanel
className={cn(
isSmallScreen
? 'hide-scrollbar flex flex-row space-x-4 overflow-x-auto'
: 'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-col flex-wrap overflow-auto sm:max-w-none',
'overflow-hidden rounded-xl rounded-b-lg bg-white pb-6 shadow-2xl backdrop-blur-2xl animate-in dark:bg-gray-700 sm:rounded-lg md:min-h-[373px] md:w-[680px]',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.GENERAL}
style={{ userSelect: 'none' }}
<DialogTitle
className="mb-3 flex items-center justify-between border-b border-black/10 p-6 pb-5 text-left dark:border-white/10"
as="div"
>
<GearIcon />
{localize('com_nav_setting_general')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.MESSAGES}
style={{ userSelect: 'none' }}
>
<MessageSquare className="icon-sm" />
{localize('com_endpoint_messages')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.BETA}
style={{ userSelect: 'none' }}
>
<ExperimentIcon />
{localize('com_nav_setting_beta')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.SPEECH}
style={{ userSelect: 'none' }}
>
<SpeechIcon className="icon-sm" />
{localize('com_nav_setting_speech')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.DATA}
style={{ userSelect: 'none' }}
>
<DataIcon />
{localize('com_nav_setting_data')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.ACCOUNT}
style={{ userSelect: 'none' }}
>
<UserIcon />
{localize('com_nav_setting_account')}
</Tabs.Trigger>
</Tabs.List>
<div className="h-auto min-h-[280px] overflow-auto sm:w-full sm:max-w-none">
<General />
<Messages />
<Beta />
<Speech />
<Data />
<Account />
</div>
</Tabs.Root>
</div>
</DialogContent>
</Dialog>
<h2 className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_nav_settings')}
</h2>
<button
type="button"
className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900 dark:data-[state=open]:bg-gray-800"
onClick={() => onOpenChange(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-5 w-5 text-black dark:text-white"
>
<line x1="18" x2="6" y1="6" y2="18"></line>
<line x1="6" x2="18" y1="6" y2="18"></line>
</svg>
<span className="sr-only">Close</span>
</button>
</DialogTitle>
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<Tabs.Root
defaultValue={SettingsTabValues.GENERAL}
className="flex flex-col gap-10 md:flex-row"
orientation="horizontal"
>
<Tabs.List
aria-label="Settings"
role="tablist"
aria-orientation="horizontal"
className={cn(
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-lg bg-gray-200 p-1 dark:bg-gray-800' : '',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.GENERAL}
style={{ userSelect: 'none' }}
>
<GearIcon />
{localize('com_nav_setting_general')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.MESSAGES}
style={{ userSelect: 'none' }}
>
<MessageSquare className="icon-sm" />
{localize('com_endpoint_messages')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.BETA}
style={{ userSelect: 'none' }}
>
<ExperimentIcon />
{localize('com_nav_setting_beta')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.SPEECH}
style={{ userSelect: 'none' }}
>
<SpeechIcon className="icon-sm" />
{localize('com_nav_setting_speech')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.DATA}
style={{ userSelect: 'none' }}
>
<DataIcon />
{localize('com_nav_setting_data')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.ACCOUNT}
style={{ userSelect: 'none' }}
>
<UserIcon />
{localize('com_nav_setting_account')}
</Tabs.Trigger>
</Tabs.List>
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
<General />
<Messages />
<Beta />
<Speech />
<Data />
<Account />
</div>
</Tabs.Root>
</div>
</DialogPanel>
</div>
</TransitionChild>
</Dialog>
</Transition>
);
}

View File

@@ -1,9 +1,10 @@
import { FileImage } from 'lucide-react';
import React, { useState, useRef, useCallback } from 'react';
import { FileImage, RotateCw, Upload } from 'lucide-react';
import { useSetRecoilState } from 'recoil';
import { useState, useEffect } from 'react';
import AvatarEditor from 'react-avatar-editor';
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
import type { TUser } from 'librechat-data-provider';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { Dialog, DialogContent, DialogHeader, DialogTitle, Slider } from '~/components/ui';
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { Spinner } from '~/components/svg';
@@ -13,9 +14,13 @@ import store from '~/store';
function Avatar() {
const setUser = useSetRecoilState(store.user);
const [input, setinput] = useState<File | null>(null);
const [image, setImage] = useState<string | File | null>(null);
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [scale, setScale] = useState<number>(1);
const [rotation, setRotation] = useState<number>(0);
const editorRef = useRef<AvatarEditor | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
@@ -27,7 +32,6 @@ function Avatar() {
onSuccess: (data) => {
showToast({ message: localize('com_ui_upload_success') });
setDialogOpen(false);
setUser((prev) => ({ ...prev, avatar: data.url } as TUser));
},
onError: (error) => {
@@ -36,24 +40,16 @@ function Avatar() {
},
});
useEffect(() => {
if (input) {
const reader = new FileReader();
reader.onloadend = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(input);
} else {
setPreviewUrl(null);
}
}, [input]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
handleFile(file);
};
const handleFile = (file: File | undefined) => {
if (fileConfig.avatarSizeLimit && file && file.size <= fileConfig.avatarSizeLimit) {
setinput(file);
setDialogOpen(true);
setImage(file);
setScale(1);
setRotation(0);
} else {
const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2;
showToast({
@@ -63,78 +59,152 @@ function Avatar() {
}
};
const handleUpload = () => {
if (!input) {
console.error('No file selected');
return;
}
const formData = new FormData();
formData.append('input', input, input.name);
formData.append('manual', 'true');
uploadAvatar(formData);
const handleScaleChange = (value: number[]) => {
setScale(value[0]);
};
const handleRotate = () => {
setRotation((prev) => (prev + 90) % 360);
};
const handleUpload = () => {
if (editorRef.current) {
const canvas = editorRef.current.getImageScaledToCanvas();
canvas.toBlob((blob) => {
if (blob) {
const formData = new FormData();
formData.append('input', blob, 'avatar.png');
formData.append('manual', 'true');
uploadAvatar(formData);
}
}, 'image/png');
}
};
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
handleFile(file);
}, []);
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
}, []);
const openFileDialog = () => {
fileInputRef.current?.click();
};
const resetImage = useCallback(() => {
setImage(null);
setScale(1);
setRotation(0);
}, []);
return (
<>
<div className="flex items-center justify-between">
<span>{localize('com_nav_profile_picture')}</span>
<label htmlFor={'file-upload-avatar'} className="btn btn-neutral relative">
<button onClick={() => setDialogOpen(true)} className="btn btn-neutral relative">
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
<span>{localize('com_nav_change_picture')}</span>
<input
id={'file-upload-avatar'}
value=""
type="file"
className={cn('hidden')}
accept=".png, .jpg"
onChange={handleFileChange}
/>
</label>
</button>
</div>
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
setDialogOpen(open);
if (!open) {
resetImage();
}
}}
>
<DialogContent
className={cn('shadow-2xl dark:bg-gray-700 dark:text-white md:h-[350px] md:w-[450px] ')}
className={cn('shadow-2xl dark:bg-gray-700 dark:text-white md:h-auto md:w-[450px]')}
style={{ borderRadius: '12px' }}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_ui_preview')}
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center justify-center">
{previewUrl && (
<img
src={previewUrl}
alt="Preview"
className="mb-2 rounded-full"
style={{
maxWidth: '100%',
maxHeight: '150px',
width: '150px',
height: '150px',
objectFit: 'cover',
}}
/>
)}
<button
className={cn(
'mt-4 rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
)}
onClick={handleUpload}
disabled={isUploading}
>
{isUploading ? (
<div className="flex h-6">
<Spinner className="icon-sm m-auto" />
{image ? (
<>
<div className="relative overflow-hidden rounded-full">
<AvatarEditor
ref={editorRef}
image={image}
width={250}
height={250}
border={0}
borderRadius={125}
color={[255, 255, 255, 0.6]}
scale={scale}
rotate={rotation}
/>
</div>
) : (
localize('com_ui_upload')
)}
</button>
<div className="mt-4 flex w-full flex-col items-center space-y-4">
<div className="flex w-full items-center justify-center space-x-4">
<span className="text-sm">Zoom:</span>
<Slider
value={[scale]}
min={1}
max={5}
step={0.001}
onValueChange={handleScaleChange}
className="w-2/3 max-w-xs"
/>
</div>
<button
onClick={handleRotate}
className="rounded-full bg-gray-200 p-2 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500"
>
<RotateCw className="h-5 w-5" />
</button>
</div>
<button
className={cn(
'mt-4 flex items-center rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
)}
onClick={handleUpload}
disabled={isUploading}
>
{isUploading ? (
<Spinner className="icon-sm mr-2" />
) : (
<Upload className="mr-2 h-5 w-5" />
)}
{localize('com_ui_upload')}
</button>
</>
) : (
<div
className="flex h-64 w-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700"
onDrop={handleDrop}
onDragOver={handleDragOver}
>
<FileImage className="mb-4 h-12 w-12 text-gray-400" />
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
{localize('com_ui_drag_drop')}
</p>
<button
onClick={openFileDialog}
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
>
{localize('com_ui_select_file')}
</button>
<input
ref={fileInputRef}
type="file"
className="hidden"
accept=".png, .jpg, .jpeg"
onChange={handleFileChange}
/>
</div>
)}
</div>
</DialogContent>
</Dialog>

View File

@@ -34,9 +34,8 @@ export const ThemeSelector = ({
value={theme}
onChange={onChange}
options={themeOptions}
width={180}
position={'left'}
maxHeight="200px"
sizeClasses="w-[220px]"
anchor="bottom start"
testId="theme-selector"
/>
</div>
@@ -102,6 +101,7 @@ export const LangSelector = ({
{ value: 'nl-NL', display: localize('com_nav_lang_dutch') },
{ value: 'id-ID', display: localize('com_nav_lang_indonesia') },
{ value: 'he-HE', display: localize('com_nav_lang_hebrew') },
{ value: 'fi-FI', display: localize('com_nav_lang_finnish') },
];
return (
@@ -111,8 +111,8 @@ export const LangSelector = ({
<Dropdown
value={langcode}
onChange={onChange}
position={'left'}
maxHeight="271px"
sizeClasses="[--anchor-max-height:256px]"
anchor="bottom start"
options={languageOptions}
/>
</div>

View File

@@ -13,6 +13,11 @@ describe('LangSelector', () => {
});
it('renders correctly', () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />
@@ -24,6 +29,11 @@ describe('LangSelector', () => {
});
it('calls onChange when the select value changes', async () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText, getByTestId } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />

View File

@@ -1,6 +1,8 @@
// ThemeSelector.spec.tsx
import 'test/matchMedia.mock';
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { ThemeSelector } from './General';
import { RecoilRoot } from 'recoil';
@@ -13,6 +15,11 @@ describe('ThemeSelector', () => {
});
it('renders correctly', () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
@@ -24,6 +31,11 @@ describe('ThemeSelector', () => {
});
it('calls onChange when the select value changes', async () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText, getByTestId } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
@@ -42,9 +54,9 @@ describe('ThemeSelector', () => {
const darkOption = getByText('Dark');
fireEvent.click(darkOption);
// Ensure that the onChange is called with the expected value after a short delay
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockOnChange).toHaveBeenCalledWith('dark');
// Ensure that the onChange is called with the expected value
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith('dark');
});
});
});

View File

@@ -29,8 +29,8 @@ export const ForkSettings = () => {
value={forkSetting}
onChange={setForkSetting}
options={forkOptions}
position={'left'}
maxHeight="199px"
sizeClasses="w-[200px]"
anchor="bottom start"
testId="fork-setting-dropdown"
/>
</div>

View File

@@ -22,8 +22,8 @@ export default function EngineSTTDropdown() {
value={engineSTT}
onChange={handleSelect}
options={endpointOptions}
width={180}
position={'left'}
sizeClasses="w-[180px]"
anchor="bottom start"
testId="EngineSTTDropdown"
/>
</div>

View File

@@ -98,8 +98,8 @@ export default function LanguageSTTDropdown() {
value={languageSTT}
onChange={handleSelect}
options={languageOptions}
width={220}
position={'left'}
sizeClasses="[--anchor-max-height:256px]"
anchor="bottom start"
testId="LanguageSTTDropdown"
/>
</div>

View File

@@ -8,26 +8,27 @@ import store from '~/store';
import { cn } from '~/utils';
import ConversationModeSwitch from './ConversationModeSwitch';
import {
CloudBrowserVoicesSwitch,
AutomaticPlaybackSwitch,
TextToSpeechSwitch,
EngineTTSDropdown,
AutomaticPlaybackSwitch,
CacheTTSSwitch,
VoiceDropdown,
PlaybackRate,
} from './TTS';
import {
DecibelSelector,
EngineSTTDropdown,
AutoTranscribeAudioSwitch,
LanguageSTTDropdown,
SpeechToTextSwitch,
AutoSendTextSwitch,
AutoTranscribeAudioSwitch,
EngineSTTDropdown,
DecibelSelector,
} from './STT';
import { useCustomConfigSpeechQuery } from '~/data-provider';
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
function Speech() {
const [confirmClear, setConfirmClear] = useState(false);
const { data } = useCustomConfigSpeechQuery();
const { data } = useGetCustomConfigSpeechQuery();
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const [advancedMode, setAdvancedMode] = useRecoilState(store.advancedMode);
@@ -42,6 +43,9 @@ function Speech() {
const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText);
const [engineTTS, setEngineTTS] = useRecoilState<string>(store.engineTTS);
const [voice, setVoice] = useRecoilState<string>(store.voice);
const [cloudBrowserVoices, setCloudBrowserVoices] = useRecoilState<boolean>(
store.cloudBrowserVoices,
);
const [languageTTS, setLanguageTTS] = useRecoilState<string>(store.languageTTS);
const [automaticPlayback, setAutomaticPlayback] = useRecoilState(store.automaticPlayback);
const [playbackRate, setPlaybackRate] = useRecoilState(store.playbackRate);
@@ -61,15 +65,18 @@ function Speech() {
autoSendText: { value: autoSendText, setFunc: setAutoSendText },
engineTTS: { value: engineTTS, setFunc: setEngineTTS },
voice: { value: voice, setFunc: setVoice },
cloudBrowserVoices: { value: cloudBrowserVoices, setFunc: setCloudBrowserVoices },
languageTTS: { value: languageTTS, setFunc: setLanguageTTS },
automaticPlayback: { value: automaticPlayback, setFunc: setAutomaticPlayback },
playbackRate: { value: playbackRate, setFunc: setPlaybackRate },
};
if (settings[key]) {
const setting = settings[key];
setting.setFunc(newValue);
if (settings[key].value !== newValue || settings[key].value === newValue || !settings[key]) {
return;
}
const setting = settings[key];
setting.setFunc(newValue);
},
[
conversationMode,
@@ -84,6 +91,7 @@ function Speech() {
autoSendText,
engineTTS,
voice,
cloudBrowserVoices,
languageTTS,
automaticPlayback,
playbackRate,
@@ -99,6 +107,7 @@ function Speech() {
setAutoSendText,
setEngineTTS,
setVoice,
setCloudBrowserVoices,
setLanguageTTS,
setAutomaticPlayback,
setPlaybackRate,
@@ -111,7 +120,8 @@ function Speech() {
updateSetting(key, value);
});
}
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
@@ -120,7 +130,7 @@ function Speech() {
<Tabs.Content
value={SettingsTabValues.SPEECH}
role="tabpanel"
className="w-full px-4 md:min-h-[300px]"
className="w-full md:min-h-[271px]"
ref={contentRef}
>
<Tabs.Root
@@ -128,8 +138,8 @@ function Speech() {
orientation="horizontal"
value={advancedMode ? 'advanced' : 'simple'}
>
<div className="sticky top-0 z-50 bg-white dark:bg-gray-700">
<Tabs.List className="sticky top-0 mb-4 flex justify-center bg-white dark:bg-gray-700">
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
<Tabs.List className="flex justify-center bg-white dark:bg-gray-700">
<Tabs.Trigger
onClick={() => setAdvancedMode(false)}
className={cn(
@@ -165,27 +175,23 @@ function Speech() {
<Tabs.Content value={'simple'}>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<ConversationModeSwitch />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<SpeechToTextSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineSTTDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<LanguageSTTDropdown />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<TextToSpeechSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineTTSDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<VoiceDropdown />
</div>
</div>
@@ -193,47 +199,52 @@ function Speech() {
<Tabs.Content value={'advanced'}>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<ConversationModeSwitch />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<SpeechToTextSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineSTTDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<LanguageSTTDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<AutoTranscribeAudioSwitch />
</div>
{autoTranscribeAudio && (
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<DecibelSelector />
</div>
)}
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<AutoSendTextSwitch />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<TextToSpeechSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<AutomaticPlaybackSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineTTSDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<VoiceDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
{engineTTS === 'browser' && (
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<CloudBrowserVoicesSwitch />
</div>
)}
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<PlaybackRate />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<CacheTTSSwitch />
</div>
</div>

View File

@@ -0,0 +1,37 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function CloudBrowserVoicesSwitch({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const localize = useLocalize();
const [cloudBrowserVoices, setCloudBrowserVoices] = useRecoilState<boolean>(
store.cloudBrowserVoices,
);
const [textToSpeech] = useRecoilState<boolean>(store.textToSpeech);
const handleCheckedChange = (value: boolean) => {
setCloudBrowserVoices(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_enable_cloud_browser_voice')}</div>
<Switch
id="CloudBrowserVoices"
checked={cloudBrowserVoices}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="CloudBrowserVoices"
disabled={!textToSpeech}
/>
</div>
);
}

View File

@@ -22,8 +22,8 @@ export default function EngineTTSDropdown() {
value={engineTTS}
onChange={handleSelect}
options={endpointOptions}
width={180}
position={'left'}
sizeClasses="w-[180px]"
anchor="bottom start"
testId="EngineTTSDropdown"
/>
</div>

View File

@@ -1,34 +1,75 @@
import React, { useMemo, useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { useMemo, useEffect } from 'react';
import Dropdown from '~/components/ui/DropdownNoState';
import { useVoicesQuery } from '~/data-provider';
import { useLocalize } from '~/hooks';
import store from '~/store';
const getLocalVoices = (): Promise<SpeechSynthesisVoice[]> => {
return new Promise((resolve) => {
const voices = speechSynthesis.getVoices();
console.log('voices', voices);
if (voices.length) {
resolve(voices);
} else {
speechSynthesis.onvoiceschanged = () => resolve(speechSynthesis.getVoices());
}
});
};
type VoiceOption = {
value: string;
display: string;
};
export default function VoiceDropdown() {
const localize = useLocalize();
const [voice, setVoice] = useRecoilState(store.voice);
const { data } = useVoicesQuery();
const [engineTTS] = useRecoilState(store.engineTTS);
const [cloudBrowserVoices] = useRecoilState(store.cloudBrowserVoices);
const externalTextToSpeech = engineTTS === 'external';
const { data: externalVoices = [] } = useVoicesQuery();
const [localVoices, setLocalVoices] = useState<SpeechSynthesisVoice[]>([]);
useEffect(() => {
if (!voice && data?.length) {
setVoice(data[0]);
if (!externalTextToSpeech) {
getLocalVoices().then(setLocalVoices);
}
}, [voice, data, setVoice]);
}, [externalTextToSpeech]);
const voiceOptions = useMemo(
() => (data ?? []).map((v: string) => ({ value: v, display: v })),
[data],
);
useEffect(() => {
if (voice) {
return;
}
if (externalTextToSpeech && externalVoices.length) {
setVoice(externalVoices[0]);
} else if (!externalTextToSpeech && localVoices.length) {
setVoice(localVoices[0].name);
}
}, [voice, setVoice, externalTextToSpeech, externalVoices, localVoices]);
const voiceOptions: VoiceOption[] = useMemo(() => {
if (externalTextToSpeech) {
return externalVoices.map((v) => ({ value: v, display: v }));
} else {
return localVoices
.filter((v) => cloudBrowserVoices || v.localService === true)
.map((v) => ({ value: v.name, display: v.name }));
}
}, [externalTextToSpeech, externalVoices, localVoices, cloudBrowserVoices]);
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_voice_select')}</div>
<Dropdown
value={voice}
onChange={(value: string) => setVoice(value)}
onChange={setVoice}
options={voiceOptions}
position={'left'}
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
anchor="bottom start"
position="left"
testId="VoiceDropdown"
/>
</div>

View File

@@ -0,0 +1,41 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render, fireEvent } from 'test/layout-test-utils';
import CloudBrowserVoicesSwitch from '../CloudBrowserVoicesSwitch';
import { RecoilRoot } from 'recoil';
describe('CloudBrowserVoicesSwitch', () => {
/**
* Mock function to set the cache-tts state.
*/
let mockSetCloudBrowserVoices:
| jest.Mock<void, [boolean]>
| ((value: boolean) => void)
| undefined;
beforeEach(() => {
mockSetCloudBrowserVoices = jest.fn();
});
it('renders correctly', () => {
const { getByTestId } = render(
<RecoilRoot>
<CloudBrowserVoicesSwitch />
</RecoilRoot>,
);
expect(getByTestId('CloudBrowserVoices')).toBeInTheDocument();
});
it('calls onCheckedChange when the switch is toggled', () => {
const { getByTestId } = render(
<RecoilRoot>
<CloudBrowserVoicesSwitch onCheckedChange={mockSetCloudBrowserVoices} />
</RecoilRoot>,
);
const switchElement = getByTestId('CloudBrowserVoices');
fireEvent.click(switchElement);
expect(mockSetCloudBrowserVoices).toHaveBeenCalledWith(true);
});
});

View File

@@ -1,6 +1,7 @@
export { default as CloudBrowserVoicesSwitch } from './CloudBrowserVoicesSwitch';
export { default as AutomaticPlaybackSwitch } from './AutomaticPlaybackSwitch';
export { default as CacheTTSSwitch } from './CacheTTSSwitch';
export { default as EngineTTSDropdown } from './EngineTTSDropdown';
export { default as PlaybackRate } from './PlaybackRate';
export { default as TextToSpeechSwitch } from './TextToSpeechSwitch';
export { default as EngineTTSDropdown } from './EngineTTSDropdown';
export { default as CacheTTSSwitch } from './CacheTTSSwitch';
export { default as VoiceDropdown } from './VoiceDropdown';
export { default as PlaybackRate } from './PlaybackRate';

View File

@@ -1,5 +1,5 @@
import { Search, X } from 'lucide-react';
import { Dialog } from '@headlessui/react';
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react';
import { useState, useEffect, useCallback } from 'react';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TError, TPlugin, TPluginAction } from 'librechat-data-provider';
@@ -134,16 +134,16 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
<div className="fixed inset-0 bg-gray-600/65 transition-opacity dark:bg-black/80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-700 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 p-6 pb-4 dark:border-white/10">
<div className="flex items-center">
<div className="text-center sm:text-left">
<Dialog.Title className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_nav_plugin_store')}
</Dialog.Title>
</DialogTitle>
</div>
</div>
<div>
@@ -236,7 +236,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
</div> */}
</div>
</div>
</Dialog.Panel>
</DialogPanel>
</div>
</Dialog>
);

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { Dialog } from '@headlessui/react';
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
import { useFormContext } from 'react-hook-form';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { AssistantsEndpoint, TError, TPluginAction } from 'librechat-data-provider';
@@ -142,19 +142,19 @@ function ToolSelectDialog({
<div className="fixed inset-0 bg-gray-600/65 transition-opacity dark:bg-black/80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-800 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
<div className="flex items-center">
<div className="text-center sm:text-left">
<Dialog.Title className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_tool_dialog')}
</Dialog.Title>
<Dialog.Description className="text-sm text-gray-500 dark:text-gray-300">
</DialogTitle>
<Description className="text-sm text-gray-500 dark:text-gray-300">
{localize('com_nav_tool_dialog_description')}
</Dialog.Description>
</Description>
</div>
</div>
<div>
@@ -232,7 +232,7 @@ function ToolSelectDialog({
)}
</div>
</div>
</Dialog.Panel>
</DialogPanel>
</div>
</Dialog>
);

View File

@@ -1,5 +1,12 @@
import React, { FC, useState } from 'react';
import { Listbox } from '@headlessui/react';
import React, { FC, useContext, useState } from 'react';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
Transition,
} from '@headlessui/react';
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
import { cn } from '~/utils/';
type OptionType = {
@@ -7,17 +14,14 @@ type OptionType = {
display?: string;
};
type DropdownPosition = 'left' | 'right';
interface DropdownProps {
value: string;
label?: string;
onChange: (value: string) => void;
options: (string | OptionType)[];
className?: string;
position?: DropdownPosition;
width?: number;
maxHeight?: string;
anchor?: AnchorPropsWithSelection;
sizeClasses?: string;
testId?: string;
}
@@ -27,18 +31,12 @@ const Dropdown: FC<DropdownProps> = ({
onChange,
options,
className = '',
position = 'right',
width,
maxHeight = 'auto',
anchor,
sizeClasses,
testId = 'dropdown-menu',
}) => {
const [selectedValue, setSelectedValue] = useState(initialValue);
const positionClasses = {
right: 'origin-bottom-left left-0',
left: 'origin-bottom-right right-0',
};
return (
<div className={cn('relative', className)}>
<Listbox
@@ -49,7 +47,7 @@ const Dropdown: FC<DropdownProps> = ({
}}
>
<div className={cn('relative', className)}>
<Listbox.Button
<ListboxButton
data-testid={testId}
className={cn(
'relative inline-flex items-center justify-between rounded-md border-gray-50 bg-white py-2 pl-3 pr-8 text-black transition-all duration-100 ease-in-out hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white dark:focus:ring-offset-gray-700',
@@ -76,52 +74,59 @@ const Dropdown: FC<DropdownProps> = ({
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
<Listbox.Options
className={cn(
`absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-100 bg-white p-1.5 text-black shadow-lg transition-opacity dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
className,
)}
style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
aria-label="List of options"
</ListboxButton>
<Transition
leave="transition ease-in duration-50"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
{options.map((item, index) => (
<Listbox.Option
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'duration-50 relative cursor-pointer select-none rounded border-gray-50 bg-white py-2.5 pl-3 pr-2 text-black transition-all ease-in-out hover:bg-gray-100 focus:bg-gray-200 dark:border-gray-50 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:bg-gray-500',
)}
style={{ width: '100%' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
>
<div className="flex w-full items-center justify-between">
<span className="block truncate">
{typeof item === 'string' ? item : (item as OptionType).display}
</span>
{selectedValue === (typeof item === 'string' ? item : item.value) && (
<span className="ml-auto pl-2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md block group-hover:hidden"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</span>
<ListboxOptions
className={cn(
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white',
sizeClasses,
className,
)}
anchor={anchor}
aria-label="List of options"
>
{options.map((item, index) => (
<ListboxOption
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-3 text-sm text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
)}
</div>
</Listbox.Option>
))}
</Listbox.Options>
style={{ width: '100%' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
>
<div className="flex w-full items-center justify-between">
<span className="block truncate">
{typeof item === 'string' ? item : (item as OptionType).display}
</span>
{selectedValue === (typeof item === 'string' ? item : item.value) && (
<span className="ml-auto pl-2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md block group-hover:hidden"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</span>
)}
</div>
</ListboxOption>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
</div>

View File

@@ -1,5 +1,12 @@
import React, { FC } from 'react';
import { Listbox } from '@headlessui/react';
import React, { FC, useContext, useState } from 'react';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
Transition,
} from '@headlessui/react';
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
import { cn } from '~/utils/';
type OptionType = {
@@ -7,17 +14,14 @@ type OptionType = {
display?: string;
};
type DropdownPosition = 'left' | 'right';
interface DropdownProps {
value: string;
label?: string;
onChange: (value: string) => void;
options: (string | OptionType)[];
className?: string;
position?: DropdownPosition;
width?: number;
maxHeight?: string;
anchor?: AnchorPropsWithSelection;
sizeClasses?: string;
testId?: string;
}
@@ -27,16 +31,10 @@ const Dropdown: FC<DropdownProps> = ({
onChange,
options,
className = '',
position = 'right',
width,
maxHeight = 'auto',
anchor,
sizeClasses,
testId = 'dropdown-menu',
}) => {
const positionClasses = {
right: 'origin-bottom-left left-0',
left: 'origin-bottom-right right-0',
};
return (
<div className={cn('relative', className)}>
<Listbox
@@ -46,13 +44,14 @@ const Dropdown: FC<DropdownProps> = ({
}}
>
<div className={cn('relative', className)}>
<Listbox.Button
<ListboxButton
data-testid={testId}
className={cn(
'relative inline-flex items-center justify-between rounded-md border-gray-300 bg-white py-2 pl-3 pr-8 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
'relative inline-flex items-center justify-between rounded-md border-gray-50 bg-white py-2 pl-3 pr-8 text-black transition-all duration-100 ease-in-out hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white dark:focus:ring-offset-gray-700',
'w-auto',
className,
)}
aria-label="Select an option"
>
<span className="block truncate">
{label}
@@ -67,35 +66,45 @@ const Dropdown: FC<DropdownProps> = ({
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className="h-4 w-5 rotate-0 transform text-gray-400 transition-transform duration-300 ease-in-out"
className="h-4 w-5 rotate-0 transform text-black transition-transform duration-300 ease-in-out dark:text-gray-50"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
<Listbox.Options
className={cn(
`absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
className,
)}
style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
</ListboxButton>
<Transition
leave="transition ease-in duration-50"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
{options.map((item, index) => (
<Listbox.Option
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-6 text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
)}
style={{ width: '100%' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
>
<span className="block truncate">
{typeof item === 'string' ? item : (item as OptionType).display}
</span>
</Listbox.Option>
))}
</Listbox.Options>
<ListboxOptions
className={cn(
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white',
sizeClasses,
className,
)}
anchor={anchor}
aria-label="List of options"
>
{options.map((item, index) => (
<ListboxOption
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-3 text-sm text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
)}
style={{ width: '100%' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
>
<div className="flex w-full items-center justify-between">
<span className="block truncate">
{typeof item === 'string' ? item : (item as OptionType).display}
</span>
</div>
</ListboxOption>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
</div>

View File

@@ -1,5 +1,12 @@
import React, { useState, useRef } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import {
Listbox,
ListboxButton,
Label,
ListboxOptions,
ListboxOption,
Transition,
} from '@headlessui/react';
import { Wrench, ArrowRight } from 'lucide-react';
import { CheckMark } from '~/components/svg';
import useOnClickOutside from '~/hooks/useOnClickOutside';
@@ -74,7 +81,7 @@ function MultiSelectDropDown({
<Listbox value={value} onChange={handleSelect} disabled={disabled}>
{() => (
<>
<Listbox.Button
<ListboxButton
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
className ?? '',
@@ -85,13 +92,13 @@ function MultiSelectDropDown({
>
{' '}
{showLabel && (
<Listbox.Label
<Label
className={cn('block text-xs text-gray-700 dark:text-gray-500', labelClassName)}
id={excludeIds[1]}
data-headlessui-state=""
>
{title}
</Listbox.Label>
</Label>
)}
<span className="inline-flex w-full truncate" id={excludeIds[2]}>
<span
@@ -144,7 +151,7 @@ function MultiSelectDropDown({
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
</ListboxButton>
<Transition
show={isOpen}
as={React.Fragment}
@@ -153,7 +160,7 @@ function MultiSelectDropDown({
leaveTo="opacity-0"
{...transitionProps}
>
<Listbox.Options
<ListboxOptions
ref={menuRef}
className={cn(
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]',
@@ -167,7 +174,7 @@ function MultiSelectDropDown({
}
const selected = isSelected(option[optionValueKey]);
return (
<Listbox.Option
<ListboxOption
key={i}
value={option[optionValueKey]}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-800 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
@@ -208,10 +215,10 @@ function MultiSelectDropDown({
</span>
)}
</span>
</Listbox.Option>
</ListboxOption>
);
})}
</Listbox.Options>
</ListboxOptions>
</Transition>
</>
)}

View File

@@ -1,5 +1,12 @@
import React from 'react';
import { Listbox, Transition } from '@headlessui/react';
import {
Listbox,
ListboxButton,
Label,
ListboxOptions,
ListboxOption,
Transition,
} from '@headlessui/react';
import type { Option, OptionWithIcon } from '~/common';
import CheckMark from '../svg/CheckMark';
import { useLocalize } from '~/hooks';
@@ -84,8 +91,7 @@ function SelectDropDown({
<Listbox value={value} onChange={setValue} disabled={disabled}>
{({ open }) => (
<>
<Listbox.Button
tabIndex={tabIndex}
<ListboxButton
data-testid="select-dropdown-button"
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
@@ -94,13 +100,13 @@ function SelectDropDown({
>
{' '}
{showLabel && (
<Listbox.Label
<Label
className="block text-xs text-gray-700 dark:text-gray-500 "
id="headlessui-listbox-label-:r1:"
data-headlessui-state=""
>
{title}
</Listbox.Label>
</Label>
)}
<span className="inline-flex w-full truncate">
<span
@@ -138,7 +144,7 @@ function SelectDropDown({
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
</ListboxButton>
<Transition
show={open}
as={React.Fragment}
@@ -147,14 +153,14 @@ function SelectDropDown({
leaveTo="opacity-0"
{...transitionProps}
>
<Listbox.Options
<ListboxOptions
className={cn(
'absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded border bg-white text-xs ring-black/10 dark:border-gray-600 dark:bg-gray-700 dark:ring-white/20 md:w-[100%]',
optionsListClass ?? '',
)}
>
{renderOption && (
<Listbox.Option
<ListboxOption
key={'listbox-render-option'}
value={null}
className={cn(
@@ -163,7 +169,7 @@ function SelectDropDown({
)}
>
{renderOption()}
</Listbox.Option>
</ListboxOption>
)}
{searchRender}
{options.map((option: string | Option, i: number) => {
@@ -181,7 +187,7 @@ function SelectDropDown({
}
return (
<Listbox.Option
<ListboxOption
key={i}
value={currentValue}
className={({ active }) =>
@@ -214,10 +220,10 @@ function SelectDropDown({
</span>
)}
</span>
</Listbox.Option>
</ListboxOption>
);
})}
</Listbox.Options>
</ListboxOptions>
</Transition>
</>
)}

View File

@@ -107,7 +107,7 @@ export default function useChatFunctions({
const intermediateId = overrideUserMessageId ?? v4();
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT;
if (conversationId == 'new') {
if (conversationId == Constants.NEW_CONVO) {
parentMessageId = Constants.NO_PARENT;
currentMessages = [];
conversationId = null;

View File

@@ -10,6 +10,14 @@ const invalidKeys = {
Escape: true,
Backspace: true,
Enter: true,
ArrowUp: true,
ArrowDown: true,
ArrowLeft: true,
ArrowRight: true,
Home: true,
End: true,
PageUp: true,
PageDown: true,
};
/**
@@ -19,21 +27,19 @@ const shouldTriggerCommand = (
textAreaRef: React.RefObject<HTMLTextAreaElement>,
commandChar: string,
) => {
const text = textAreaRef.current?.value;
if (!(text && text[text.length - 1] === commandChar)) {
const textArea = textAreaRef.current;
if (!textArea) {
return false;
}
const startPos = textAreaRef.current?.selectionStart;
if (!startPos) {
const text = textArea.value;
const cursorPosition = textArea.selectionStart;
if (cursorPosition !== text.length || text.length !== 1 || text[0] !== commandChar) {
return false;
}
const isAtStart = startPos === 1;
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
const shouldTrigger = isAtStart || isPrecededBySpace;
return shouldTrigger;
return true;
};
/**

View File

@@ -1,12 +1,24 @@
import { useRecoilState } from 'recoil';
import { useState } from 'react';
import store from '~/store';
function useTextToSpeechBrowser() {
const [cloudBrowserVoices] = useRecoilState(store.cloudBrowserVoices);
const [isSpeaking, setIsSpeaking] = useState(false);
const [voiceName] = useRecoilState(store.voice);
const generateSpeechLocal = (text: string) => {
const synth = window.speechSynthesis;
const voices = synth.getVoices().filter((v) => cloudBrowserVoices || v.localService === true);
const voice = voices.find((v) => v.name === voiceName);
if (!voice) {
return;
}
synth.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = voice;
utterance.onend = () => {
setIsSpeaking(false);
};

View File

@@ -3,8 +3,11 @@ import { useMemo, useRef, useEffect, useCallback } from 'react';
import { usePromptGroupsInfiniteQuery } from '~/data-provider';
import debounce from 'lodash/debounce';
import store from '~/store';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
export default function usePromptGroupsNav() {
const queryClient = useQueryClient();
const category = useRecoilValue(store.promptsCategory);
const [name, setName] = useRecoilState(store.promptsName);
const [pageSize, setPageSize] = useRecoilState(store.promptsPageSize);
@@ -28,6 +31,7 @@ export default function usePromptGroupsNav() {
useEffect(() => {
maxPageNumberReached.current = 1;
setPageNumber(1);
queryClient.resetQueries([QueryKeys.promptGroups, name, category, pageSize]);
}, [pageSize, name, category, setPageNumber]);
const promptGroups = useMemo(() => {

View File

@@ -17,6 +17,7 @@ import Turkish from './languages/Tr';
import Dutch from './languages/Nl';
import Indonesia from './languages/Id';
import Hebrew from './languages/He';
import Finnish from './languages/Fi';
// === import additional language files here === //
@@ -42,6 +43,7 @@ const languageMap: Record<string, Language> = {
'nl-NL': Dutch,
'id-ID': Indonesia,
'he-HE': Hebrew,
'fi-FI': Finnish,
// Add additional language mappings here
};

View File

@@ -559,6 +559,9 @@ export default {
com_ui_code: 'Code',
com_ui_travel: 'Travel',
com_ui_teach_or_explain: 'Learning',
com_ui_select_file: 'Select a file',
com_ui_drag_drop_file: 'Drag and drop a file here',
com_ui_upload_image: 'Upload an image',
com_ui_select_a_category: 'No category selected',
com_nav_tool_dialog_description: 'Assistant must be saved to persist tool selections.',
com_show_agent_settings: 'Show Agent Settings',
@@ -638,6 +641,7 @@ export default {
com_nav_delete_cache_storage: 'Delete TTS cache storage',
com_nav_enable_cache_tts: 'Enable cache TTS',
com_nav_voice_select: 'Voice',
com_nav_enable_cloud_browser_voice: 'Use cloud-based voices',
com_nav_info_enter_to_send:
'When enabled, pressing `ENTER` will send your message. When disabled, pressing Enter will add a new line, and you\'ll need to press `CTRL + ENTER` to send your message.',
com_nav_info_save_draft:
@@ -680,4 +684,5 @@ export default {
com_nav_lang_dutch: 'Nederlands',
com_nav_lang_indonesia: 'Indonesia',
com_nav_lang_hebrew: 'עברית',
com_nav_lang_finnish: 'Suomi',
};

View File

@@ -0,0 +1,664 @@
// Finnish phrases
// file deepcode ignore NoHardcodedPasswords: No hardcoded values present in this file
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
export default {
com_error_moderation:
'Näyttää siltä, että moderointijärjestelmämme merkitsi lähetetyn sisällön yhteisön sääntöjen vastaisiksi. Emme voi jatkaa tämän aiheen käsittelyä. Jos sinulla on muita kysymyksiä tai aiheita joita haluaisit käsitellä, ole hyvä ja muokkaa viestiäsi, tai aloita uusi keskustelu.',
com_error_no_user_key: 'Avainta ei löytynyt. Lisää avain ja yritä uudestaan.',
com_error_no_base_url: 'Base URL puuttuu. Syötä URL ja yritä uudestaan.',
com_error_invalid_user_key: 'Avain ei kelpaa. Lisää toimiva avain ja yritä uudestaan.',
com_error_expired_user_key:
'{0} varten annettu avain vanheni {1}. Syötä uusi avain ja yritä uudestaan.',
com_files_no_results: 'Ei tuloksia.',
com_files_filter: 'Suodata tiedostoja...',
com_files_number_selected: '{0}/{1} tiedostoa valittu',
com_sidepanel_select_assistant: 'Valitse Avustaja',
com_sidepanel_parameters: 'Parametrit',
com_sidepanel_assistant_builder: 'Avustajan rakentaminen',
com_sidepanel_hide_panel: 'Piilota sivupalkki',
com_sidepanel_attach_files: 'Liitä tiedostoja',
com_sidepanel_manage_files: 'Hallinnoi tiedostoja',
com_assistants_capabilities: 'Kyvykkyydet',
com_assistants_file_search: 'Tiedostohaku',
com_assistants_file_search_info:
'Vektoritietokannan liittämistä tiedostohakuun ei vielä tueta. Voit liittää ne rajapinnan palveluntarjoajan käyttöliittymän kautta, tai liittää tiedostoja viesteihin keskusteluketjupohjaisesti.',
com_assistants_knowledge: 'Tiedot',
com_assistants_knowledge_info:
'Jos lataat tiedostoja Tietoihin, Avustajasi kanssa käytyihin keskusteluihin voi tulla niiden sisältöä.',
com_assistants_knowledge_disabled:
'Avustaja täytyy ensin luoda, ja Kooditulkki tai Tiedonhaku täytyy olla päällä ja asetukset tallennettuna, ennen kuin tiedostoja voidaan ladata Tietoihin.',
com_assistants_image_vision: 'Kuvanäkö',
com_assistants_code_interpreter: 'Kooditulkki',
com_assistants_code_interpreter_files:
'Seuraavat tiedostot ovat vain Kooditulkin käytettävissä:',
com_assistants_retrieval: 'Tiedonhaku',
com_assistants_search_name: 'Hae Avustajia nimen perusteella',
com_assistants_tools: 'Työkalut',
com_assistants_actions: 'Toiminnot',
com_assistants_add_tools: 'Lisää Työkaluja',
com_assistants_add_actions: 'Lisää Toimintoja',
com_assistants_non_retrieval_model:
'Tiedostohaku ei ole käytössä tässä mallissa. Valitse toinen malli.',
com_assistants_available_actions: 'Käytettävissä olevat Toiminnot',
com_assistants_running_action: 'Suoritetaan toimintoa',
com_assistants_completed_action: 'Puhuttiin {0}:lle',
com_assistants_completed_function: 'Suoritettiin {0}',
com_assistants_function_use: 'Avustaja käytti: {0}',
com_assistants_domain_info: 'Avustaja lähetti tiedon tänne: {0}',
com_assistants_delete_actions_success: 'Toiminto poistettiin Avustajalta onnistuneesti',
com_assistants_update_actions_success: 'Toiminto luotiiin tai päivitettiin onnistuneesti',
com_assistants_update_actions_error: 'Toiminnon luomisessa tai päivittämisessä tapahtui virhe.',
com_assistants_delete_actions_error: 'Toiminnon poistamisessa tapahtui virhe.',
com_assistants_actions_info: 'Salli Avustajalle Tiedonhaku tai Toimintojen suorittaminen API-kutsujen kautta',
com_assistants_name_placeholder: 'Valinnainen: Avustajan nimi',
com_assistants_instructions_placeholder: 'Avustajan käyttämät järjestelmäohjeet',
com_assistants_description_placeholder: 'Valinnainen: Kuvaus Avustajasta',
com_assistants_actions_disabled: 'Avustaja täytyy luoda ennen toimintojen lisäämistä',
com_assistants_update_success: 'Päivitys onnistui',
com_assistants_update_error: 'Avustajan päivittämisessä tapahtui virhe.',
com_assistants_create_success: 'Luonti onnistui',
com_assistants_create_error: 'Avustajan luonnissa tapahtui virhe.',
com_ui_date_today: 'Tänään',
com_ui_date_yesterday: 'Eilen',
com_ui_date_previous_7_days: 'Edelliset 7 päivää',
com_ui_date_previous_30_days: 'Edelliset 30 päivää',
com_ui_date_january: 'Tammikuu',
com_ui_date_february: 'Helmikuu',
com_ui_date_march: 'Maaliskuu',
com_ui_date_april: 'Huhtikuu',
com_ui_date_may: 'Toukokuu',
com_ui_date_june: 'Kesäkuu',
com_ui_date_july: 'Heinäkuu',
com_ui_date_august: 'Elokuu',
com_ui_date_september: 'Syyskuu',
com_ui_date_october: 'Lokakuu',
com_ui_date_november: 'Marraskuu',
com_ui_date_december: 'Joulukuu',
com_ui_field_required: 'Tämä kenttä on pakollinen',
com_ui_download_error: 'Virhe tiedoston lataamisesta. Tiedosto on saatettu poistaa.',
com_ui_attach_error_type: 'Päätepiste ei tue tiedostotyyppiä::',
com_ui_attach_error_openai: 'Avustajan tiedostoja ei voi liittää muihin päätepisteisiin',
com_ui_attach_warn_endpoint: 'Ilman yhteensopivaa työkalua muut kuin Avustajan tiedostot voidaan jättää huomiotta.',
com_ui_attach_error_size: 'Tiedoston koko ylittää päätepisteen rajan:',
com_ui_attach_error:
'Tiedosto ei voi liittää. Luo tai valitse keskustelu, tai kokeile ladata sivu uudestaan.',
com_ui_examples: 'Esimerkkejä',
com_ui_new_chat: 'Uusi keskustelu',
com_ui_happy_birthday: 'On 1. syntymäpäiväni!',
com_ui_experimental: 'Kokeelliset ominaisuudet',
com_ui_on: 'Päällä',
com_ui_off: 'Pois',
com_ui_yes: 'Kyllä',
com_ui_no: 'Ei',
com_ui_ascending: 'Nouseva',
com_ui_descending: 'Laskeva',
com_ui_show_all: 'Näytä kaikki',
com_ui_name: 'Nimi',
com_ui_date: 'Päivämäärä',
com_ui_storage: 'Varasto',
com_ui_context: 'Konteksti',
com_ui_size: 'Koko',
com_ui_host: 'Host',
com_ui_update: 'Päivitys',
com_ui_authentication: 'Autentikointi',
com_ui_instructions: 'Ohjeet',
com_ui_description: 'Kuvaus',
com_ui_error: 'Virhe',
com_ui_error_connection: 'Palvelimeen yhdistäessä tapahtui virhe. Kokeile ladata sivu uudestaan.',
com_ui_select: 'Valitse',
com_ui_input: 'Syöte',
com_ui_close: 'Sulje',
com_ui_model: 'Malli',
com_ui_select_model: 'Valitse malli',
com_ui_select_search_model: 'Hae mallia nimen perusteella',
com_ui_select_search_plugin: 'Hae lisäosaa nimen perusteella',
com_ui_use_prompt: 'Käytä syötettä',
com_ui_prev: 'Edellinen',
com_ui_next: 'Seuraava',
com_ui_stop: 'Pysäytä',
com_ui_upload_files: 'Lataa tiedostoja',
com_ui_prompt: 'Syöte',
com_ui_prompts: 'Syötteet',
com_ui_prompt_name: 'Syötteen nimi',
com_ui_delete_prompt: 'Poista syöte?',
com_ui_admin: 'Ylläpito',
com_ui_simple: 'Yksinkertainen',
com_ui_versions: 'Versiot',
com_ui_version_var: 'Versio {0}',
com_ui_advanced: 'Edistynyt',
com_ui_admin_settings: 'Ylläpitoasetukset',
com_ui_error_save_admin_settings: 'Ylläpitoasetusten tallentamisessa tapahtui virhe.',
com_ui_prompt_preview_not_shared: 'Tekijä ei ole sallinut yhteistyötä tälle syötteelle.',
com_ui_prompt_name_required: 'Syötteen nimi on pakollinen',
com_ui_prompt_text_required: 'Teksti on pakollinen',
com_ui_prompt_text: 'Teksti',
com_ui_back_to_chat: 'Palaa keskusteluun',
com_ui_back_to_prompts: 'Palaa syötteisiin',
com_ui_categories: 'Kategoriat',
com_ui_filter_prompts_name: 'Syötteiden nimisuodatus',
com_ui_search_categories: 'Hakukategoriat',
com_ui_manage: 'Hallinnoi',
com_ui_variables: 'Muuttujat',
com_ui_variables_info:
'Käytä kaksoisaaltosulkeita tekstissäsi muuttujien luomiseen, esim. {{esimerkkimuuttuja}}. Muuttujia voi täyttää myöhemmin syötettä käyttäessä.',
com_ui_special_variables:
'Erikoismuuttujat: Käytä {{current_date}} kuluvaa päivämäärää varten, ja {{current_user}} käyttäjätunnustasi varten.',
com_ui_showing: 'Näytetään',
com_ui_of: '/',
com_ui_entries: 'Merkinnät',
com_ui_pay_per_call: 'Kaikki tekoälykeskustelut yhdessä paikassa. Maksa kerrasta, älä kuukaudesta.',
com_ui_new_footer: 'Kaikki tekoälykeskustelut yhdessä paikassa.',
com_ui_latest_footer: 'Kaikki tekoälyt kaikille.',
com_ui_enter: 'Syötä',
com_ui_submit: 'Lähetä',
com_ui_none_selected: 'Ei valintaa',
com_ui_upload_success: 'Tiedoston lataus onnistui',
com_ui_upload_error: 'Tiedoston lataamisessa tapahtui virhe',
com_ui_upload_invalid: 'Virheellinen ladattava tiedosto. Tiedoston täytyy olla kokorajaan mahtuva kuvatiedosto',
com_ui_upload_invalid_var: 'Virheellinen ladattava tiedosto. Tiedoston täytyy olla enintään {0} MB kokoinen kuvatiedosto',
com_ui_cancel: 'Peruuta',
com_ui_save: 'Tallenna',
com_ui_renaming_var: 'Uudelleennimetään "{0}"',
com_ui_save_submit: 'Tallenna & Lähetä',
com_user_message: 'Sinä',
com_ui_read_aloud: 'Lue ääneen',
com_ui_copied: 'Kopioitu!',
com_ui_copy_code: 'Kopioi koodi',
com_ui_copy_to_clipboard: 'Kopioi leikepöydälle',
com_ui_copied_to_clipboard: 'Kopioitu leikepöydältä',
com_ui_fork: 'Haarauta',
com_ui_fork_info_1: 'Käytä tätä asetusta viestien haarauttamiseen halutulla tavalla.',
com_ui_fork_info_2:
'"Haarauttaminen" luo uuden keskustelun siten, että se alkaa/päättyy tietyistä tämänhetkisen keskustelun viesteistä, luoden kopion halutulla tavalla.',
com_ui_fork_info_3:
'"Kohdeviesti" tarkoittaa joko viestiä, josta tämä ponnahdusikkuna avattiin, tai, jos rastitat "{0}", viimeisintä viestiä keskustelussa.',
com_ui_fork_info_visible:
'Tämä vaihtoehto haarauttaa vain näkyvissä olevat viestit; toisin sanoen, suoran polun kohdeviestiin, ilman sivupolkuja.',
com_ui_fork_info_branches:
'Tämä vaihtoehto haarauttaa näkyvissä olevat viestit sekä niihin liittyvät sivupolut; toisin sanoen, suoran polun kohdeviestiin sisällyttäen matkalla olevat sivupolut.',
com_ui_fork_info_target:
'Tämä vaihtoehto haarauttaa kaikki viestit kohdeviestiin asti, sisällyttäen sen naapurit; toisin sanoen, kaikki sivupolut riippumatta siitä ovatko ne näkyvissä tai samalla polulla tulevat matkaan.',
com_ui_fork_info_start:
'Jos tämä on valittu, haarauttaminen alkaa tästä viestistä keskustelun viimeiseen viestiin saakka, yllä valitun toimintatavan mukaisesti.',
com_ui_fork_info_remember:
'Jos tämä on valittu, tallentaa tehdyt valinnat tulevaa jatkokäyttöä varten nopeuttaen keskusteluhaarojen luomista samoilla asetuksilla.',
com_ui_fork_success: 'Keskustelun haarauttaminen onnistui.',
com_ui_fork_processing: 'Haarautetaan keskustelua...',
com_ui_fork_error: 'Keskustelun haarauttamisessa tapahtui virhe',
com_ui_fork_change_default: 'Oletushaarautustapa',
com_ui_fork_default: 'Käytä oletushaarautustapaa',
com_ui_fork_remember: 'Muista',
com_ui_fork_split_target_setting: 'Aloita haara oletuksena kohdeviestistä',
com_ui_fork_split_target: 'Aloita haara tästä',
com_ui_fork_remember_checked:
'Valintasi muistetaan käytön jälkeen. Voit muuttaa tätä milloin tahansa asetuksista.',
com_ui_fork_all_target: 'Sisällytä kaikki tänne/täältä',
com_ui_fork_branches: 'Sisällytä sivupolut',
com_ui_fork_visible: 'Vain näkyvät viestit',
com_ui_fork_from_message: 'Valitse haarautustapa',
com_ui_mention: 'Mainitse päätepiste, Avustaja tai asetus vaihtaaksesi siihen pikana',
com_ui_add: 'Lisää malli tai esiasetus lisävastausta varten',
com_ui_regenerate: 'Luo uudestaan',
com_ui_continue: 'Jatka',
com_ui_edit: 'Muokkaa',
com_ui_loading: 'Ladataan...',
com_ui_success: 'Onnistui',
com_ui_all: 'kaikki',
com_ui_all_proper: 'Kaikki',
com_ui_clear: 'Tyhjennä',
com_ui_revoke: 'Peruuta',
com_ui_revoke_info: 'Peruuta kaikki käyttäjän antamat tunnisteet',
com_ui_import_conversation: 'Tuo',
com_ui_nothing_found: 'Mitään ei löytynyt',
com_ui_go_to_conversation: 'Siirry keskusteluun',
com_ui_import_conversation_info: 'Tuo keskusteluja JSON-tiedostosta',
com_ui_import_conversation_success: 'Keskustelujen tuonti onnistui',
com_ui_import_conversation_error: 'Keskustelujesi tuonnissa tapahtui virhe',
com_ui_import_conversation_file_type_error: 'Tiedostotyyppi ei ole tuettu tuonnissa',
com_ui_confirm_action: 'Vahvista toiminto',
com_ui_chat: 'Keskustelu',
com_ui_dashboard: 'Työpöytä',
com_ui_chats: 'keskustelut',
com_ui_avatar: 'Profiilikuva',
com_ui_unknown: 'Tuntematon',
com_ui_result: 'Tulos',
com_ui_image_gen: 'Kuvanluonti',
com_ui_assistant: 'Avustaja',
com_ui_assistant_deleted: 'Avustajan poisto onnistui',
com_ui_assistant_delete_error: 'Avustajan poistossa tapahtui virhe',
com_ui_assistants: 'Avustajat',
com_ui_attachment: 'Liitetiedosto',
com_ui_assistants_output: 'Avustajien tuotokset',
com_ui_delete: 'Poista',
com_ui_create: 'Luo',
com_ui_create_prompt: 'Luo syöte',
com_ui_share: 'Jaa',
com_ui_share_var: 'Jaa {0}',
com_ui_copy_link: 'Kopioi linkki',
com_ui_update_link: 'Päivitä linkki',
com_ui_create_link: 'Luo linkki',
com_ui_share_to_all_users: 'Jaa kaikille käyttäjille',
com_ui_my_prompts: 'Omat syötteet',
com_ui_no_category: 'Ei kategoriaa',
com_ui_shared_prompts: 'Jaetut syötteet',
com_ui_prompts_allow_use: 'Salli syötteiden käyttäminen',
com_ui_prompts_allow_create: 'Salli syötteiden luominen',
com_ui_prompts_allow_share_global: 'Salli syötteiden jakaminen kaikille käyttäjille',
com_ui_prompt_shared_to_all: 'Tämä syöte on jaettu kaikille käyttäjille',
com_ui_prompt_update_error: 'Syötteen päivityksessä tapahtui virhe',
com_ui_prompt_already_shared_to_all: 'Tämä syöte on jo jaettu kaikille käyttäjille',
com_ui_description_placeholder: 'Valinnainen: Lisää kuvaus syötteelle',
com_ui_command_placeholder: 'Valinnainen: Käsky syötteelle. Oletuskäskynä on nimi.',
com_ui_command_usage_placeholder: 'Valitse syöte käskyn tai nimen perusteella',
com_ui_no_prompt_description: 'Kuvausta ei löytynyt.',
com_ui_share_link_to_chat: 'Jaa linkki keskusteluun',
com_ui_share_error: 'Keskustelulinkin jakamisessa tapahtui virhe',
com_ui_share_retrieve_error: 'Jaettujen linkkien jakamisessa tapahtui virhe',
com_ui_share_delete_error: 'Jaetun linkin poistossa tapahtui virhe',
com_ui_share_create_message: 'Nimesi ja jakamisen jälkeen lisätäämäsi viestit pysyvät yksityisinä.',
com_ui_share_created_message:
'Jakolinkki keskusteluun on luotu. Hallinnoi aiemmin jaettuja keskusteluja milloin vain Asetusten kautta.',
com_ui_share_update_message:
'Nimesi, mukautetut ohjeet, ja mahdolliset viestit jotka lisäät jakamisen jälkeen jäävät yksityisiksi.',
com_ui_share_updated_message:
'Keskustelun jakolinkki on päivitetty. Hallinnoi aiemmin jaettuja keskusteluja milloin vain Asetusten kautta.',
com_ui_shared_link_not_found: 'Jakolinkki ei löytynyt',
com_ui_delete_conversation: 'Poista keskustelu?',
com_ui_delete_confirm: 'Tämä suorittaa poiston',
com_ui_delete_confirm_prompt_version_var:
'Tämä poistaa valitun version "{0}":lta. Jos muita versioita ei ole, syöte poistetaan samalla.',
com_ui_delete_assistant_confirm:
'Haluatko varmasti poistaa tämän Avustajan? Poistoa ei voi perua.',
com_ui_rename: 'Nimeä uudestaan',
com_ui_archive: 'Arkisto',
com_ui_archive_error: 'Keskustelun arkistointi epäonnistui',
com_ui_unarchive: 'Palauta arkistosta',
com_ui_unarchive_error: 'Palautus arkistosta epäonnistui',
com_ui_more_options: 'Lisää',
com_ui_preview: 'Esikatsele',
com_ui_upload: 'Lataa',
com_ui_connect: 'Yhdistä',
com_ui_locked: 'Lukittu',
com_ui_upload_delay:
'"{0}" lataaminen kestää odotettua pidempään. Ole hyvä ja odota kunnes tiedosto saadaan indeksoitua tiedonhakua varten.',
com_ui_privacy_policy: 'Tietosuojailmoitus',
com_ui_terms_of_service: 'Käyttöehdot',
com_ui_use_micrphone: 'Käytä mikrofonia',
com_ui_min_tags: 'Enempää arvoja ei voida poistaa. Niiden minimimäärä on {0}.',
com_ui_max_tags: 'Maksimimäärä on {0}. käytetään viimeisimpiä arvoja.',
com_auth_error_login:
'Kirjautuminen annetuilla tiedoilla ei onnistunut. Tarkista kirjautumistiedot, ja yritä uudestaan.',
com_auth_error_login_rl:
'Liian monta kirjautumisyritystä lyhyen ajan sisällä. Yritä myöhemmin uudestaan.',
com_auth_error_login_ban:
'Tilisi on väliaikaisesti suljettu palvelun sääntöjen rikkomisesta.',
com_auth_error_login_server:
'Tapahtui sisäinen palvelinvirhe. Odota hetki, ja yritä uudestaan.',
com_auth_error_login_unverified:
'Tiliäsi ei ole vahvistettu. Vahvistuslinkin pitäisi löytyä sähköposteistasi.',
com_auth_no_account: 'Ei tunnusta?',
com_auth_sign_up: 'Rekisteröidy',
com_auth_sign_in: 'Kirjaudu',
com_auth_google_login: 'Jatka Googlella',
com_auth_facebook_login: 'Jatka Facebookilla',
com_auth_github_login: 'Jatka Githubilla',
com_auth_discord_login: 'Jatka Discordilla',
com_auth_email: 'Sähköposti',
com_auth_email_required: 'Sähköposti on pakollinen',
com_auth_email_min_length: 'Sähköpostiosoitteen on oltava vähintään 6 merkkiä pitkä',
com_auth_email_max_length: 'Sähköpostiosoitteen ei pitäisi olla 120 merkkiä pidempi',
com_auth_email_pattern: 'Sähköpostiosoite on syötettävä oikeassa muodossa',
com_auth_email_address: 'Sähköpostiosoite Email address',
com_auth_password: 'Salasana',
com_auth_password_required: 'Salasana on pakollinen',
com_auth_password_min_length: 'Salasanan on oltava vähintään 8 merkkiä pitkä',
com_auth_password_max_length: 'Salasana voi olla enintään 128 merkkiä',
com_auth_password_forgot: 'Salasana unohtunut?',
com_auth_password_confirm: 'Vahvista salasana',
com_auth_password_not_match: 'Salasanat eivät täsmää',
com_auth_continue: 'Jatka',
com_auth_create_account: 'Luo tili',
com_auth_error_create:
'Tilin rekisteröinnissä tapahtui virhe. Yritä uudestaan.',
com_auth_full_name: 'Koko nimi',
com_auth_name_required: 'Nimi on pakollinen',
com_auth_name_min_length: 'Nimessä on oltava vähintään 3 merkkiä',
com_auth_name_max_length: 'Nimi voi olla enintään 80 merkkiä pitkä',
com_auth_username: 'Käyttäjänimi (valinnainen)',
com_auth_username_required: 'Käyttäjänimi on pakollinen',
com_auth_username_min_length: 'Käyttäjänimessä on oltava vähintään 2 merkkiä',
com_auth_username_max_length: 'Käyttäjänimi voi olla enintään 20 merkkiä pitkä',
com_auth_already_have_account: 'Käyttäjätilisi on jo luotu?',
com_auth_login: 'Kirjaudu',
com_auth_registration_success_insecure: 'Rekisteröityminen onnistui.',
com_auth_registration_success_generic: 'Tarkista sähköpostisi sähköpostiosoitteen vahvistamiseksi.',
com_auth_reset_password: 'Aseta uusi salasana',
com_auth_click: 'Napauta',
com_auth_here: 'TÄTÄ',
com_auth_to_reset_your_password: 'asettaaksesi uuden salasanan.',
com_auth_reset_password_link_sent: 'Sähköposti lähetetty',
com_auth_reset_password_if_email_exists:
'Jos kyseiselle sähköpostiosoitteelle löytyy käyttäjätili, siihen lähetetään sähköposti joka sisältää ohjeet salasanan uusimiseen. Tarkistathan myös roskapostikansion.',
com_auth_reset_password_email_sent:
'Jos käyttäjä on rekisteröitynyt, salasanan vaihto-ohjeet lähetetään hänelle sähköpostitse.',
com_auth_reset_password_success: 'Salasanan asettaminen onnistui',
com_auth_login_with_new_password: 'Voit nyt kirjautua uudella salasanallasi.',
com_auth_error_invalid_reset_token: 'Tämä salasanan uusimistunniste ei ole enää voimassa.',
com_auth_click_here: 'Napauta tästä',
com_auth_to_try_again: 'kokeillaksesi uudestaan.',
com_auth_submit_registration: 'Lähetä rekisteröityminen',
com_auth_welcome_back: 'Tervetuloa takaisin',
com_auth_back_to_login: 'Palaa kirjautumiseen',
com_auth_email_verification_failed: 'Sähköpostin varmentaminen epäonnistui',
com_auth_email_verification_rate_limited: 'Liian monta yritystä. Kokeile myöhemmin uudestaan',
com_auth_email_verification_success: 'Sähköposti varmennettu',
com_auth_email_resent_success: 'Varmennussähköpostin uudelleenlähetys onnistui',
com_auth_email_resent_failed: 'Varmennussähköpostin uudelleenlähetys epäonnistui',
com_auth_email_verification_failed_token_missing: 'Varmennus epäonnistui tunnisteen puuttumisen vuoksi',
com_auth_email_verification_invalid: 'Sähköpostin varmentaminen ei voimassa',
com_auth_email_verification_in_progress: 'Varmennetaan sähköpostia. Ole hyvä ja odota.',
com_auth_email_verification_resend_prompt: 'Sähköposti ei saapunut perille?',
com_auth_email_resend_link: 'Lähetä sähköposti uudestaan',
com_auth_email_verification_redirecting: 'Uudelleenohjataan {0} sekunnissa...',
com_endpoint_open_menu: 'Avaa valikko',
com_endpoint_bing_enable_sydney: 'Ota Sydney käyttöön',
com_endpoint_bing_to_enable_sydney: 'Ottaaksesi Sydneyn käyttöön',
com_endpoint_bing_jailbreak: 'Jailbreak',
com_endpoint_bing_context_placeholder:
'Bing voi käyttää jopa 7000 tokenia keskustelussa viittausympäristönä käytettyä \'kontekstia\' varten. Tarkka raja ei ole tiedossa, mutta yli 7000 tokenia käyttäessä voi esiintyä virheitä.',
com_endpoint_bing_system_message_placeholder:
'VAROITUS: Tämän ominaisuuden väärinkäyttö saattaa johtaa Bingin KÄYTTÖKIELTOON! Napauta \'Järjestelmäviestiä\', niin saat täydet ohjeet ja oletusviestin (jos jätetty pois), mikä on turvalliseksi katsottu \'Sydney\'-esiasetus.',
com_endpoint_system_message: 'Järjestelmäviesti',
com_endpoint_message: 'Vastaanottajana',
com_endpoint_messages: 'Viestit',
com_endpoint_message_not_appendable: 'Muokkaa viestiäsi tai Luo uudestaan.',
com_endpoint_default_blank: 'oletus: tyhjä',
com_endpoint_default_false: 'oletus: false',
com_endpoint_default_creative: 'oletus: creative',
com_endpoint_default_empty: 'oletus: tyhjä',
com_endpoint_default_with_num: 'oletus: {0}',
com_endpoint_context: 'Konteksti',
com_endpoint_tone_style: 'Tyylisävy',
com_endpoint_token_count: 'Token-määrä',
com_endpoint_output: 'Tulos',
com_endpoint_context_tokens: 'Konteksti-tokenien maksimimäärä',
com_endpoint_context_info: `Kontekstia varten käytettävien tokeneiden maksimimäärä. Käytä tätä pyyntökohtaisten token-määrien hallinnointiin. Jos tätä ei määritetä, käytössä ovat järjestelmän oletusarvot perustuen tiedossa olevien mallien konteksti-ikkunoiden kokoon. Korkeamman arvon asettaminen voi aiheuttaa virheitä tai korkeamman token-hinnan.`,
com_endpoint_google_temp:
'Korkeampi arvo = satunnaisempi; matalampi arvo = keskittyneempi ja deterministisempi. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.',
com_endpoint_google_topp:
'Top-P vaikuttaa siihen kuinka malli valitsee tokeneita tulokseen. Tokenit valitaan top-k:sta (ks. Top-k -parametri) todennäköisimmistä vähiten todennäköseen, kunnes niiden todennäköisyyksien summa ylittää Top-P -arvon.',
com_endpoint_google_topk:
'Top-k vaikuttaa siihen, miten malli valitsee tokeineita tulokseen. Jos Top-k on 1, valitaan se token, joka on kaikkien todennäköisen mallin sanastossa (tunnetaan myös nimellä ahne dekoodaus), kun taas top-k 3 tarkoittaisi, että seuraavat token valitaan 3 todennäköisimmän tokenin joukosta, lämpötilaa hyödyntäen.',
com_endpoint_google_maxoutputtokens:
'Maksimimäärä tokeneillre, joita generoidaan tulokseen. Valitse pienempi arvo saadaksesi lyhyempiä vastauksia, ja suurempi arvo pitkiä vastauksia varten.',
com_endpoint_google_custom_name_placeholder: 'Aseta Googlelle mukautettu nimi',
com_endpoint_prompt_prefix_placeholder: 'Aseta mukautetut ohjeet tai konteksti. Jätetään huomiotta, jos tyhjä.',
com_endpoint_instructions_assistants_placeholder:
'Yliajaa Avustajan ohjeet. Tätä voi hyödyntää käytöksen muuttamiseen keskustelukohtaisesti.',
com_endpoint_prompt_prefix_assistants_placeholder:
'Anna lisäohjeita tai kontekstia Avustajan pääohjeiden lisäksi. Set additional instructions or context on top of the Assistant\'s main instructions. Jätetään huomiotta, jos tyhjä.',
com_endpoint_custom_name: 'Mukautettu nimi',
com_endpoint_prompt_prefix: 'Mukautetut ohjeet',
com_endpoint_prompt_prefix_assistants: 'Lisäohjeet',
com_endpoint_instructions_assistants: 'Yliaja ohjeet',
com_endpoint_temperature: 'Lämpötila',
com_endpoint_default: 'oletus',
com_endpoint_top_p: 'Top P',
com_endpoint_top_k: 'Top k',
com_endpoint_max_output_tokens: 'Tulos-tokeneiden maksimimäärä',
com_endpoint_stop: 'Pysäytyssekvenssit',
com_endpoint_stop_placeholder: 'Erota arvot toisistaan rivinvaihdoilla',
com_endpoint_openai_max_tokens: `Valinnainen \`max_tokens\` -kenttä, joka kuvaa keskustelun vastauksessa generoitujen tokeneiden maksimimäärää.
Syötteen ja vastauksen kokonaispituutta rajoittaa mallin konteksti-ikkuna. Konteksti -ikkunan koon ylittämisestä voi seurata virheitä.`,
com_endpoint_openai_temp:
'Korkeampi arvo = satunnaisempi; matalampi arvo = keskittyneempi ja deterministisempi. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.',
com_endpoint_openai_max:
'Luotavien tokeneiden maksimimäärä. Mallin konteksti-ikkuna rajoittaa syötteiden ja vastausten kokonaispituutta.',
com_endpoint_openai_topp:
'Vaihtoehto lämpötilapohjaiselle otannalle, ydinotanta, valitsee tokeneita Top P -todennäköisyysmassasta. Esimerkiksi arvo 0.1 tarkoittaa että vain top 10% tokeneista todennäköisyysmassassa huomioidaan. Suosittelemme, että muokkaat tätä tai lämpötilaa, mutta ei molempia.',
com_endpoint_openai_freq:
'Lukuarvo väliltä -2.0 - 2.0. Positiiviset arvot rankaisevat uusia tokeneita perustuen niiden esiintymistiheyteen siihen mennessä luodussa tekstissä, mikä vähentää todennäköisyyttä, että malli toistaa saman rivin täsmälleen samanlaisena.',
com_endpoint_openai_pres:
'Lukuarvo väliltä -2.0 - 2.0. Positiiviset arvot rankaisevat uusia tokeneita perustuen niiden esiintymiseen siihen mennessä luodussa tekstissä, ja lisäävät todennäköisyyttä että malli aloittaa uuden aiheen.',
com_endpoint_openai_resend:
'Lähetä uudestaan kaikki aiemmin liitetyt kuvat. Huom: tämä voi lisätä token-kustannuksia huomattavasti, ja useiden kuvien käsittelystä kerralla voi seurata virheitä.',
com_endpoint_openai_resend_files:
'Lähetä uudestaan kaikki aiemmin liitetyt tiedostot. Huom: tämä lisää token-kustannuksia, ja useiden tiedostojen käsittelystä kerralla voi seurata virheitä.',
com_endpoint_openai_detail:
'Kuvatarkkuus Vision-pyynnöille. "Matala" on halvempi ja nopeampi, "Korkea" on yksityiskohtaisempi ja kalliimpi, ja "Auto" valitsee näiden välillä automaattisesti kuvan koon perusteella.',
com_endpoint_openai_stop: 'Enintään 4 sekvenssiä, joiden kohdalla API lopettaa tokenien luomisen.',
com_endpoint_openai_custom_name_placeholder: 'Anna tekoälylle mukautettu nimi',
com_endpoint_openai_prompt_prefix_placeholder:
'Aseta mukautetut ohjeet Järjestelmäohjeisiin sisällytettäväksi. Oletus: tyhjä',
com_endpoint_anthropic_temp:
'Vaihteluväli on 0 - 1. Käytä lähempänä nollaa olevaa lämpötilaa analyyttisiin tai monivalintatehtäviin, ja lähempänä yhtä luoviin ja generatiivisiin tehtäviin. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.',
com_endpoint_anthropic_topp:
'Top-P vaikuttaa siihen kuinka malli valitsee tokeneita tulokseen. Tokenit valitaan top-k:sta (ks. Top-k -parametri) todennäköisimmistä vähiten todennäköseen, kunnes niiden todennäköisyyksien summa ylittää Top-P -arvon.',
com_endpoint_anthropic_topk:
'Top-k vaikuttaa siihen, miten malli valitsee tokeineita tulokseen. Jos Top-k on 1, valitaan se token, joka on kaikkien todennäköisen mallin sanastossa (tunnetaan myös nimellä ahne dekoodaus), kun taas top-k 3 tarkoittaisi, että seuraavat token valitaan 3 todennäköisimmän tokenin joukosta, lämpötilaa hyödyntäen.',
com_endpoint_anthropic_maxoutputtokens:
'Vastauksen maksimi-tokenmäärä. valitse pienempi arvo, jos haluat lyhyempiä vastauksia, ja korkeampi arvo, jos haluat pidempiä vastauksia.',
com_endpoint_anthropic_custom_name_placeholder: 'Aseta mukautettu nimi Anthropicille',
com_endpoint_frequency_penalty: 'Esiintymistiheysrangaistus',
com_endpoint_presence_penalty: 'Esiintymisrangaistus',
com_endpoint_plug_use_functions: 'Käytä Toimintoja',
com_endpoint_plug_resend_files: 'Lähetä tiedostot uudestaan',
com_endpoint_plug_resend_images: 'Lähetä kuvat uudestaan',
com_endpoint_plug_image_detail: 'Kuvan yksityiskohdat',
com_endpoint_plug_skip_completion: 'Ohita Vastaus',
com_endpoint_disabled_with_tools: 'poissa käytöstä työkalujan kanssa',
com_endpoint_disabled_with_tools_placeholder: 'Poissa käytössä Työkalut valittuna',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder:
'Aseta mukautetut ohjeet Järjestelmäohjeisiin liitettäviksi. Oletus: tyhjä',
com_endpoint_import: 'Tuo',
com_endpoint_set_custom_name: 'Aseta mukautettu nimi, jotta esiasetus olisi helpompi löytää',
com_endpoint_preset_delete_confirm: 'Haluatko varmasti poistaa nämä esiasetukset?',
com_endpoint_preset_clear_all_confirm: 'Haluatko varmasti poistaa kaikki esiasetuksesi?',
com_endpoint_preset_import: 'Esiasetus tuotu!',
com_endpoint_preset_import_error: 'Esiasetuksen tuonnissa tapahtui virhe. Yritä uudestaan.',
com_endpoint_preset_save_error: 'Esiasetuksen tallennuksessa tapahtui virhe. Yritä uudestaan.',
com_endpoint_preset_delete_error: 'Esiasetuksen poistossa tapahtui virhe. Yritä uudestaan.',
com_endpoint_preset_default_removed: ' ei ole enää oletus-esiasetus.',
com_endpoint_preset_default_item: 'Oletus:',
com_endpoint_preset_default_none: 'Oletus-esiasetusta ei ole käytössä',
com_endpoint_preset_title: 'Esiasetus',
com_endpoint_preset_saved: 'Tallennettu!',
com_endpoint_preset_default: 'on nyt oletus-esiasetus.',
com_endpoint_preset: 'esiasetus',
com_endpoint_presets: 'esiasetukset',
com_endpoint_preset_selected: 'Esiasetus käytössä!',
com_endpoint_preset_selected_title: 'Käytössä!',
com_endpoint_preset_name: 'Esiasetuksen nimi',
com_endpoint_new_topic: 'Uusi aihe',
com_endpoint: 'Päätepiste',
com_endpoint_hide: 'Piilota',
com_endpoint_show: 'Näytä',
com_endpoint_examples: ' Esiasetukset',
com_endpoint_completion: 'Vastaus',
com_endpoint_agent: 'Agentti',
com_endpoint_show_what_settings: 'Näytä {0} asetusta',
com_endpoint_export: 'Vie',
com_endpoint_export_share: 'Vie/Jaa',
com_endpoint_assistant: 'Avustaja',
com_endpoint_use_active_assistant: 'Käytä aktiivista Avustajaa',
com_endpoint_assistant_model: 'Avustajan malli',
com_endpoint_save_as_preset: 'Tallenna esiasetukseksi',
com_endpoint_presets_clear_warning:
'Haluatko varmasti tyhjentää kaikki esiasetukset? Tätä toimintoa ei voi perua.',
com_endpoint_not_implemented: 'Ei toteutettu',
com_endpoint_no_presets: 'Ei vielä esiasetuksia. Käytä Asetukset-painiketta luodaksesi esiasetuksen.',
com_endpoint_not_available: 'Päätepistettä ei ole tarjolla',
com_endpoint_view_options: 'Katseluvaihtoehdot',
com_endpoint_save_convo_as_preset: 'Tallenna keskustelu esiasetukseksi',
com_endpoint_my_preset: 'Esiasetukseni',
com_endpoint_agent_model: 'Agenttimalli (Suositus: GPT-3.5)',
com_endpoint_completion_model: 'Vastausmalli (Suositus: GPT-4)',
com_endpoint_func_hover: 'Salli lisäosien käyttö OpenAI-toimintoina',
com_endpoint_skip_hover:
'Mahdollista vastausaskeleen ohitus, joka mahdollistaa lopuulisen vastauksen ja generoitujen askeleiden tarkastelun',
com_endpoint_config_key: 'Aseta API-avain',
com_endpoint_assistant_placeholder: 'Valitse Avustaja oikeanpuoleisesta sivupalkista',
com_endpoint_config_placeholder: 'Keskustellaksesi aseta avaimesi Ylätunnistevalikossa.',
com_endpoint_config_key_for: 'Aseta API-avain:',
com_endpoint_config_key_name: 'Avain',
com_endpoint_config_value: 'Aseta arvo:',
com_endpoint_config_key_name_placeholder: 'Aseta ensin API-avain',
com_endpoint_config_key_encryption: 'Avaimesi salataan ja poistetaan: ',
com_endpoint_config_key_never_expires: 'Avaimesi ei koskaan vanhene',
com_endpoint_config_key_expiry: 'vanhenemisaika',
com_endpoint_config_click_here: 'Napauta tästä',
com_endpoint_config_google_service_key: 'Google Service Account Key',
com_endpoint_config_google_cloud_platform: '(Google Cloud Platform:ista)',
com_endpoint_config_google_api_key: 'Google API Key',
com_endpoint_config_google_gemini_api: '(Gemini API)',
com_endpoint_config_google_api_info: 'Saadaksesi Generative Language API -avaimesi (Gemini:a varten),',
com_endpoint_config_key_import_json_key: 'Tuo palveluosoitteen JSON-avain.',
com_endpoint_config_key_import_json_key_success: 'Palveluosoitteetn JSON-avain tuotu onnistuneesti',
com_endpoint_config_key_import_json_key_invalid:
'Virheellinen palveluosoitteen JSON-avain. Toitko oikean tiedoston?',
com_endpoint_config_key_get_edge_key: 'Saadaksisi pääsytunnuksesi Bingiä varten, kirjaudu',
com_endpoint_config_key_get_edge_key_dev_tool:
'Käytä kehitystyökaluja ja lisäosaa sivustolle kirjautuneena _U -evästeen kopioimiseen. Jos tämä ei toimi, seuraa näitä',
com_endpoint_config_key_edge_instructions: 'ohjeita',
com_endpoint_config_key_edge_full_key_string: 'saadaksesi täydet evästemerkkijonot.',
com_endpoint_config_key_chatgpt: 'Saadaksesi pääsytunnuksesi ChatGPT:n \'ilmaisversiota\' varten, kirjaudu',
com_endpoint_config_key_chatgpt_then_visit: 'sitten vieraile',
com_endpoint_config_key_chatgpt_copy_token: 'Kopioi pääsytunnus.',
com_endpoint_config_key_google_need_to: 'Sinun täytyy',
com_endpoint_config_key_google_vertex_ai: 'sallia Vertex AI',
com_endpoint_config_key_google_vertex_api: 'API Google Cloud:issa, sitten',
com_endpoint_config_key_google_service_account: 'Luo Palvelutili (Service Account)',
com_endpoint_config_key_google_vertex_api_role:
'Muista napauttaa \'Create and Continue\' jotta saat ainakin \'Vertex AI User\' -roolin. Lopuksi luo JSON-avain tänne tuotavaksi.',
com_nav_welcome_assistant: 'Ole hyvä ja valitse Avustaja',
com_nav_welcome_message: 'Miten voin auttaa tänään?',
com_nav_auto_scroll: 'Vieritä automaattisesti viimeisimpään viestiin keskustelua avatessa',
com_nav_hide_panel: 'Piilota oikeanpuoleinen sivupaneeli',
com_nav_modular_chat: 'Salli päätepisteen vaihto kesken keskustelun',
com_nav_latex_parsing: 'Tulkitse LaTeX:ia viesteissä (saattaa vaikuttaa suoritustehoon)',
com_nav_text_to_speech: 'Tekstistä puheeksi',
com_nav_automatic_playback: 'Toista viimeisin viesti automaattisesti',
com_nav_speech_to_text: 'Puheesta tekstiksi',
com_nav_profile_picture: 'Profiilikuva',
com_nav_change_picture: 'Vaihda kuva',
com_nav_plugin_store: 'Lisäosakauppa',
com_nav_plugin_install: 'Asenna',
com_nav_plugin_uninstall: 'Poista',
com_nav_tool_add: 'Lisää',
com_nav_tool_remove: 'Poista',
com_nav_tool_dialog: 'Avustajatyökalut',
com_ui_misc: 'Muu',
com_ui_roleplay: 'Roolipeli',
com_ui_write: 'Kirjoittaminen',
com_ui_idea: 'Ideat',
com_ui_shop: 'Ostokset',
com_ui_finance: 'Talous',
com_ui_code: 'Koodi',
com_ui_travel: 'Matkustus',
com_ui_teach_or_explain: 'Oppiminen',
com_ui_select_a_category: 'Kategoriaa ei valittu',
com_nav_tool_dialog_description: 'Avustaja täytyy tallentaa, jotta työkaluvalinta säilyisi.',
com_show_agent_settings: 'Näytä Agentin asetukset',
com_show_completion_settings: 'Näytä Vastausasetukset',
com_hide_examples: 'Piilota esimerkit',
com_show_examples: 'Näytä esimerkit',
com_nav_plugin_search: 'Hae lisäosaa',
com_nav_tool_search: 'Hakutyökalut',
com_nav_plugin_auth_error:
'Tämän lisäosan varmentamisessa tapahtui virhe. Yritä uudestaan.',
com_nav_export_filename: 'Tiedoston nimi',
com_nav_export_filename_placeholder: 'Aseta tiedoston nimi',
com_nav_export_type: 'Tyyppi',
com_nav_export_include_endpoint_options: 'Sisällytä päätepistevaihtoehdot',
com_nav_enabled: 'Päällä',
com_nav_not_supported: 'Ei tuettu',
com_nav_export_all_message_branches: 'Vie kaikki sivupolut',
com_nav_export_recursive_or_sequential: 'Rekursiivisesti vai sarjassa?',
com_nav_export_recursive: 'Rekursiivisesti',
com_nav_export_conversation: 'Vie keskustelu',
com_nav_export: 'Vie',
com_nav_shared_links: 'Jaetut linkit',
com_nav_shared_links_manage: 'Hallinnoi',
com_nav_shared_links_empty: 'Sinulla ei ole jaettuja linkkejä.',
com_nav_shared_links_name: 'Nimi',
com_nav_shared_links_date_shared: 'Jakopäivä',
com_nav_source_chat: 'Katsele lähdekeskustelua',
com_nav_my_files: 'Omat tiedostot',
com_nav_theme: 'Teema',
com_nav_theme_system: 'Oletus',
com_nav_theme_dark: 'Tumma',
com_nav_theme_light: 'Vaalea',
com_nav_enter_to_send: 'Lähetä viestit Enter-painikkeella',
com_nav_user_name_display: 'Näytä käyttäjänimi viesteissä',
com_nav_save_drafts: 'Tallenna luonnokset paikallisesti',
com_nav_show_code: 'Kooditulkkia käyttäessä näytä aina koodi',
com_nav_auto_send_prompts: 'Lähetä syötteet automaattisesti',
com_nav_always_make_prod: 'Tee aina uudet versiot tuotantoon',
com_nav_clear_all_chats: 'Tyhjennä kaikki keskustelut',
com_nav_confirm_clear: 'Vahvista tyhjennys',
com_nav_close_sidebar: 'Sulje sivupalkki',
com_nav_open_sidebar: 'Avaa sivupalkki',
com_nav_send_message: 'Lähetä viesti',
com_nav_log_out: 'Kirjaudu ulos',
com_nav_user: 'KÄYTTÄJÄ',
com_nav_archived_chats: 'Arkistoidut keskustelut',
com_nav_archived_chats_manage: 'Hallinnoi',
com_nav_archived_chats_empty: 'Sinulla ei ole arkistoituja keskusteluita.',
com_nav_archive_all_chats: 'Arkistoi kaikki keskustelut',
com_nav_archive_all: 'Arkistoi kaikki',
com_nav_archive_name: 'Nimi',
com_nav_archive_created_at: 'Arkistointipäivä',
com_nav_clear_conversation: 'Tyhjennä keskustelut',
com_nav_clear_conversation_confirm_message:
'Oletko varma että haluat tyhjentää kaikki keskustelut? Tätä toimintoa ei voi peruuttaa.',
com_nav_help_faq: 'Ohjeet & FAQ',
com_nav_settings: 'Asetukset',
com_nav_search_placeholder: 'Etsi keskusteluista',
com_nav_delete_account: 'Poista käyttäjätili',
com_nav_delete_account_confirm: 'Poista käyttäjätili - oletko varma?',
com_nav_delete_account_button: 'Poista käyttäjätilini pysyvästi',
com_nav_delete_account_email_placeholder: 'Syötä käyttäjätilisi sähköpostiosoite',
com_nav_delete_account_confirm_placeholder: 'Jatkaaksesi syötä "DELETE" alla olevaan syötekenttään',
com_nav_delete_warning: 'VAROITUS: Tämä poistaa käyttäjätilisi pysyvästi.',
com_nav_delete_data_info: 'Kaikki tietosi poistetaan.',
com_nav_conversation_mode: 'Keskustelumoodi',
com_nav_auto_send_text: 'Lähetä teksti automaattisesti (3 sekunnin kuluttua)',
com_nav_auto_transcribe_audio: 'Automaattinen äänen litterointi',
com_nav_db_sensitivity: 'Desibeliherkkyys',
com_nav_playback_rate: 'Äänen toiston nopeus',
com_nav_audio_play_error: 'Virhe ääntä toistaessa: {0}',
com_nav_audio_process_error: 'Virhe ääntä käsitellessä: {0}',
com_nav_long_audio_warning: 'Pidemmän tekstin käsittely kestää kauemmin.',
com_nav_engine: 'Puhemoottori',
com_nav_browser: 'Selain',
com_nav_external: 'Ulkoinen',
com_nav_delete_cache_storage: 'Tyhjennä TTS (tekstistä ääneksi) -välimuistivarasto',
com_nav_enable_cache_tts: 'TTS (tekstistä ääneksi) -välimuisti käyttöön',
com_nav_voice_select: 'Ääni',
com_nav_info_enter_to_send:
'Jos tämä on päällä, Enter-näppäimen painaminen lähettää viestin. Kun asetus on pois päältä, Enter lisää rivinvaihdon, ja viestin lähettämiseksi on painettava CTRL + ENTER.',
com_nav_info_save_draft:
'Jos tämä on päällä, teksti ja liitteet jotka syötät keskusteluun tallennetaan automaattisesti paikallisina luonnoksina. Nämä luonnokset ovat käytettävissä, vaikka välillä lataisit sivun uudestaan tai vaihtaisit toiseen keskusteluun. Luonnokset on tallettettu paikallisesti omalle laitteellesi, ja ne poistetaan, kun viesti on lähetetty.',
com_nav_info_fork_change_default:
'\'Vain näkyvät viestit\' sisältää vain suoran polun valittuun viestiin. \'Sisällytä sivupolut\' lisää polun varrella olevat sivupolut. \'Lisää kaikki tänne/täältä\' sisällyttää kaikki kytköksissä olevat viestit ja sivupolut.',
com_nav_info_fork_split_target_setting:
'Jos tämä on päällä, haara syntyy kohdeviestistä keskustelun viimeiseen viestiin valitun haarautumistavan mukaisesti.',
com_nav_info_user_name_display:
'Jos tämä on päällä, lähettäjän käyttäjänimi näytetään jokaisen lähettämäsi viestin päällä. Jos tämä ei ole käytössä, viestien päällä näytetään vain "Sinä".',
com_nav_info_latex_parsing:
'Kun tämä on päällä, viesteissä oleva LaTeX-koodi näytetään yhtälöinä. Tämän asetuksen jättäminen pois päältä saattaa parantaa suorituskykyä, jos et tarvitse LaTeX-tulkkia.',
com_nav_info_revoke:
'Tämä toiminto peruu ja poistaa kaikki antamasi API-avaimet. Ennen kuin voit jatkaa päätepisteiden käyttöä, sinun on syötettävä uudet tunnisteet.',
com_nav_info_delete_cache_storage:
'Tämä toiminto poistaa kaikki laitteesi välimuistiin tallennetut TTS (tekstistä puheeksi) -äänitiedostot. Välimuistiin tallennettuja äänitiedostoja käytetään aiemmin luotujen TTS-tiedostojen toistamisen nopeuttamikseksi, mutta ne saattavat viedä levytilaa laitteellasi.',
com_nav_setting_general: 'Yleiset',
com_nav_setting_beta: 'Beta-toiminnot',
com_nav_setting_data: 'Datakontrollit',
com_nav_setting_account: 'Käyttäjätili',
com_nav_setting_speech: 'Puhe',
com_nav_language: 'Kieli',
com_nav_lang_auto: 'Tunnista automaattisesti',
};

View File

@@ -44,7 +44,7 @@ export default {
com_ui_revoke: '撤銷',
com_ui_revoke_info: '撤銷所有使用者提供的憑證。',
com_ui_import_conversation: '匯入',
com_ui_import_conversation_info: '從JSON文件匯入對話',
com_ui_import_conversation_info: '從 JSON 文件匯入對話',
com_ui_import_conversation_success: '對話匯入成功',
com_ui_import_conversation_error: '匯入對話時發生錯誤',
com_ui_confirm_action: '確認操作',
@@ -55,11 +55,11 @@ export default {
com_ui_create_link: '建立連結',
com_ui_share_link_to_chat: '分享連結到聊天',
com_ui_share_error: '分享聊天連結時發生錯誤',
com_ui_share_retrieve_error: '刪除共享鏈接時出錯。',
com_ui_share_delete_error: '刪除共享鏈接時出錯。',
com_ui_share_retrieve_error: '刪除共享連結時發生錯誤。',
com_ui_share_delete_error: '刪除共享連結時發生錯誤。',
com_ui_share_create_message: '您的姓名以及您在共享後新增的任何訊息都會保密。',
com_ui_share_created_message: '已建立到您的聊天的共享連結。可以隨時透過設定管理以前共享的聊天。',
com_ui_share_update_message: '您的姓名、自定義指示以及您在共享後新增的任何訊息都會保密。',
com_ui_share_update_message: '您的姓名、自訂提示指令以及您在共享後新增的任何訊息都會保密。',
com_ui_share_updated_message: '已更新到您的聊天的共享連結。可以隨時透過設定管理以前共享的聊天。',
com_ui_shared_link_not_found: '未找到共享連結',
com_ui_delete: '刪除',
@@ -150,9 +150,9 @@ export default {
'Top-k 調整模型如何選取輸出的 token。當 Top-k 設為 1 時,模型會選取在其詞彙庫中機率最高的 token 進行輸出(這也被稱為貪婪解碼)。相對地,當 Top-k 設為 3 時,模型會從機率最高的三個 token 中選取下一個輸出 token這會涉及到所謂的「溫度」調整',
com_endpoint_google_maxoutputtokens:
'設定回應中可生成的最大 token 數。若希望回應簡短,請設定較低的數值;若需較長的回應,則設定較高的數值。',
com_endpoint_google_custom_name_placeholder: '為 Google 設定自定義名稱',
com_endpoint_prompt_prefix_placeholder: '設定自定義提示或前後文。如果為空則忽略。',
com_endpoint_custom_name: '自定義名稱',
com_endpoint_google_custom_name_placeholder: '為 Google 設定自名稱',
com_endpoint_prompt_prefix_placeholder: '設定自提示或前後文。如果為空則忽略。',
com_endpoint_custom_name: '自名稱',
com_endpoint_prompt_prefix: '提示起始字串',
com_endpoint_temperature: '溫度',
com_endpoint_default: '預設',
@@ -164,13 +164,13 @@ export default {
com_endpoint_openai_max:
'要生成的最大 token 數。輸入 token 和生成 token 的總長度受到模型前後文長度的限制。',
com_endpoint_openai_topp:
'與溫度取樣的替代方法,稱為核心取樣,其中模型考慮 top_p 機率質量的 token 結果。所以 0.1 表示只考慮佔 top 10% 機率質量的 token 。我們建議修改這個或溫度,但不建議兩者都修改。',
'與溫度取樣的替代方法,稱為核心取樣,其中模型考慮 top_p 機率質量的 token 結果。所以 0.1 表示只考慮佔 top 10% 機率質量的 token。我們建議修改這個或溫度但不建議兩者都修改。',
com_endpoint_openai_freq:
'數值範圍介於 -2.0 和 2.0 之間。正值會根據該 token 在目前的文字中出現的頻率進行懲罰,減少模型產生重複內容的可能性。',
com_endpoint_openai_pres:
'數值範圍介於 -2.0 和 2.0 之間。正值會根據該 token 是否在目前的文字中出現來進行懲罰,增加模型談及新主題的可能性。',
com_endpoint_openai_custom_name_placeholder: '為 ChatGPT 設定自定義名稱',
com_endpoint_openai_prompt_prefix_placeholder: '在系統訊息中設定自定義提示。',
com_endpoint_openai_custom_name_placeholder: '為 ChatGPT 設定自名稱',
com_endpoint_openai_prompt_prefix_placeholder: '在系統訊息中設定自提示。',
com_endpoint_anthropic_temp:
'範圍從 0 到 1。對於分析/多選題,使用接近 0 的溫度,對於創意和生成式任務,使用接近 1 的溫度。我們建議修改這個或 Top P但不建議兩者都修改。',
com_endpoint_anthropic_topp:
@@ -179,16 +179,16 @@ export default {
'Top-k 改變模型選擇輸出 token 的方式。Top-k 為 1 表示所選 token 在模型詞彙表中所有 token 中最可能(也稱為貪婪解碼),而 Top-k 為 3 表示下一個 token 從最可能的 3 個 token 中選擇(使用溫度)。',
com_endpoint_anthropic_maxoutputtokens:
'設定回應中可生成的最大 token 數。若希望回應簡短,請設定較低的數值;若需較長的回應,則設定較高的數值。',
com_endpoint_anthropic_custom_name_placeholder: '為 Anthropic 設定自定義名稱',
com_endpoint_anthropic_custom_name_placeholder: '為 Anthropic 設定自名稱',
com_endpoint_frequency_penalty: '頻率懲罰',
com_endpoint_presence_penalty: '出現懲罰',
com_endpoint_plug_use_functions: '使用外掛作為 OpenAI 函式',
com_endpoint_plug_skip_completion: '跳過完成步驟',
com_endpoint_disabled_with_tools: '與工具一起停用',
com_endpoint_disabled_with_tools_placeholder: '選擇工具時停用',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: '在系統訊息中新增自定義提示。',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: '在系統訊息中新增自提示。',
com_endpoint_import: '匯入',
com_endpoint_set_custom_name: '設定自定義名稱,以便您找到此預設設定',
com_endpoint_set_custom_name: '設定自名稱,以便您找到此預設設定',
com_endpoint_preset: '預設設定',
com_endpoint_presets: '預設設定',
com_endpoint_preset_name: '名稱',
@@ -297,7 +297,7 @@ export default {
com_nav_source_chat: '檢視原始對話',
com_ui_date_today: '今天',
com_ui_date_yesterday: '昨天',
com_ui_date_previous_7_days: ' 7 天',
com_ui_date_previous_7_days: '過去 7 天',
com_ui_date_previous_30_days: '過去 30 天',
com_ui_date_january: '一月',
com_ui_date_february: '二月',
@@ -314,7 +314,7 @@ export default {
com_ui_nothing_found: '找不到任何內容',
com_ui_go_to_conversation: '前往對話',
com_error_moderation:
'似乎您所提交的內容被我們的內容審查系統標記為不符合社群準則。我們無法就此特定主題繼續進行。如果您有任何其他問題或想要探討的主題,請編輯您的訊息或開啟新的對話。',
'您所提交的內容似乎被我們的內容審查系統標記為不符合社群準則。我們無法就此特定主題繼續進行討論。如果您有任何其他問題或想要探討的主題請編輯您的訊息或開啟新的對話。',
com_error_no_user_key: '找不到金鑰,請提供金鑰後再試一次。',
com_error_no_base_url: '找不到基礎 URL。請提供一個基礎 URL 後再試一次。',
com_error_invalid_user_key: '提供的金鑰無效。請提供有效的金鑰並重試。',
@@ -324,7 +324,7 @@ export default {
com_files_number_selected: '已選取 {0} 個檔案,共 {1} 個檔案',
com_sidepanel_select_assistant: '選擇一位助理',
com_sidepanel_parameters: '參數',
com_sidepanel_assistant_builder: '助建構器',
com_sidepanel_assistant_builder: '助建構器',
com_sidepanel_hide_panel: '隱藏側邊選單',
com_sidepanel_attach_files: '附加檔案',
com_sidepanel_manage_files: '管理檔案',
@@ -354,16 +354,16 @@ export default {
com_assistants_delete_actions_error: '刪除操作時發生錯誤',
com_assistants_actions_info: '讓您的助理透過 API 取得資訊或執行操作',
com_assistants_name_placeholder: '選填:助理的名稱',
com_assistants_instructions_placeholder: '系統指令是助理使用的指示',
com_assistants_description_placeholder: '選擇性:在此描述您的助理',
com_assistants_actions_disabled: '您需要先建立一個助,才能新增動作。',
com_assistants_instructions_placeholder: '系統指令是助理使用的提示指令',
com_assistants_description_placeholder: '選:在此描述您的助理',
com_assistants_actions_disabled: '您需要先建立一個助,才能新增動作。',
com_assistants_update_success: '更新成功',
com_assistants_update_error: '更新您的助理時發生錯誤。',
com_assistants_create_success: '已成功建立',
com_assistants_create_error: '建立您的助理時發生錯誤。',
com_ui_field_required: '此欄位為必填',
com_ui_download_error: '下載檔案時發生錯誤。該檔案可能已被刪除。',
com_ui_attach_error_type: '不支援的檔案類型,無法上傳至端點:',
com_ui_attach_error_type: '不支援的檔案類型,無法上傳至端點',
com_ui_attach_error_size: '檔案大小超過端點的限制',
com_ui_attach_error: '無法附加檔案。請建立或選擇對話,或嘗試重新整理頁面。',
com_ui_experimental: '實驗性功能',
@@ -371,7 +371,7 @@ export default {
com_ui_off: '關閉',
com_ui_yes: '是',
com_ui_no: '否',
com_ui_ascending: '升冪',
com_ui_ascending: '遞增',
com_ui_descending: '遞減',
com_ui_show_all: '顯示全部',
com_ui_name: '名稱',
@@ -429,7 +429,7 @@ export default {
com_ui_avatar: '大頭照',
com_ui_unknown: '未知',
com_ui_result: '結果',
com_ui_image_gen: '影生成',
com_ui_image_gen: '影生成',
com_ui_assistant: '助理',
com_ui_assistants: '助理',
com_ui_attachment: '附件',
@@ -448,29 +448,29 @@ export default {
com_endpoint_message: '訊息',
com_endpoint_messages: '訊息',
com_endpoint_message_not_appendable: '無法附加訊息或重新生成。',
com_endpoint_context_tokens: '最大前後文 Token 數',
com_endpoint_context_tokens: '最大前後文 token 數',
com_endpoint_context_info:
'可用於上下文的最大 token 數量。用於控制每個請求傳送的 token 數量。如果未指定,將根據已知模型的上下文大小使用系統預設值。設定較高的值可能會導致錯誤和/或更高的 token 成本。',
com_endpoint_instructions_assistants_placeholder:
'覆寫助理的指示。這對於在每次執行時修改行為很有用。',
'覆寫助理的提示指令。這對於在每次執行時修改行為很有用。',
com_endpoint_prompt_prefix_assistants_placeholder:
'在助的主要指示之上設定額外的指示或上下文。如果為空白,則會被忽略。',
'在助的主要提示指令之上設定額外的提示指令或上下文。如果為空白,則會被忽略。',
com_endpoint_prompt_prefix_assistants: '提示字首',
com_endpoint_instructions_assistants: '覆寫指示',
com_endpoint_instructions_assistants: '覆寫提示指令',
com_endpoint_stop: '停止序列',
com_endpoint_stop_placeholder: '以 `Enter` 鍵分隔值',
com_endpoint_openai_max_tokens:
'可選的 `max_tokens` 欄位,代表在對話完成中可以生成的最大 token 數。\n\n輸入 token 和生成 token 的總長度受限於模型的上下文長度。如果此數字超過最大上下文 token 數,您可能會遇到錯誤。',
com_endpoint_openai_resend:
'重新傳送之前所有附加的圖片。注意:這可能會大幅增加 token 成本,如果附加了太多圖片,您可能會遇到錯誤。',
'重新傳送之前所有附加的圖片。注意這可能會大幅增加 token 成本如果附加了太多圖片您可能會遇到錯誤。',
com_endpoint_openai_resend_files:
'重新傳送之前附加的所有檔案。注意:這將增加 token 成本,如果附件過多,您可能會遇到錯誤。',
com_endpoint_openai_detail:
'「低」解析度的視覺請求較便宜且快速,「高」解析度則更詳細但成本較高,而「自動」會根據影解析度自動在兩者之間選擇。',
'「低」解析度的視覺請求較便宜且快速,「高」解析度則更詳細但成本較高,而「自動」會根據影解析度自動在兩者之間選擇。',
com_endpoint_openai_stop: '最多 4 個序列API 將在生成更多 token 時停止。',
com_endpoint_plug_resend_files: '重新傳送檔案',
com_endpoint_plug_resend_images: '重新傳送圖片',
com_endpoint_plug_image_detail: '影詳細資訊',
com_endpoint_plug_image_detail: '影詳細資訊',
com_endpoint_preset_delete_confirm: '您確定要刪除這個預設設定嗎?',
com_endpoint_preset_clear_all_confirm: '您確定要刪除所有的預設設定嗎?',
com_endpoint_preset_import: '預設設定已匯入!',
@@ -485,8 +485,8 @@ export default {
com_endpoint_preset_default: '現在是預設的預設設定。',
com_endpoint_preset_selected: '已選擇預設設定!',
com_endpoint_preset_selected_title: '已選取!',
com_endpoint_assistant: '助',
com_endpoint_use_active_assistant: '使用活躍助',
com_endpoint_assistant: '助',
com_endpoint_use_active_assistant: '使用活躍助',
com_endpoint_assistant_model: 'AI 模型',
com_endpoint_assistant_placeholder: '請從右側面板選擇一位助理',
com_endpoint_config_placeholder: '在標頭選單中設定您的金鑰以開始對話。',
@@ -524,7 +524,7 @@ export default {
com_nav_language: '語言',
com_nav_lang_auto: '自動偵測',
com_nav_lang_english: '英文',
com_nav_lang_chinese: '繁體中文',
com_nav_lang_chinese: '中文',
com_nav_lang_german: '德語',
com_nav_lang_spanish: '西班牙語',
com_nav_lang_french: '法語',
@@ -723,7 +723,7 @@ export const comparisons = {
},
com_ui_import_conversation_info: {
english: 'Import conversations from a JSON file',
translated: '從JSON文件匯入對話',
translated: '從 JSON 文件匯入對話',
},
com_ui_import_conversation_success: {
english: 'Conversations imported successfully',
@@ -763,11 +763,11 @@ export const comparisons = {
},
com_ui_share_retrieve_error: {
english: 'There was an error deleting the shared link.',
translated: '刪除共享鏈接時出錯。',
translated: '刪除共享連結時發生錯誤。',
},
com_ui_share_delete_error: {
english: 'There was an error deleting the shared link.',
translated: '刪除共享鏈接時出錯。',
translated: '刪除共享連結時發生錯誤。',
},
com_ui_share_error: {
english: 'There was an error sharing the chat link',
@@ -784,7 +784,7 @@ export const comparisons = {
},
com_ui_share_update_message: {
english: 'Your name, custom instructions, and any messages you add after sharing stay private.',
translated: '您的姓名、自定義指示以及您在共享後新增的任何訊息都會保密。',
translated: '您的姓名、自訂提示指令以及您在共享後新增的任何訊息都會保密。',
},
com_ui_share_updated_message: {
english:
@@ -1135,15 +1135,15 @@ export const comparisons = {
},
com_endpoint_google_custom_name_placeholder: {
english: 'Set a custom name for Google',
translated: '為 Google 設定自定義名稱',
translated: '為 Google 設定自名稱',
},
com_endpoint_prompt_prefix_placeholder: {
english: 'Set custom instructions or context. Ignored if empty.',
translated: '設定自定義提示或前後文。如果為空則忽略。',
translated: '設定自提示或前後文。如果為空則忽略。',
},
com_endpoint_custom_name: {
english: 'Custom Name',
translated: '自定義名稱',
translated: '自名稱',
},
com_endpoint_prompt_prefix: {
english: 'Custom Instructions',
@@ -1200,11 +1200,11 @@ export const comparisons = {
},
com_endpoint_openai_custom_name_placeholder: {
english: 'Set a custom name for the AI',
translated: '為 ChatGPT 設定自定義名稱',
translated: '為 ChatGPT 設定自名稱',
},
com_endpoint_openai_prompt_prefix_placeholder: {
english: 'Set custom instructions to include in System Message. Default: none',
translated: '在系統訊息中設定自定義提示。',
translated: '在系統訊息中設定自提示。',
},
com_endpoint_anthropic_temp: {
english:
@@ -1232,7 +1232,7 @@ export const comparisons = {
},
com_endpoint_anthropic_custom_name_placeholder: {
english: 'Set a custom name for Anthropic',
translated: '為 Anthropic 設定自定義名稱',
translated: '為 Anthropic 設定自名稱',
},
com_endpoint_frequency_penalty: {
english: 'Frequency Penalty',
@@ -1260,7 +1260,7 @@ export const comparisons = {
},
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: {
english: 'Set custom instructions to include in System Message. Default: none',
translated: '在系統訊息中新增自定義提示。',
translated: '在系統訊息中新增自提示。',
},
com_endpoint_import: {
english: 'Import',
@@ -1268,7 +1268,7 @@ export const comparisons = {
},
com_endpoint_set_custom_name: {
english: 'Set a custom name, in case you can find this preset',
translated: '設定自定義名稱,以便您找到此預設設定',
translated: '設定自名稱,以便您找到此預設設定',
},
com_endpoint_preset: {
english: 'preset',
@@ -1753,7 +1753,7 @@ export const comparisons = {
english:
'It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We\'re unable to proceed with this specific topic. If you have any other questions or topics you\'d like to explore, please edit your message, or create a new conversation.',
translated:
'似乎您所提交的內容被我們的內容審查系統標記為不符合社群準則。我們無法就此特定主題繼續進行。如果您有任何其他問題或想要探討的主題,請編輯您的訊息或開啟新的對話。',
'似乎您所提交的內容被我們的內容審查系統標記為不符合社群準則。我們無法就此特定主題繼續進行。如果您有任何其他問題或想要探討的主題請編輯您的訊息或開啟新的對話。',
},
com_error_no_user_key: {
english: 'No key found. Please provide a key and try again.',
@@ -1793,7 +1793,7 @@ export const comparisons = {
},
com_sidepanel_assistant_builder: {
english: 'Assistant Builder',
translated: '助建構器',
translated: '助建構器',
},
com_sidepanel_hide_panel: {
english: 'Hide Panel',
@@ -1912,7 +1912,7 @@ export const comparisons = {
},
com_assistants_instructions_placeholder: {
english: 'The system instructions that the assistant uses',
translated: '系統指令是助理使用的指示',
translated: '系統指令是助理使用的提示指令',
},
com_assistants_description_placeholder: {
english: 'Optional: Describe your Assistant here',
@@ -1920,7 +1920,7 @@ export const comparisons = {
},
com_assistants_actions_disabled: {
english: 'You need to create an assistant before adding actions.',
translated: '您需要先建立一個助,才能新增動作。',
translated: '您需要先建立一個助,才能新增動作。',
},
com_assistants_update_success: {
english: 'Successfully updated',
@@ -2197,7 +2197,7 @@ export const comparisons = {
},
com_ui_image_gen: {
english: 'Image Gen',
translated: '影生成',
translated: '影生成',
},
com_ui_assistant: {
english: 'Assistant',
@@ -2274,7 +2274,7 @@ export const comparisons = {
},
com_endpoint_context_tokens: {
english: 'Max Context Tokens',
translated: '最大前後文 Token 數',
translated: '最大前後文 token 數',
},
com_endpoint_context_info: {
english:
@@ -2285,20 +2285,20 @@ export const comparisons = {
com_endpoint_instructions_assistants_placeholder: {
english:
'Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.',
translated: '覆寫助理的指示。這對於在每次執行時修改行為很有用。',
translated: '覆寫助理的提示指令。這對於在每次執行時修改行為很有用。',
},
com_endpoint_prompt_prefix_assistants_placeholder: {
english:
'Set additional instructions or context on top of the Assistant\'s main instructions. Ignored if empty.',
translated: '在助的主要指示之上設定額外的指示或上下文。如果為空白,則會被忽略。',
translated: '在助的主要提示指令之上設定額外的提示指令或上下文。如果為空白,則會被忽略。',
},
com_endpoint_prompt_prefix_assistants: {
english: 'Additional Instructions',
translated: '提示字首',
translated: '額外的提示指令',
},
com_endpoint_instructions_assistants: {
english: 'Override Instructions',
translated: '覆寫指示',
translated: '覆寫提示指令',
},
com_endpoint_stop: {
english: 'Stop Sequences',
@@ -2330,7 +2330,7 @@ export const comparisons = {
english:
'The resolution for Vision requests. "Low" is cheaper and faster, "High" is more detailed and expensive, and "Auto" will automatically choose between the two based on the image resolution.',
translated:
'「低」解析度的視覺請求較便宜且快速,「高」解析度則更詳細但成本較高,而「自動」會根據影解析度自動在兩者之間選擇。',
'「低」解析度的視覺請求較便宜且快速,「高」解析度則更詳細但成本較高,而「自動」會根據影解析度自動在兩者之間選擇。',
},
com_endpoint_openai_stop: {
english: 'Up to 4 sequences where the API will stop generating further tokens.',
@@ -2346,7 +2346,7 @@ export const comparisons = {
},
com_endpoint_plug_image_detail: {
english: 'Image Detail',
translated: '影詳細資訊',
translated: '影詳細資訊',
},
com_endpoint_preset_delete_confirm: {
english: 'Are you sure you want to delete this preset?',
@@ -2406,11 +2406,11 @@ export const comparisons = {
},
com_endpoint_assistant: {
english: 'Assistant',
translated: '助',
translated: '助',
},
com_endpoint_use_active_assistant: {
english: 'Use Active Assistant',
translated: '使用活躍助',
translated: '使用活躍助',
},
com_endpoint_assistant_model: {
english: 'Assistant Model',

View File

@@ -50,6 +50,7 @@ const localStorageAtoms = {
textToSpeech: atomWithLocalStorage('textToSpeech', true),
engineTTS: atomWithLocalStorage('engineTTS', 'browser'),
voice: atomWithLocalStorage('voice', ''),
cloudBrowserVoices: atomWithLocalStorage('cloudBrowserVoices', false),
languageTTS: atomWithLocalStorage('languageTTS', ''),
automaticPlayback: atomWithLocalStorage('automaticPlayback', false),
playbackRate: atomWithLocalStorage<number | null>('playbackRate', null),

View File

@@ -107,7 +107,7 @@ export const getFileType = (
* @example
* formatDate('2020-01-01T00:00:00.000Z') // '1 Jan 2020'
*/
export function formatDate(dateString) {
export function formatDate(dateString: string) {
const months = [
'Jan',
'Feb',

View File

@@ -16,3 +16,7 @@ import '@testing-library/jest-dom/extend-expect';
// Mock canvas when run unit test cases with jest.
// 'react-lottie' uses canvas
import 'jest-canvas-mock';
beforeEach(() => {
jest.clearAllMocks();
});

277
package-lock.json generated
View File

@@ -1116,7 +1116,7 @@
"@ariakit/react": "^0.4.5",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^1.7.13",
"@headlessui/react": "^2.1.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.3",
@@ -1155,6 +1155,7 @@
"match-sorter": "^6.3.4",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",
"react-avatar-editor": "^13.0.2",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
@@ -1250,7 +1251,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
"integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -2274,7 +2274,6 @@
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
@@ -2287,7 +2286,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -2299,7 +2297,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -2313,7 +2310,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@@ -2321,14 +2317,12 @@
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
@@ -2337,7 +2331,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -2346,7 +2339,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -2358,7 +2350,6 @@
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
"integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2367,7 +2358,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
@@ -2397,7 +2387,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -2406,7 +2395,6 @@
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
@@ -2445,7 +2433,6 @@
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
"integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.23.5",
"@babel/helper-validator-option": "^7.23.5",
@@ -2461,7 +2448,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -2528,7 +2514,6 @@
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz",
"integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==",
"dev": true,
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -2544,7 +2529,6 @@
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2553,7 +2537,6 @@
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
@@ -2566,7 +2549,6 @@
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
},
@@ -2590,7 +2572,6 @@
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
"integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.15"
},
@@ -2602,7 +2583,6 @@
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
"integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-module-imports": "^7.22.15",
@@ -2633,7 +2613,6 @@
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
"integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2676,7 +2655,6 @@
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
"integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
},
@@ -2700,7 +2678,6 @@
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
},
@@ -2712,7 +2689,6 @@
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2721,7 +2697,6 @@
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2730,7 +2705,6 @@
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
"integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -2753,7 +2727,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz",
"integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==",
"dev": true,
"dependencies": {
"@babel/template": "^7.23.9",
"@babel/traverse": "^7.23.9",
@@ -2767,7 +2740,6 @@
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
@@ -2781,7 +2753,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -2793,7 +2764,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -2807,7 +2777,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@@ -2815,14 +2784,12 @@
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
@@ -2831,7 +2798,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -2840,7 +2806,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -2852,7 +2817,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
"integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -3938,7 +3902,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz",
"integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==",
"dev": true,
"dependencies": {
"@babel/helper-module-imports": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -3958,7 +3921,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -4297,7 +4259,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
"integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@babel/parser": "^7.23.9",
@@ -4311,7 +4272,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz",
"integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.23.5",
"@babel/generator": "^7.23.6",
@@ -4332,7 +4292,6 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -4341,7 +4300,6 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
@@ -6002,12 +5960,28 @@
"@floating-ui/utils": "^0.2.1"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
"integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
"node_modules/@floating-ui/react": {
"version": "0.26.19",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.19.tgz",
"integrity": "sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.1"
"@floating-ui/react-dom": "^2.1.1",
"@floating-ui/utils": "^0.2.4",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
"integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -6015,9 +5989,10 @@
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {
"version": "0.1.3",
@@ -6057,19 +6032,22 @@
}
},
"node_modules/@headlessui/react": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
"integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.2.tgz",
"integrity": "sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==",
"license": "MIT",
"dependencies": {
"@tanstack/react-virtual": "^3.0.0-beta.60",
"client-only": "^0.0.1"
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
"react": "^18",
"react-dom": "^18"
}
},
"node_modules/@humanwhocodes/config-array": {
@@ -8573,6 +8551,86 @@
"node": ">=8.x"
}
},
"node_modules/@react-aria/focus": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz",
"integrity": "sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.21.3",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/focus/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.21.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz",
"integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.4",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz",
"integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/utils": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz",
"integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.4",
"@react-stately/utils": "^3.10.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/utils/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
@@ -8588,6 +8646,27 @@
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
},
"node_modules/@react-stately/utils": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz",
"integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-types/shared": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz",
"integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz",
@@ -9584,6 +9663,15 @@
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz",
"integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tanstack/match-sorter-utils": {
"version": "8.11.8",
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz",
@@ -9675,11 +9763,12 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.4.tgz",
"integrity": "sha512-tiqKW/e2MJVCr7/pRUXulpkyxllaOclkHNfhKTo4pmHjJIqnhMfwIjc1Q1R0Un3PI3kQywywu/791c8z9u0qeA==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.2.tgz",
"integrity": "sha512-g78+DA29K0ByAfDkuibfLQqDshf8Aha/zcyEZ+huAX/yS/TWj/CUiEY4IJfDrFacdxIFmsLm0u4VtsLSKTngRw==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.0.0"
"@tanstack/virtual-core": "3.8.2"
},
"funding": {
"type": "github",
@@ -9703,9 +9792,10 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz",
"integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.2.tgz",
"integrity": "sha512-ffpN6kTaPGwQPoWMcBAHbdv2ZCpj1SugldoYAcY0C4xH+Pej1KCOEUisNeEgbUnXOp8Y/4q6wGPu2tFHthOIQw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
@@ -11356,7 +11446,6 @@
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz",
"integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.6",
"@babel/helper-define-polyfill-provider": "^0.5.0",
@@ -11370,7 +11459,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -11379,7 +11467,6 @@
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz",
"integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==",
"dev": true,
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.5.0",
"core-js-compat": "^3.34.0"
@@ -11392,7 +11479,6 @@
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz",
"integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==",
"dev": true,
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.5.0"
},
@@ -11844,7 +11930,6 @@
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
"integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -12077,7 +12162,6 @@
"version": "1.0.30001591",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
"integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -12344,11 +12428,6 @@
"node": ">= 12"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clipboardy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
@@ -12682,8 +12761,7 @@
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"node_modules/cookie": {
"version": "0.5.0",
@@ -12742,7 +12820,6 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
"integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
"dev": true,
"dependencies": {
"browserslist": "^4.22.2"
},
@@ -13608,8 +13685,7 @@
"node_modules/electron-to-chromium": {
"version": "1.4.656",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz",
"integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==",
"dev": true
"integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q=="
},
"node_modules/elliptic": {
"version": "6.5.4",
@@ -15492,7 +15568,6 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -18510,7 +18585,6 @@
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true,
"bin": {
"jsesc": "bin/jsesc"
},
@@ -18567,7 +18641,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
},
@@ -19679,7 +19752,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"dependencies": {
"yallist": "^3.0.2"
}
@@ -21200,8 +21272,7 @@
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node_modules/node-stdlib-browser": {
"version": "1.2.0",
@@ -24244,6 +24315,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-avatar-editor": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
"integrity": "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ==",
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/runtime": "^7.12.5",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
@@ -26472,6 +26558,12 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"devOptional": true
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
@@ -26835,7 +26927,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -27542,7 +27633,6 @@
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -29247,8 +29337,7 @@
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/yaml": {
"version": "2.3.4",

View File

@@ -58,9 +58,7 @@
"b:test:client": "cd client && bun run b:test",
"b:test:api": "cd api && bun run b:test",
"b:balance": "bun config/add-balance.js",
"b:list-balances": "bun config/list-balances.js",
"pull:rag": "docker compose -f ./rag.yml pull",
"copy-ex": "cp .env.example .env"
"b:list-balances": "bun config/list-balances.js"
},
"repository": {
"type": "git",

View File

@@ -6,7 +6,6 @@ import { fileConfigSchema } from './file-config';
import { specsConfigSchema } from './models';
import { FileSources } from './types/files';
import { TModelsConfig } from './types';
import { speech } from './api-endpoints';
export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord'];
@@ -234,6 +233,15 @@ const ttsOpenaiSchema = z.object({
voices: z.array(z.string()),
});
const ttsAzureOpenAISchema = z.object({
instanceName: z.string(),
apiKey: z.string(),
deploymentName: z.string(),
apiVersion: z.string(),
model: z.string(),
voices: z.array(z.string()),
});
const ttsElevenLabsSchema = z.object({
url: z.string().optional(),
websocketUrl: z.string().optional(),
@@ -260,18 +268,27 @@ const ttsLocalaiSchema = z.object({
const ttsSchema = z.object({
openai: ttsOpenaiSchema.optional(),
azureOpenAI: ttsAzureOpenAISchema.optional(),
elevenLabs: ttsElevenLabsSchema.optional(),
localai: ttsLocalaiSchema.optional(),
});
const sttOpenaiSchema = z.object({
url: z.string().optional(),
apiKey: z.string(),
model: z.string(),
});
const sttAzureOpenAISchema = z.object({
instanceName: z.string(),
apiKey: z.string(),
deploymentName: z.string(),
apiVersion: z.string(),
});
const sttSchema = z.object({
openai: z
.object({
url: z.string().optional(),
apiKey: z.string().optional(),
model: z.string().optional(),
})
.optional(),
openai: sttOpenaiSchema.optional(),
azureOpenAI: sttAzureOpenAISchema.optional(),
});
const speechTab = z
@@ -846,6 +863,36 @@ export enum SettingsTabValues {
ACCOUNT = 'account',
}
export enum STTProviders {
/**
* Provider for OpenAI STT
*/
OPENAI = 'openai',
/**
* Provider for Microsoft Azure STT
*/
AZURE_OPENAI = 'azureOpenAI',
}
export enum TTSProviders {
/**
* Provider for OpenAI TTS
*/
OPENAI = 'openai',
/**
* Provider for Microsoft Azure OpenAI TTS
*/
AZURE_OPENAI = 'azureOpenAI',
/**
* Provider for ElevenLabs TTS
*/
ELEVENLABS = 'elevenlabs',
/**
* Provider for LocalAI TTS
*/
LOCALAI = 'localai',
}
/** Enum for app-wide constants */
export enum Constants {
/** Key for the app's version. */

View File

@@ -355,7 +355,7 @@ export const getVoices = (): Promise<f.VoiceResponse> => {
return request.get(endpoints.textToSpeechVoices());
};
export const getCustomConfigSpeech = (): Promise<f.getCustomConfigSpeechResponse[]> => {
export const getCustomConfigSpeech = (): Promise<t.TCustomConfigSpeechResponse> => {
return request.get(endpoints.getCustomConfigSpeech());
};

View File

@@ -422,3 +422,18 @@ export const useGetStartupConfig = (
},
);
};
export const useGetCustomConfigSpeechQuery = (
config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {
return useQuery<t.TCustomConfigSpeechResponse>(
[QueryKeys.customConfigSpeech],
() => dataService.getCustomConfigSpeech(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};

View File

@@ -463,3 +463,5 @@ export type TGetRandomPromptsRequest = {
limit: number;
skip: number;
};
export type TCustomConfigSpeechResponse = { [key: string]: string };

View File

@@ -83,8 +83,6 @@ export type SpeechToTextResponse = {
export type VoiceResponse = string[];
export type getCustomConfigSpeechResponse = { [key: string]: string };
export type UploadMutationOptions = {
onSuccess?: (data: TFileUpload, variables: FormData, context?: unknown) => void;
onMutate?: (variables: FormData) => void | Promise<unknown>;
@@ -115,12 +113,6 @@ export type VoiceOptions = {
onError?: (error: unknown, variables: unknown, context?: unknown) => void;
};
export type getCustomConfigSpeechOptions = {
onSuccess?: (data: getCustomConfigSpeechResponse, variables: unknown, context?: unknown) => void;
onMutate?: () => void | Promise<unknown>;
onError?: (error: unknown, variables: unknown, context?: unknown) => void;
};
export type DeleteFilesResponse = {
message: string;
result: Record<string, unknown>;