Compare commits
10 Commits
codespaces
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d69465ea3d | ||
|
|
5ef71a7a36 | ||
|
|
326069d7a6 | ||
|
|
785430daf5 | ||
|
|
03fe361917 | ||
|
|
b34a4ddac1 | ||
|
|
7d5b03dd98 | ||
|
|
f959ee302c | ||
|
|
cd00df69bb | ||
|
|
a05e2c1dcc |
@@ -1,2 +0,0 @@
|
||||
# When running devcontainers, you can specify if docker & docker-compose should be installed in your environment
|
||||
INSTALL_DOCKER=false
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -436,6 +436,3 @@ HELP_AND_FAQ_URL=https://librechat.ai
|
||||
|
||||
# E2E_USER_EMAIL=
|
||||
# E2E_USER_PASSWORD=
|
||||
|
||||
# RAG_PORT
|
||||
RAG_PORT=8000
|
||||
@@ -112,7 +112,6 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
progressCallback,
|
||||
progressOptions: {
|
||||
res,
|
||||
text,
|
||||
// parentMessageId: overrideParentMessageId || userMessageId,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -119,7 +119,6 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
progressCallback,
|
||||
progressOptions: {
|
||||
res,
|
||||
text,
|
||||
// parentMessageId: overrideParentMessageId || userMessageId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -195,7 +195,6 @@ router.post(
|
||||
progressCallback,
|
||||
progressOptions: {
|
||||
res,
|
||||
text,
|
||||
// parentMessageId: overrideParentMessageId || userMessageId,
|
||||
plugins,
|
||||
},
|
||||
|
||||
@@ -168,7 +168,6 @@ router.post(
|
||||
progressCallback,
|
||||
progressOptions: {
|
||||
res,
|
||||
text,
|
||||
plugin,
|
||||
// parentMessageId: overrideParentMessageId || userMessageId,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ?? '',
|
||||
)}
|
||||
|
||||
@@ -178,7 +178,7 @@ const SetKeyDialog = ({
|
||||
value={expiresAtLabel}
|
||||
onChange={handleExpirationChange}
|
||||
options={expirationOptions.map((option) => option.display)}
|
||||
width={185}
|
||||
sizeClasses="w-[185px]"
|
||||
/>
|
||||
<FormProvider {...methods}>
|
||||
<EndpointComponent
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
664
client/src/localization/languages/Fi.ts
Normal file
664
client/src/localization/languages/Fi.ts
Normal 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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
277
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -463,3 +463,5 @@ export type TGetRandomPromptsRequest = {
|
||||
limit: number;
|
||||
skip: number;
|
||||
};
|
||||
|
||||
export type TCustomConfigSpeechResponse = { [key: string]: string };
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user