Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f101419af3 | ||
|
|
251d8ac410 | ||
|
|
bdccadbe06 | ||
|
|
70a56ac04a | ||
|
|
193ed93ee8 | ||
|
|
b096fb98ce | ||
|
|
002bba20f9 | ||
|
|
b896225bd8 | ||
|
|
de34d8b47c | ||
|
|
759e585c29 | ||
|
|
c6f5d5d65c | ||
|
|
cb3cf9b33e | ||
|
|
68ad46a9be | ||
|
|
600a0d15b1 | ||
|
|
92f87b8dcc | ||
|
|
06a7fba39b | ||
|
|
96d29f7390 | ||
|
|
5828200197 | ||
|
|
173b8ce2da | ||
|
|
1f5a79f073 | ||
|
|
495ffaeb06 | ||
|
|
c7b586ba4c | ||
|
|
d6dbd56e33 | ||
|
|
315faf707e | ||
|
|
d79f585052 | ||
|
|
6ee0dbfdbd | ||
|
|
60d0e97425 | ||
|
|
956aa6c674 | ||
|
|
fb99e5a7da | ||
|
|
0630b54193 | ||
|
|
851dce720f | ||
|
|
30a49ae611 | ||
|
|
2faeebfae2 | ||
|
|
41ed33e792 | ||
|
|
da60d77a14 | ||
|
|
b5aadc4b6d | ||
|
|
545342bbcb | ||
|
|
2c00279aaf | ||
|
|
77a76f8511 | ||
|
|
488b373695 | ||
|
|
94764c9c2a | ||
|
|
e9c981c202 | ||
|
|
bec1d245bd | ||
|
|
131cb6cddb | ||
|
|
a2b6e9a6a8 | ||
|
|
428fd5bed8 | ||
|
|
9cacf76c10 | ||
|
|
7b8036a369 | ||
|
|
d56817850c | ||
|
|
f88a0685f7 | ||
|
|
ae51e6153f | ||
|
|
745eef2eb0 | ||
|
|
1f8520cdad | ||
|
|
dae2805d27 | ||
|
|
ba2e95db04 | ||
|
|
2a6e000217 | ||
|
|
32281d1b8d | ||
|
|
369b1f4eba | ||
|
|
777d64088b | ||
|
|
d59a3f20cb | ||
|
|
8959576d75 | ||
|
|
dd8bc39001 | ||
|
|
4898f7489b | ||
|
|
c9c77d6fdf | ||
|
|
4dc86c4c18 | ||
|
|
6ae807c404 | ||
|
|
b5353e2640 | ||
|
|
f4f1199a55 | ||
|
|
b6028a3434 | ||
|
|
abef8c02c1 | ||
|
|
19af2b06ce | ||
|
|
2f7658e39f | ||
|
|
d3138c79fc | ||
|
|
1e49b7ecb1 | ||
|
|
3b865fbc59 | ||
|
|
afd894553c | ||
|
|
df485e5bfe | ||
|
|
77252bafc1 | ||
|
|
bd1d5e991d | ||
|
|
18c4883ae0 | ||
|
|
197307d514 | ||
|
|
130356654c | ||
|
|
8f9f09698b | ||
|
|
5da833e066 | ||
|
|
b64273957a | ||
|
|
4148c6d219 | ||
|
|
e9d68e3bef | ||
|
|
bbe690cc4b | ||
|
|
a1ad471d87 | ||
|
|
c319d709f3 | ||
|
|
6943f1c2c7 | ||
|
|
e38483a8b9 | ||
|
|
2a2e6d9991 | ||
|
|
deb1472aa5 | ||
|
|
8aa58ea240 | ||
|
|
3a112a344d | ||
|
|
712be248be | ||
|
|
530f9d303f | ||
|
|
ad29d25396 | ||
|
|
1ef53a41f0 | ||
|
|
0246f164b0 |
@@ -39,7 +39,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": ""
|
||||
"postCreateCommand": "",
|
||||
// "workspaceMount": "src=${localWorkspaceFolder},dst=/code,type=bind,consistency=cached"
|
||||
|
||||
// "runArgs": [
|
||||
@@ -54,4 +54,5 @@
|
||||
// "settings": {
|
||||
// "terminal.integrated.shell.linux": "/bin/bash"
|
||||
// },
|
||||
}
|
||||
"features": {"ghcr.io/devcontainers/features/git:1": {}}
|
||||
}
|
||||
|
||||
14
.env.example
14
.env.example
@@ -32,7 +32,7 @@ OPENAI_API_KEY="user_provided"
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
# OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,text-davinci-003,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
|
||||
# Reverse proxy settings for OpenAI:
|
||||
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
||||
@@ -124,7 +124,7 @@ ANTHROPIC_MODELS=claude-1,claude-instant-1,claude-2
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
# The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
PLUGIN_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
# PLUGIN_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-16k,gpt-3.5-turbo-0301,gpt-4,gpt-4-0314,gpt-4-0613
|
||||
|
||||
# For securely storing credentials, you need a fixed key and IV. You can set them here for prod and dev environments
|
||||
# If you don't set them, the app will crash on startup.
|
||||
@@ -261,3 +261,13 @@ DISCORD_CALLBACK_URL=/oauth/discord/callback # this should be the same for every
|
||||
|
||||
DOMAIN_CLIENT=http://localhost:3080
|
||||
DOMAIN_SERVER=http://localhost:3080
|
||||
|
||||
###########################
|
||||
# Email
|
||||
###########################
|
||||
|
||||
# Email is used for password reset. Note that all 4 values must be set for email to work.
|
||||
EMAIL_SERVICE= # eg. gmail
|
||||
EMAIL_USERNAME= # eg. your email address if using gmail
|
||||
EMAIL_PASSWORD= # eg. this is the "app password" if using gmail
|
||||
EMAIL_FROM= # eg. email address for from field like noreply@librechat.ai
|
||||
|
||||
27
.eslintrc.js
27
.eslintrc.js
@@ -13,7 +13,6 @@ module.exports = {
|
||||
'plugin:jest/recommended',
|
||||
'prettier',
|
||||
],
|
||||
// ignorePatterns: ['packages/data-provider/types/**/*'],
|
||||
ignorePatterns: [
|
||||
'client/dist/**/*',
|
||||
'client/public/**/*',
|
||||
@@ -29,7 +28,7 @@ module.exports = {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow' }],
|
||||
@@ -44,15 +43,17 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
'curly': ['error', 'all'],
|
||||
'semi': ['error', 'always'],
|
||||
'no-trailing-spaces': 'error',
|
||||
curly: ['error', 'all'],
|
||||
semi: ['error', 'always'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'no-multiple-empty-lines': ['error', { max: 1 }],
|
||||
'no-trailing-spaces': 'error',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-self-import': 'error',
|
||||
'import/extensions': 'off',
|
||||
'no-promise-executor-return': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
@@ -100,7 +101,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
files: '**/*.+(ts)',
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './client/tsconfig.json',
|
||||
@@ -110,6 +111,9 @@ module.exports = {
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: './packages/data-provider/**/*.ts',
|
||||
@@ -132,5 +136,16 @@ module.exports = {
|
||||
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
||||
version: 'detect', // React version. "detect" automatically picks the version you have installed.
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./client/tsconfig.json'],
|
||||
},
|
||||
node: {
|
||||
project: ['./client/tsconfig.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -7,7 +7,7 @@ version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/api" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
target-branch: "dev"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -20,7 +20,7 @@ updates:
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/client" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
target-branch: "dev"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -33,7 +33,7 @@ updates:
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
target-branch: "develop"
|
||||
target-branch: "dev"
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
62
.github/playwright.yml
vendored
62
.github/playwright.yml
vendored
@@ -1,62 +0,0 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [feat/playwright-jest-cicd]
|
||||
pull_request:
|
||||
branches: [feat/playwright-jest-cicd]
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run Playwright tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# BINGAI_TOKEN: ${{ secrets.BINGAI_TOKEN }}
|
||||
# CHATGPT_TOKEN: ${{ secrets.CHATGPT_TOKEN }}
|
||||
MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
|
||||
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
||||
CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
# NODE_ENV: ${{ vars.NODE_ENV }}
|
||||
DOMAIN_CLIENT: ${{ vars.DOMAIN_CLIENT }}
|
||||
DOMAIN_SERVER: ${{ vars.DOMAIN_SERVER }}
|
||||
# PALM_KEY: ${{ secrets.PALM_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install global dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./api
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Install Client dependencies
|
||||
working-directory: ./client
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Build Client
|
||||
run: cd client && npm run build:ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps && npm install -D @playwright/test
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npm run backend & sleep 10
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test --config=e2e/playwright.config.ts
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
53
.github/pull_request_template.md
vendored
53
.github/pull_request_template.md
vendored
@@ -1,35 +1,48 @@
|
||||
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
# Pull Request Template
|
||||
|
||||
|
||||
### ⚠️ Pre-Submission Steps:
|
||||
|
||||
## Type of change
|
||||
1. Before starting work, make sure your main branch has the latest commits with `npm run update`
|
||||
2. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning.
|
||||
3. After your changes, reinstall packages in your current branch using `npm run reinstall` and ensure everything still works.
|
||||
- Restart the ESLint server ("ESLint: Restart ESLint Server" in VS Code command bar) and your IDE after reinstalling or updating.
|
||||
4. Clear web app localStorage and cookies before and after changes.
|
||||
5. For frontend changes:
|
||||
- Install typescript globally: `npm i -g typescript`.
|
||||
- Compile typescript before and after changes to check for introduced errors: `tsc --noEmit`.
|
||||
6. Run tests locally:
|
||||
- Backend unit tests: `npm run test:api`
|
||||
- Frontend unit tests: `npm run test:client`
|
||||
- Integration tests: `npm run e2e` (requires playwright installed, `npx install playwright`)
|
||||
|
||||
Please delete options that are not relevant.
|
||||
## Summary
|
||||
|
||||
Please provide a brief summary of your changes and the related issue. Include any motivation and context that is relevant to your changes. If there are any dependencies necessary for your changes, please list them here.
|
||||
|
||||
## Change Type
|
||||
|
||||
Please delete any irrelevant options.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] Documentation update
|
||||
|
||||
- [ ] Documentation update
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration:
|
||||
##
|
||||
## Testing
|
||||
|
||||
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
|
||||
|
||||
### **Test Configuration**:
|
||||
##
|
||||
|
||||
## Checklist
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
- [ ] My code adheres to this project's style guidelines
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented in any complex areas of my code
|
||||
- [ ] I have made pertinent documentation changes
|
||||
- [ ] My changes do not introduce new warnings
|
||||
- [ ] I have written tests demonstrating that my changes are effective or that my feature works
|
||||
- [ ] Local unit tests pass with my changes
|
||||
- [ ] Any changes dependent on mine have been merged and published in downstream modules.
|
||||
|
||||
28
.github/wip-playwright.yml
vendored
28
.github/wip-playwright.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run end-to-end tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
16
.github/workflows/backend-review.yml
vendored
16
.github/workflows/backend-review.yml
vendored
@@ -1,15 +1,17 @@
|
||||
name: Backend Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - release/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- 'api/**'
|
||||
jobs:
|
||||
tests_Backend:
|
||||
name: Run Backend unit tests
|
||||
@@ -23,10 +25,10 @@ jobs:
|
||||
CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 19.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19.x
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
38
.github/workflows/build.yml
vendored
Normal file
38
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Linux_Container_Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.293.0
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# checkout the repo
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: 'Login via Azure CLI'
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: 'Build GitHub Runner container image'
|
||||
uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker build --build-arg RUNNER_VERSION=${{ env.RUNNER_VERSION }} -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }} .
|
||||
|
||||
- name: 'Push container image to ACR'
|
||||
uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- run: |
|
||||
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
||||
34
.github/workflows/data-provider.yml
vendored
Normal file
34
.github/workflows/data-provider.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Node.js Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/data-provider/package.json'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- run: cd packages/data-provider && npm ci
|
||||
- run: cd packages/data-provider && npm run build
|
||||
|
||||
publish-npm:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: cd packages/data-provider && npm ci
|
||||
- run: cd packages/data-provider && npm run build
|
||||
- run: cd packages/data-provider && npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
38
.github/workflows/deploy.yml
vendored
Normal file
38
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Deploy_GHRunner_Linux_ACI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.293.0
|
||||
ACI_RESOURCE_GROUP: 'Demo-ACI-GitHub-Runners-RG'
|
||||
ACI_NAME: 'gh-runner-linux-01'
|
||||
DNS_NAME_LABEL: 'gh-lin-01'
|
||||
GH_OWNER: ${{ github.repository_owner }}
|
||||
GH_REPOSITORY: 'LibreChat' #Change here to deploy self hosted runner ACI to another repo.
|
||||
|
||||
jobs:
|
||||
deploy-gh-runner-aci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# checkout the repo
|
||||
- name: 'Checkout GitHub Action'
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: 'Login via Azure CLI'
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: 'Deploy to Azure Container Instances'
|
||||
uses: 'azure/aci-deploy@v1'
|
||||
with:
|
||||
resource-group: ${{ env.ACI_RESOURCE_GROUP }}
|
||||
image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }}
|
||||
registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
|
||||
registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
name: ${{ env.ACI_NAME }}
|
||||
dns-name-label: ${{ env.DNS_NAME_LABEL }}
|
||||
environment-variables: GH_TOKEN=${{ secrets.PAT_TOKEN }} GH_OWNER=${{ env.GH_OWNER }} GH_REPOSITORY=${{ env.GH_REPOSITORY }}
|
||||
location: 'eastus'
|
||||
51
.github/workflows/dev-images.yml
vendored
Normal file
51
.github/workflows/dev-images.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Docker Dev Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build Docker images
|
||||
- name: Build Docker images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker build -f Dockerfile.multi --target api-build -t librechat-dev-api .
|
||||
docker build -f Dockerfile -t librechat-dev .
|
||||
|
||||
# Tag and push the images to GitHub Container Registry
|
||||
- name: Tag and push images
|
||||
run: |
|
||||
docker tag librechat-dev-api:latest ghcr.io/${{ github.repository_owner }}/librechat-dev-api:${{ github.sha }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev-api:${{ github.sha }}
|
||||
docker tag librechat-dev-api:latest ghcr.io/${{ github.repository_owner }}/librechat-dev-api:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev-api:latest
|
||||
|
||||
docker tag librechat-dev:latest ghcr.io/${{ github.repository_owner }}/librechat-dev:${{ github.sha }}
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev:${{ github.sha }}
|
||||
docker tag librechat-dev:latest ghcr.io/${{ github.repository_owner }}/librechat-dev:latest
|
||||
docker push ghcr.io/${{ github.repository_owner }}/librechat-dev:latest
|
||||
17
.github/workflows/frontend-review.yml
vendored
17
.github/workflows/frontend-review.yml
vendored
@@ -1,16 +1,19 @@
|
||||
#github action to run unit tests for frontend with jest
|
||||
name: Frontend Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - release/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
jobs:
|
||||
tests_frontend:
|
||||
name: Run frontend unit tests
|
||||
@@ -18,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 19.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19.x
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
81
.github/workflows/playwright.yml
vendored
Normal file
81
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
- 'e2e/**'
|
||||
jobs:
|
||||
tests_e2e:
|
||||
name: Run Playwright tests
|
||||
if: github.event.pull_request.head.repo.full_name == 'danny-avila/LibreChat'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_ENV: ci
|
||||
CI: true
|
||||
SEARCH: false
|
||||
BINGAI_TOKEN: user_provided
|
||||
CHATGPT_TOKEN: user_provided
|
||||
MONGO_URI: ${{ secrets.MONGO_URI }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
|
||||
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
CREDS_KEY: ${{ secrets.CREDS_KEY }}
|
||||
CREDS_IV: ${{ secrets.CREDS_IV }}
|
||||
DOMAIN_CLIENT: ${{ secrets.DOMAIN_CLIENT }}
|
||||
DOMAIN_SERVER: ${{ secrets.DOMAIN_SERVER }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
|
||||
# - name: Cache Node.js modules
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
|
||||
- name: Install global dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Remove sharp dependency
|
||||
run: rm -rf node_modules/sharp
|
||||
|
||||
- name: Install sharp with linux dependencies
|
||||
run: cd api && SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp
|
||||
|
||||
- name: Build Client
|
||||
run: npm run frontend
|
||||
|
||||
# - name: Cache Playwright installations
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.cache/ms-playwright/
|
||||
# key: ${{ runner.os }}-pw-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-pw-
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium && npm install -D @playwright/test@latest
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run e2e:ci
|
||||
|
||||
- name: Upload playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e/playwright-report/
|
||||
retention-days: 30
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -50,6 +50,7 @@ types/
|
||||
# Environment
|
||||
.npmrc
|
||||
.env*
|
||||
my.secrets
|
||||
!**/.env.example
|
||||
!**/.env.test.example
|
||||
cache.json
|
||||
@@ -71,6 +72,7 @@ junit.xml
|
||||
|
||||
# meilisearch
|
||||
meilisearch
|
||||
meilisearch.exe
|
||||
data.ms/*
|
||||
auth.json
|
||||
|
||||
|
||||
40
Dockerfile.multi
Normal file
40
Dockerfile.multi
Normal file
@@ -0,0 +1,40 @@
|
||||
# Build API, Client and Data Provider
|
||||
FROM node:19-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
COPY config/loader.js ./config/
|
||||
RUN npm install dotenv
|
||||
|
||||
WORKDIR /app/api
|
||||
COPY api/package*.json ./
|
||||
COPY api/ ./
|
||||
RUN npm install
|
||||
|
||||
# React client build
|
||||
FROM base AS client-build
|
||||
WORKDIR /app/client
|
||||
COPY ./client/ ./
|
||||
|
||||
WORKDIR /app/packages/data-provider
|
||||
COPY ./packages/data-provider ./
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN mkdir -p /app/client/node_modules/librechat-data-provider/
|
||||
RUN cp -R /app/packages/data-provider/* /app/client/node_modules/librechat-data-provider/
|
||||
|
||||
WORKDIR /app/client
|
||||
RUN npm install
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
RUN npm run build
|
||||
|
||||
# Node API setup
|
||||
FROM base AS api-build
|
||||
COPY --from=client-build /app/client/dist /app/client/dist
|
||||
EXPOSE 3080
|
||||
ENV HOST=0.0.0.0
|
||||
CMD ["node", "server/index.js"]
|
||||
|
||||
# Nginx setup
|
||||
FROM nginx:1.21.1-alpine AS prod-stage
|
||||
COPY ./client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
28
README.md
28
README.md
@@ -31,7 +31,10 @@ LibreChat brings together the future of assistant AIs with the revolutionary tec
|
||||
|
||||
With LibreChat, you no longer need to opt for ChatGPT Plus and can instead use free or pay-per-call APIs. We welcome contributions, cloning, and forking to enhance the capabilities of this advanced chatbot platform.
|
||||
|
||||
https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59
|
||||
<!-- https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b982-84b278b53d59 -->
|
||||
|
||||
[](https://youtu.be/pNIOs1ovsXw)
|
||||
Click on the thumbnail to open the video☝️
|
||||
|
||||
# Features
|
||||
- Response streaming identical to ChatGPT through server-sent events
|
||||
@@ -44,7 +47,8 @@ https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b98
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ [Breaking Changes as of v0.5.0](docs/general_info/breaking_changes.md#v050) ⚠️
|
||||
## ⚠️ [Breaking Changes](docs/general_info/breaking_changes.md) ⚠️
|
||||
|
||||
**Please read this before updating from a previous version**
|
||||
|
||||
---
|
||||
@@ -59,12 +63,16 @@ Keep up with the latest updates by visiting the releases page - [Releases](https
|
||||
<details open>
|
||||
<summary><strong>Getting Started</strong></summary>
|
||||
|
||||
* [Docker Install](docs/install/docker_install.md)
|
||||
* [Linux Install](docs/install/linux_install.md)
|
||||
* [Mac Install](docs/install/mac_install.md)
|
||||
* [Windows Install](docs/install/windows_install.md)
|
||||
* [APIs and Tokens](docs/install/apis_and_tokens.md)
|
||||
* [User Auth System](docs/install/user_auth_system.md)
|
||||
* Installation
|
||||
* [Docker Install🐳](docs/install/docker_install.md)
|
||||
* [Linux Install🐧](docs/install/linux_install.md)
|
||||
* [Mac Install🍎](docs/install/mac_install.md)
|
||||
* [Windows Install💙](docs/install/windows_install.md)
|
||||
* Configuration
|
||||
* [APIs and Tokens](docs/install/apis_and_tokens.md)
|
||||
* [User Auth System](docs/install/user_auth_system.md)
|
||||
* [Online MongoDB Database](docs/install/mongodb.md)
|
||||
* [Languages](docs/install/languages.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -85,6 +93,7 @@ Keep up with the latest updates by visiting the releases page - [Releases](https
|
||||
* [Stable Diffusion](docs/features/plugins/stable_diffusion.md)
|
||||
* [Wolfram](docs/features/plugins/wolfram.md)
|
||||
* [Make Your Own Plugin](docs/features/plugins/make_your_own.md)
|
||||
* [Using official ChatGPT Plugins](docs/features/plugins/chatgpt_plugins_openapi.md)
|
||||
|
||||
* [Proxy](docs/features/proxy.md)
|
||||
* [Bing Jailbreak](docs/features/bing_jailbreak.md)
|
||||
@@ -99,11 +108,12 @@ Keep up with the latest updates by visiting the releases page - [Releases](https
|
||||
* [Cloudflare](docs/deployment/cloudflare.md)
|
||||
* [Ngrok](docs/deployment/ngrok.md)
|
||||
* [Render](docs/deployment/render.md)
|
||||
* [Azure](docs/deployment/azure-terraform.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Contributions</strong></summary>
|
||||
|
||||
|
||||
* [Contributor Guidelines](CONTRIBUTING.md)
|
||||
* [Documentation Guidelines](docs/contributions/documentation_guidelines.md)
|
||||
* [Code Standards and Conventions](docs/contributions/coding_conventions.md)
|
||||
|
||||
@@ -47,6 +47,16 @@ const askBing = async ({
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress,
|
||||
clientOptions: {
|
||||
features: {
|
||||
genImage: {
|
||||
server: {
|
||||
enable: true,
|
||||
type: 'markdown_list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
@@ -56,6 +66,16 @@ const askBing = async ({
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress,
|
||||
clientOptions: {
|
||||
features: {
|
||||
genImage: {
|
||||
server: {
|
||||
enable: true,
|
||||
type: 'markdown_list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// don't give those parameters for new conversation
|
||||
|
||||
@@ -6,7 +6,10 @@ const {
|
||||
} = require('@dqbd/tiktoken');
|
||||
const { maxTokensMap, genAzureChatCompletion } = require('../../utils');
|
||||
|
||||
// Cache to store Tiktoken instances
|
||||
const tokenizersCache = {};
|
||||
// Counter for keeping track of the number of tokenizer calls
|
||||
let tokenizerCallsCount = 0;
|
||||
|
||||
class OpenAIClient extends BaseClient {
|
||||
constructor(apiKey, options = {}) {
|
||||
@@ -89,7 +92,6 @@ class OpenAIClient extends BaseClient {
|
||||
this.chatGptLabel = this.options.chatGptLabel || 'Assistant';
|
||||
|
||||
this.setupTokens();
|
||||
this.setupTokenizer();
|
||||
|
||||
if (!this.modelOptions.stop) {
|
||||
const stopTokens = [this.startToken];
|
||||
@@ -133,68 +135,87 @@ class OpenAIClient extends BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
setupTokenizer() {
|
||||
// Selects an appropriate tokenizer based on the current configuration of the client instance.
|
||||
// It takes into account factors such as whether it's a chat completion, an unofficial chat GPT model, etc.
|
||||
selectTokenizer() {
|
||||
let tokenizer;
|
||||
this.encoding = 'text-davinci-003';
|
||||
if (this.isChatCompletion) {
|
||||
this.encoding = 'cl100k_base';
|
||||
this.gptEncoder = this.constructor.getTokenizer(this.encoding);
|
||||
tokenizer = this.constructor.getTokenizer(this.encoding);
|
||||
} else if (this.isUnofficialChatGptModel) {
|
||||
this.gptEncoder = this.constructor.getTokenizer(this.encoding, true, {
|
||||
const extendSpecialTokens = {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
});
|
||||
};
|
||||
tokenizer = this.constructor.getTokenizer(this.encoding, true, extendSpecialTokens);
|
||||
} else {
|
||||
try {
|
||||
this.encoding = this.modelOptions.model;
|
||||
this.gptEncoder = this.constructor.getTokenizer(this.modelOptions.model, true);
|
||||
tokenizer = this.constructor.getTokenizer(this.modelOptions.model, true);
|
||||
} catch {
|
||||
this.gptEncoder = this.constructor.getTokenizer(this.encoding, true);
|
||||
tokenizer = this.constructor.getTokenizer(this.encoding, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
freeAndResetEncoder() {
|
||||
try {
|
||||
if (!this.gptEncoder) {
|
||||
return;
|
||||
// Retrieves a tokenizer either from the cache or creates a new one if one doesn't exist in the cache.
|
||||
// If a tokenizer is being created, it's also added to the cache.
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
let tokenizer;
|
||||
if (tokenizersCache[encoding]) {
|
||||
tokenizer = tokenizersCache[encoding];
|
||||
} else {
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
this.gptEncoder.free();
|
||||
delete tokenizersCache[this.encoding];
|
||||
delete tokenizersCache.count;
|
||||
this.setupTokenizer();
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
}
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
// Frees all encoders in the cache and resets the count.
|
||||
static freeAndResetAllEncoders() {
|
||||
try {
|
||||
Object.keys(tokenizersCache).forEach((key) => {
|
||||
if (tokenizersCache[key]) {
|
||||
tokenizersCache[key].free();
|
||||
delete tokenizersCache[key];
|
||||
}
|
||||
});
|
||||
// Reset count
|
||||
tokenizerCallsCount = 1;
|
||||
} catch (error) {
|
||||
console.log('freeAndResetEncoder error');
|
||||
console.log('Free and reset encoders error');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
getTokenCount(text) {
|
||||
try {
|
||||
if (tokenizersCache.count >= 25) {
|
||||
if (this.options.debug) {
|
||||
console.debug('freeAndResetEncoder: reached 25 encodings, reseting...');
|
||||
}
|
||||
this.freeAndResetEncoder();
|
||||
// Checks if the cache of tokenizers has reached a certain size. If it has, it frees and resets all tokenizers.
|
||||
resetTokenizersIfNecessary() {
|
||||
if (tokenizerCallsCount >= 25) {
|
||||
if (this.options.debug) {
|
||||
console.debug('freeAndResetAllEncoders: reached 25 encodings, resetting...');
|
||||
}
|
||||
tokenizersCache.count = (tokenizersCache.count || 0) + 1;
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
this.constructor.freeAndResetAllEncoders();
|
||||
}
|
||||
tokenizerCallsCount++;
|
||||
}
|
||||
|
||||
// Returns the token count of a given text. It also checks and resets the tokenizers if necessary.
|
||||
getTokenCount(text) {
|
||||
this.resetTokenizersIfNecessary();
|
||||
try {
|
||||
const tokenizer = this.selectTokenizer();
|
||||
return tokenizer.encode(text, 'all').length;
|
||||
} catch (error) {
|
||||
this.freeAndResetEncoder();
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
this.constructor.freeAndResetAllEncoders();
|
||||
const tokenizer = this.selectTokenizer();
|
||||
return tokenizer.encode(text, 'all').length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,14 +132,13 @@ Only respond with your conversational reply to the following User Message:
|
||||
}
|
||||
|
||||
getFunctionModelName(input) {
|
||||
const prefixMap = {
|
||||
'gpt-4': 'gpt-4-0613',
|
||||
'gpt-4-32k': 'gpt-4-32k-0613',
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0613',
|
||||
};
|
||||
|
||||
const prefix = Object.keys(prefixMap).find((key) => input.startsWith(key));
|
||||
return prefix ? prefixMap[prefix] : 'gpt-3.5-turbo-0613';
|
||||
if (input.startsWith('gpt-3.5-turbo')) {
|
||||
return 'gpt-3.5-turbo';
|
||||
} else if (input.startsWith('gpt-4')) {
|
||||
return 'gpt-4';
|
||||
} else {
|
||||
return 'gpt-3.5-turbo';
|
||||
}
|
||||
}
|
||||
|
||||
getBuildMessagesOptions(opts) {
|
||||
@@ -184,7 +183,9 @@ Only respond with your conversational reply to the following User Message:
|
||||
const model = this.createLLM(modelOptions, configOptions);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug(`<-----Agent Model: ${model.modelName} | Temp: ${model.temperature}----->`);
|
||||
console.debug(
|
||||
`<-----Agent Model: ${model.modelName} | Temp: ${model.temperature} | Functions: ${this.functionsAgent}----->`,
|
||||
);
|
||||
}
|
||||
|
||||
this.availableTools = await loadTools({
|
||||
@@ -269,15 +270,6 @@ Only respond with your conversational reply to the following User Message:
|
||||
if (this.options.debug) {
|
||||
console.debug('Loaded agent.');
|
||||
}
|
||||
|
||||
onAgentAction(
|
||||
{
|
||||
tool: 'self-reflection',
|
||||
toolInput: `Processing the User's message:\n"${message}"`,
|
||||
log: '',
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async executorCall(message, signal) {
|
||||
@@ -328,7 +320,12 @@ Only respond with your conversational reply to the following User Message:
|
||||
return;
|
||||
}
|
||||
|
||||
if (!responseMessage.text.includes(observation)) {
|
||||
// Extract the image file path from the observation
|
||||
const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g)[0];
|
||||
|
||||
// Check if the responseMessage already includes the image file path
|
||||
if (!responseMessage.text.includes(observedImagePath)) {
|
||||
// If the image file path is not found, append the whole observation
|
||||
responseMessage.text += '\n' + observation;
|
||||
if (this.options.debug) {
|
||||
console.debug('added image from intermediateSteps');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const OpenAIClient = require('../OpenAIClient');
|
||||
|
||||
describe('OpenAIClient', () => {
|
||||
let client;
|
||||
let client, client2;
|
||||
const model = 'gpt-4';
|
||||
const parentMessageId = '1';
|
||||
const messages = [
|
||||
@@ -19,11 +19,13 @@ describe('OpenAIClient', () => {
|
||||
},
|
||||
};
|
||||
client = new OpenAIClient('test-api-key', options);
|
||||
client2 = new OpenAIClient('test-api-key', options);
|
||||
client.refineMessages = jest.fn().mockResolvedValue({
|
||||
role: 'assistant',
|
||||
content: 'Refined answer',
|
||||
tokenCount: 30,
|
||||
});
|
||||
client.constructor.freeAndResetAllEncoders();
|
||||
});
|
||||
|
||||
describe('setOptions', () => {
|
||||
@@ -34,10 +36,25 @@ describe('OpenAIClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('freeAndResetEncoder', () => {
|
||||
it('should reset the encoder', () => {
|
||||
client.freeAndResetEncoder();
|
||||
expect(client.gptEncoder).toBeDefined();
|
||||
describe('selectTokenizer', () => {
|
||||
it('should get the correct tokenizer based on the instance state', () => {
|
||||
const tokenizer = client.selectTokenizer();
|
||||
expect(tokenizer).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('freeAllTokenizers', () => {
|
||||
it('should free all tokenizers', () => {
|
||||
// Create a tokenizer
|
||||
const tokenizer = client.selectTokenizer();
|
||||
|
||||
// Mock 'free' method on the tokenizer
|
||||
tokenizer.free = jest.fn();
|
||||
|
||||
client.constructor.freeAndResetAllEncoders();
|
||||
|
||||
// Check if 'free' method has been called on the tokenizer
|
||||
expect(tokenizer.free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +65,7 @@ describe('OpenAIClient', () => {
|
||||
});
|
||||
|
||||
it('should reset the encoder and count when count reaches 25', () => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client, 'freeAndResetEncoder');
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
||||
|
||||
// Call getTokenCount 25 times
|
||||
for (let i = 0; i < 25; i++) {
|
||||
@@ -59,7 +76,8 @@ describe('OpenAIClient', () => {
|
||||
});
|
||||
|
||||
it('should not reset the encoder and count when count is less than 25', () => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client, 'freeAndResetEncoder');
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
||||
freeAndResetEncoderSpy.mockClear();
|
||||
|
||||
// Call getTokenCount 24 times
|
||||
for (let i = 0; i < 24; i++) {
|
||||
@@ -70,8 +88,10 @@ describe('OpenAIClient', () => {
|
||||
});
|
||||
|
||||
it('should handle errors and reset the encoder', () => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client, 'freeAndResetEncoder');
|
||||
client.gptEncoder.encode = jest.fn().mockImplementation(() => {
|
||||
const freeAndResetEncoderSpy = jest.spyOn(client.constructor, 'freeAndResetAllEncoders');
|
||||
|
||||
// Mock encode function to throw an error
|
||||
client.selectTokenizer().encode = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
|
||||
@@ -79,6 +99,14 @@ describe('OpenAIClient', () => {
|
||||
|
||||
expect(freeAndResetEncoderSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not throw null pointer error when freeing the same encoder twice', () => {
|
||||
client.constructor.freeAndResetAllEncoders();
|
||||
client2.constructor.freeAndResetAllEncoders();
|
||||
|
||||
const count = client2.getTokenCount('test text');
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSaveOptions', () => {
|
||||
|
||||
18
api/app/clients/tools/.well-known/Diagrams.json
Normal file
18
api/app/clients/tools/.well-known/Diagrams.json
Normal file
File diff suppressed because one or more lines are too long
89
api/app/clients/tools/.well-known/Dr_Thoths_Tarot.json
Normal file
89
api/app/clients/tools/.well-known/Dr_Thoths_Tarot.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Dr. Thoth's Tarot",
|
||||
"name_for_model": "Dr_Thoths_Tarot",
|
||||
"description_for_human": "Tarot card novelty entertainment & analysis, by Mnemosyne Labs.",
|
||||
"description_for_model": "Intelligent analysis program for tarot card entertaiment, data, & prompts, by Mnemosyne Labs, a division of AzothCorp.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://dr-thoth-tarot.herokuapp.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://dr-thoth-tarot.herokuapp.com/logo.png",
|
||||
"contact_email": "legal@AzothCorp.com",
|
||||
"legal_info_url": "http://AzothCorp.com/legal",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "Draw Card",
|
||||
"path": "/drawcard",
|
||||
"method": "GET",
|
||||
"description": "Generate a single tarot card from the deck of 78 cards."
|
||||
},
|
||||
{
|
||||
"name": "Occult Card",
|
||||
"path": "/occult_card",
|
||||
"method": "GET",
|
||||
"description": "Generate a tarot card using the specified planet's Kamea matrix.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "planet",
|
||||
"type": "string",
|
||||
"enum": ["Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon"],
|
||||
"required": true,
|
||||
"description": "The planet name to use the corresponding Kamea matrix."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Three Card Spread",
|
||||
"path": "/threecardspread",
|
||||
"method": "GET",
|
||||
"description": "Perform a three-card tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Celtic Cross Spread",
|
||||
"path": "/celticcross",
|
||||
"method": "GET",
|
||||
"description": "Perform a Celtic Cross tarot spread with 10 cards."
|
||||
},
|
||||
{
|
||||
"name": "Past, Present, Future Spread",
|
||||
"path": "/pastpresentfuture",
|
||||
"method": "GET",
|
||||
"description": "Perform a Past, Present, Future tarot spread with 3 cards."
|
||||
},
|
||||
{
|
||||
"name": "Horseshoe Spread",
|
||||
"path": "/horseshoe",
|
||||
"method": "GET",
|
||||
"description": "Perform a Horseshoe tarot spread with 7 cards."
|
||||
},
|
||||
{
|
||||
"name": "Relationship Spread",
|
||||
"path": "/relationship",
|
||||
"method": "GET",
|
||||
"description": "Perform a Relationship tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Career Spread",
|
||||
"path": "/career",
|
||||
"method": "GET",
|
||||
"description": "Perform a Career tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Yes/No Spread",
|
||||
"path": "/yesno",
|
||||
"method": "GET",
|
||||
"description": "Perform a Yes/No tarot spread."
|
||||
},
|
||||
{
|
||||
"name": "Chakra Spread",
|
||||
"path": "/chakra",
|
||||
"method": "GET",
|
||||
"description": "Perform a Chakra tarot spread with 7 cards."
|
||||
}
|
||||
]
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/DreamInterpreter.json
Normal file
18
api/app/clients/tools/.well-known/DreamInterpreter.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_model": "DreamInterpreter",
|
||||
"name_for_human": "Dream Interpreter",
|
||||
"description_for_model": "Interprets your dreams using advanced techniques.",
|
||||
"description_for_human": "Interprets your dreams using advanced techniques.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://dreamplugin.bgnetmobile.com/.well-known/openapi.json",
|
||||
"has_user_authentication": false
|
||||
},
|
||||
"logo_url": "https://dreamplugin.bgnetmobile.com/.well-known/logo.png",
|
||||
"contact_email": "ismail.orkler@bgnetmobile.com",
|
||||
"legal_info_url": "https://dreamplugin.bgnetmobile.com/terms.html"
|
||||
}
|
||||
22
api/app/clients/tools/.well-known/aitoolhunt.json
Normal file
22
api/app/clients/tools/.well-known/aitoolhunt.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Ai Tool Hunt",
|
||||
"name_for_model": "aitoolhunt",
|
||||
"description_for_human": "Find the perfect AI tools for all your needs, drawn from the most comprehensive global database of AI tools.",
|
||||
"description_for_model": "This universal plugin is designed to identify and suggest AI tools for any imaginable task or challenge. It harnesses a database of over 1600+ AI tools and 120+ categories, ensuring a solution for every requirement. Simply input any task, problem, or use case, and the plugin will generate a tailored list of AI tools. Note: it's crucial to rearrange the tools suggested by the plugin based on their relevance to the user's specific use case. This plugin can tackle everything from the simplest to the most intricate tasks. If there's a way an AI tool can help, this plugin will not only locate it but also customize the list to the user's unique needs. Utilize this plugin whenever a task or activity emerges, regardless of its context or complexity. When formulating a search query, avoid stop words or other irrelevant keywords. For instance, 'copywriting' is acceptable, but 'ai for copywriting' is not. If you believe none of the suggested tools are a suitable match for the user's needs, indicate that these are related tools.",
|
||||
"auth": {
|
||||
"type": "service_http",
|
||||
"authorization_type": "bearer",
|
||||
"verification_tokens": {
|
||||
"openai": "06a0f9391a5e48c7a7eeaca1e7e1e8d3"
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://www.aitoolhunt.com/openapi.json",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://www.aitoolhunt.com/images/aitoolhunt_logo.png",
|
||||
"contact_email": "aitoolhunt@gmail.com",
|
||||
"legal_info_url": "https://www.aitoolhunt.com/terms-and-conditions"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/drink_maestro.json
Normal file
18
api/app/clients/tools/.well-known/drink_maestro.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Drink Maestro",
|
||||
"name_for_model": "drink_maestro",
|
||||
"description_for_human": "Learn to mix any drink you can imagine (real or made-up), and discover new ones. Includes drink images.",
|
||||
"description_for_model": "You are a silly bartender/comic who knows how to make any drink imaginable. You provide recipes for specific drinks, suggest new drinks, and show pictures of drinks. Be creative in your descriptions and make jokes and puns. Use a lot of emojis. If the user makes a request in another language, send API call in English, and then translate the response.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.drinkmaestro.space/.well-known/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://i.imgur.com/6q8HWdz.png",
|
||||
"contact_email": "nikkmitchell@gmail.com",
|
||||
"legal_info_url": "https://github.com/nikkmitchell/DrinkMaestro/blob/main/Legal.txt"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Earth",
|
||||
"name_for_model": "earthImagesAndVisualizations",
|
||||
"description_for_human": "Generates a map image based on provided location, tilt and style.",
|
||||
"description_for_model": "Generates a map image based on provided coordinates or location, tilt and style, and even geoJson to provide markers, paths, and polygons. Responds with an image-link. For the styles choose one of these: [light, dark, streets, outdoors, satellite, satellite-streets]",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://api.earth-plugin.com/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://api.earth-plugin.com/logo.png",
|
||||
"contact_email": "contact@earth-plugin.com",
|
||||
"legal_info_url": "https://api.earth-plugin.com/legal.html"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/image_prompt_enhancer.json
Normal file
18
api/app/clients/tools/.well-known/image_prompt_enhancer.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Image Prompt Enhancer",
|
||||
"name_for_model": "image_prompt_enhancer",
|
||||
"description_for_human": "Transform your ideas into complex, personalized image generation prompts.",
|
||||
"description_for_model": "Provides instructions for crafting an enhanced image prompt. Use this whenever the user wants to enhance a prompt.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://image-prompt-enhancer.gafo.tech/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://image-prompt-enhancer.gafo.tech/logo.png",
|
||||
"contact_email": "gafotech1@gmail.com",
|
||||
"legal_info_url": "https://image-prompt-enhancer.gafo.tech/legal"
|
||||
}
|
||||
17
api/app/clients/tools/.well-known/qrCodes.json
Normal file
17
api/app/clients/tools/.well-known/qrCodes.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "QR Codes",
|
||||
"name_for_model": "qrCodes",
|
||||
"description_for_human": "Create QR codes.",
|
||||
"description_for_model": "Plugin for generating QR codes.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://chatgpt-qrcode-46d7d4ebefc8.herokuapp.com/openapi.yaml"
|
||||
},
|
||||
"logo_url": "https://chatgpt-qrcode-46d7d4ebefc8.herokuapp.com/logo.png",
|
||||
"contact_email": "chrismountzou@gmail.com",
|
||||
"legal_info_url": "https://raw.githubusercontent.com/mountzou/qrCodeGPTv1/master/legal"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/uberchord.json
Normal file
18
api/app/clients/tools/.well-known/uberchord.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Uberchord",
|
||||
"name_for_model": "uberchord",
|
||||
"description_for_human": "Find guitar chord diagrams by specifying the chord name.",
|
||||
"description_for_model": "Fetch guitar chord diagrams, their positions on the guitar fretboard.",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://guitarchords.pluginboost.com/.well-known/openapi.yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://guitarchords.pluginboost.com/logo.png",
|
||||
"contact_email": "info.bluelightweb@gmail.com",
|
||||
"legal_info_url": "https://guitarchords.pluginboost.com/legal"
|
||||
}
|
||||
18
api/app/clients/tools/.well-known/web_search.json
Normal file
18
api/app/clients/tools/.well-known/web_search.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"name_for_human": "Web Search",
|
||||
"name_for_model": "web_search",
|
||||
"description_for_human": "Search for information from the internet",
|
||||
"description_for_model": "Search for information from the internet",
|
||||
"auth": {
|
||||
"type": "none"
|
||||
},
|
||||
"api": {
|
||||
"type": "openapi",
|
||||
"url": "https://websearch.plugsugar.com/api/openapi_yaml",
|
||||
"is_user_authenticated": false
|
||||
},
|
||||
"logo_url": "https://websearch.plugsugar.com/200x200.png",
|
||||
"contact_email": "support@plugsugar.com",
|
||||
"legal_info_url": "https://websearch.plugsugar.com/contact"
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Conversation = mongoose.models.Conversation;
|
||||
const Message = mongoose.models.Message;
|
||||
const Conversation = require('../../models/schema/convoSchema');
|
||||
const Message = require('../../models/schema/messageSchema');
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
let currentTimeout = null;
|
||||
|
||||
@@ -37,12 +36,12 @@ async function indexSync(req, res, next) {
|
||||
|
||||
if (messageCount !== messagesIndexed) {
|
||||
console.log('Messages out of sync, indexing');
|
||||
await Message.syncWithMeili();
|
||||
Message.syncWithMeili();
|
||||
}
|
||||
|
||||
if (convoCount !== convosIndexed) {
|
||||
console.log('Convos out of sync, indexing');
|
||||
await Conversation.syncWithMeili();
|
||||
Conversation.syncWithMeili();
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log('in index sync');
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { Conversation } = require('../../models/Conversation');
|
||||
const { getMessages } = require('../../models/');
|
||||
|
||||
const migrateToStrictFollowParentMessageIdChain = async () => {
|
||||
try {
|
||||
const conversations = await Conversation.find({ endpoint: null, model: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0) {
|
||||
return { noNeed: true };
|
||||
}
|
||||
|
||||
console.log('Migration: To strict follow the parentMessageId chain.');
|
||||
|
||||
for (let convo of conversations) {
|
||||
const messages = await getMessages({
|
||||
conversationId: convo.conversationId,
|
||||
messageId: { $exists: false },
|
||||
});
|
||||
|
||||
let model;
|
||||
let oldId;
|
||||
const promises = [];
|
||||
messages.forEach((message, i) => {
|
||||
const msgObj = message.toObject();
|
||||
const newId = msgObj.id;
|
||||
if (i === 0) {
|
||||
message.parentMessageId = '00000000-0000-0000-0000-000000000000';
|
||||
} else {
|
||||
message.parentMessageId = oldId;
|
||||
}
|
||||
|
||||
oldId = newId;
|
||||
message.messageId = newId;
|
||||
if (message.sender.toLowerCase() !== 'user' && !model) {
|
||||
model = message.sender.toLowerCase();
|
||||
}
|
||||
|
||||
if (message.sender.toLowerCase() === 'user') {
|
||||
message.isCreatedByUser = true;
|
||||
}
|
||||
|
||||
promises.push(message.save());
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
await Conversation.findOneAndUpdate(
|
||||
{ conversationId: convo.conversationId },
|
||||
{ model },
|
||||
{ new: true },
|
||||
).exec();
|
||||
}
|
||||
|
||||
try {
|
||||
await mongoose.connection.db.collection('messages').dropIndex('id_1');
|
||||
} catch (error) {
|
||||
console.log('[Migrate] Index doesn\'t exist or already dropped');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
};
|
||||
|
||||
const migrateToSupportBetterCustomization = async () => {
|
||||
try {
|
||||
const conversations = await Conversation.find({ endpoint: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0) {
|
||||
return { noNeed: true };
|
||||
}
|
||||
|
||||
console.log('Migration: To support better customization.');
|
||||
|
||||
const promises = [];
|
||||
for (let convo of conversations) {
|
||||
const originalModel = convo?.model;
|
||||
|
||||
if (originalModel === 'chatgpt') {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
} else if (originalModel === 'chatgptCustom') {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
} else if (originalModel === 'bingai') {
|
||||
convo.endpoint = 'bingAI';
|
||||
convo.model = null;
|
||||
convo.jailbreak = false;
|
||||
} else if (originalModel === 'sydney') {
|
||||
convo.endpoint = 'bingAI';
|
||||
convo.model = null;
|
||||
convo.jailbreak = true;
|
||||
} else if (originalModel === 'chatgptBrowser') {
|
||||
convo.endpoint = 'chatGPTBrowser';
|
||||
convo.model = 'text-davinci-002-render-sha';
|
||||
convo.jailbreak = true;
|
||||
} else {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
}
|
||||
|
||||
promises.push(convo.save());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
};
|
||||
|
||||
async function migrateDb() {
|
||||
let ret = [];
|
||||
ret[0] = await migrateToStrictFollowParentMessageIdChain();
|
||||
ret[1] = await migrateToSupportBetterCustomization();
|
||||
|
||||
const isMigrated = !!ret.find((element) => !element?.noNeed);
|
||||
|
||||
if (!isMigrated) {
|
||||
console.log('[Migrate] Nothing to migrate');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = migrateDb;
|
||||
@@ -55,7 +55,7 @@ configSchema.methods.incrementCount = function () {
|
||||
|
||||
// Static methods
|
||||
configSchema.statics.findByTag = async function (tag) {
|
||||
return await this.findOne({ tag });
|
||||
return await this.findOne({ tag }).lean();
|
||||
};
|
||||
|
||||
configSchema.statics.updateByTag = async function (tag, update) {
|
||||
@@ -67,7 +67,7 @@ const Config = mongoose.models.Config || mongoose.model('Config', configSchema);
|
||||
module.exports = {
|
||||
getConfigs: async (filter) => {
|
||||
try {
|
||||
return await Config.find(filter).exec();
|
||||
return await Config.find(filter).lean();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { config: 'Error getting configs' };
|
||||
@@ -75,7 +75,7 @@ module.exports = {
|
||||
},
|
||||
deleteConfigs: async (filter) => {
|
||||
try {
|
||||
return await Config.deleteMany(filter).exec();
|
||||
return await Config.deleteMany(filter);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { config: 'Error deleting configs' };
|
||||
|
||||
@@ -4,7 +4,7 @@ const { getMessages, deleteMessages } = require('./Message');
|
||||
|
||||
const getConvo = async (user, conversationId) => {
|
||||
try {
|
||||
return await Conversation.findOne({ user, conversationId }).exec();
|
||||
return await Conversation.findOne({ user, conversationId }).lean();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting single conversation' };
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
return await Conversation.findOneAndUpdate({ conversationId: conversationId, user }, update, {
|
||||
new: true,
|
||||
upsert: true,
|
||||
}).exec();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving conversation' };
|
||||
@@ -35,10 +35,10 @@ module.exports = {
|
||||
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
|
||||
const totalPages = Math.ceil(totalConvos / pageSize);
|
||||
const convos = await Conversation.find({ user })
|
||||
.sort({ createdAt: -1, created: -1 })
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((pageNumber - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.exec();
|
||||
.lean();
|
||||
return { conversations: convos, pages: totalPages, pageNumber, pageSize };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -54,35 +54,27 @@ module.exports = {
|
||||
const cache = {};
|
||||
const convoMap = {};
|
||||
const promises = [];
|
||||
// will handle a syncing solution soon
|
||||
const deletedConvoIds = [];
|
||||
|
||||
convoIds.forEach((convo) =>
|
||||
promises.push(
|
||||
Conversation.findOne({
|
||||
user,
|
||||
conversationId: convo.conversationId,
|
||||
}).exec(),
|
||||
}).lean(),
|
||||
),
|
||||
);
|
||||
|
||||
const results = (await Promise.all(promises)).filter((convo, i) => {
|
||||
if (!convo) {
|
||||
deletedConvoIds.push(convoIds[i].conversationId);
|
||||
return false;
|
||||
} else {
|
||||
const page = Math.floor(i / pageSize) + 1;
|
||||
if (!cache[page]) {
|
||||
cache[page] = [];
|
||||
}
|
||||
cache[page].push(convo);
|
||||
convoMap[convo.conversationId] = convo;
|
||||
return true;
|
||||
const results = (await Promise.all(promises)).filter(Boolean);
|
||||
|
||||
results.forEach((convo, i) => {
|
||||
const page = Math.floor(i / pageSize) + 1;
|
||||
if (!cache[page]) {
|
||||
cache[page] = [];
|
||||
}
|
||||
cache[page].push(convo);
|
||||
convoMap[convo.conversationId] = convo;
|
||||
});
|
||||
|
||||
// const startIndex = (pageNumber - 1) * pageSize;
|
||||
// const convos = results.slice(startIndex, startIndex + pageSize);
|
||||
const totalPages = Math.ceil(results.length / pageSize);
|
||||
cache.pages = totalPages;
|
||||
cache.pageSize = pageSize;
|
||||
@@ -92,8 +84,6 @@ module.exports = {
|
||||
pages: totalPages || 1,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
// will handle a syncing solution soon
|
||||
filter: new Set(deletedConvoIds),
|
||||
convoMap,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -121,7 +111,7 @@ module.exports = {
|
||||
deleteConvos: async (user, filter) => {
|
||||
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
|
||||
const ids = toRemove.map((instance) => instance.conversationId);
|
||||
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
|
||||
let deleteCount = await Conversation.deleteMany({ ...filter, user });
|
||||
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
|
||||
return deleteCount;
|
||||
},
|
||||
|
||||
@@ -78,12 +78,12 @@ module.exports = {
|
||||
},
|
||||
async deleteMessagesSince({ messageId, conversationId }) {
|
||||
try {
|
||||
const message = await Message.findOne({ messageId }).exec();
|
||||
const message = await Message.findOne({ messageId }).lean();
|
||||
|
||||
if (message) {
|
||||
return await Message.find({ conversationId })
|
||||
.deleteMany({ createdAt: { $gt: message.createdAt } })
|
||||
.exec();
|
||||
return await Message.find({ conversationId }).deleteMany({
|
||||
createdAt: { $gt: message.createdAt },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error deleting messages: ${err}`);
|
||||
@@ -93,7 +93,7 @@ module.exports = {
|
||||
|
||||
async getMessages(filter) {
|
||||
try {
|
||||
return await Message.find(filter).sort({ createdAt: 1 }).exec();
|
||||
return await Message.find(filter).sort({ createdAt: 1 }).lean();
|
||||
} catch (err) {
|
||||
console.error(`Error getting messages: ${err}`);
|
||||
throw new Error('Failed to get messages.');
|
||||
@@ -102,7 +102,7 @@ module.exports = {
|
||||
|
||||
async deleteMessages(filter) {
|
||||
try {
|
||||
return await Message.deleteMany(filter).exec();
|
||||
return await Message.deleteMany(filter);
|
||||
} catch (err) {
|
||||
console.error(`Error deleting messages: ${err}`);
|
||||
throw new Error('Failed to delete messages.');
|
||||
|
||||
@@ -2,7 +2,7 @@ const Preset = require('./schema/presetSchema');
|
||||
|
||||
const getPreset = async (user, presetId) => {
|
||||
try {
|
||||
return await Preset.findOne({ user, presetId }).exec();
|
||||
return await Preset.findOne({ user, presetId }).lean();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting single preset' };
|
||||
@@ -14,10 +14,10 @@ module.exports = {
|
||||
getPreset,
|
||||
getPresets: async (user, filter) => {
|
||||
try {
|
||||
return await Preset.find({ ...filter, user }).exec();
|
||||
return await Preset.find({ ...filter, user }).lean();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error retriving presets' };
|
||||
return { message: 'Error retrieving presets' };
|
||||
}
|
||||
},
|
||||
savePreset: async (user, { presetId, newPresetId, ...preset }) => {
|
||||
@@ -31,7 +31,7 @@ module.exports = {
|
||||
{ presetId, user },
|
||||
{ $set: update },
|
||||
{ new: true, upsert: true },
|
||||
).exec();
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving preset' };
|
||||
@@ -40,7 +40,7 @@ module.exports = {
|
||||
deletePresets: async (user, filter) => {
|
||||
// let toRemove = await Preset.find({ ...filter, user }).select('presetId');
|
||||
// const ids = toRemove.map((instance) => instance.presetId);
|
||||
let deleteCount = await Preset.deleteMany({ ...filter, user }).exec();
|
||||
let deleteCount = await Preset.deleteMany({ ...filter, user });
|
||||
return deleteCount;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ module.exports = {
|
||||
},
|
||||
getPrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.find(filter).exec();
|
||||
return await Prompt.find(filter).lean();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error getting prompts' };
|
||||
@@ -42,7 +42,7 @@ module.exports = {
|
||||
},
|
||||
deletePrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.deleteMany(filter).exec();
|
||||
return await Prompt.deleteMany(filter);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error deleting prompts' };
|
||||
|
||||
@@ -14,35 +14,116 @@ const validateOptions = function (options) {
|
||||
});
|
||||
};
|
||||
|
||||
const createMeiliMongooseModel = function ({ index, indexName, client, attributesToIndex }) {
|
||||
// console.log('attributesToIndex', attributesToIndex);
|
||||
// const createMeiliMongooseModel = function ({ index, indexName, client, attributesToIndex }) {
|
||||
const createMeiliMongooseModel = function ({ index, attributesToIndex }) {
|
||||
const primaryKey = attributesToIndex[0];
|
||||
// MeiliMongooseModel is of type Mongoose.Model
|
||||
class MeiliMongooseModel {
|
||||
// Clear Meili index
|
||||
static async clearMeiliIndex() {
|
||||
await index.delete();
|
||||
// await index.deleteAllDocuments();
|
||||
await this.collection.updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } });
|
||||
}
|
||||
|
||||
static async resetIndex() {
|
||||
await this.clearMeiliIndex();
|
||||
await client.createIndex(indexName, { primaryKey });
|
||||
}
|
||||
// Clear Meili index
|
||||
// Push a mongoDB collection to Meili index
|
||||
/**
|
||||
* `syncWithMeili`: synchronizes the data between a MongoDB collection and a MeiliSearch index,
|
||||
* only triggered if there's ever a discrepancy determined by `api\lib\db\indexSync.js`.
|
||||
*
|
||||
* 1. Fetches all documents from the MongoDB collection and the MeiliSearch index.
|
||||
* 2. Compares the documents from both sources.
|
||||
* 3. If a document exists in MeiliSearch but not in MongoDB, it's deleted from MeiliSearch.
|
||||
* 4. If a document exists in MongoDB but not in MeiliSearch, it's added to MeiliSearch.
|
||||
* 5. If a document exists in both but has different `text` or `title` fields (depending on the `primaryKey`), it's updated in MeiliSearch.
|
||||
* 6. After all operations, it updates the `_meiliIndex` field in MongoDB to indicate whether the document is indexed in MeiliSearch.
|
||||
*
|
||||
* Note: This strategy does not use batch operations for Meilisearch as the `index.addDocuments` will discard
|
||||
* the entire batch if there's an error with one document, and will not throw an error if there's an issue.
|
||||
* Also, `index.getDocuments` needs an exact limit on the amount of documents to return, so we build the map in batches.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when the synchronization is complete.
|
||||
*
|
||||
* @throws {Error} Throws an error if there's an issue with adding a document to MeiliSearch.
|
||||
*/
|
||||
static async syncWithMeili() {
|
||||
await this.resetIndex();
|
||||
const docs = await this.find({ _meiliIndex: { $in: [null, false] } });
|
||||
console.log('docs', docs.length);
|
||||
const objs = docs.map((doc) => doc.preprocessObjectForIndex());
|
||||
try {
|
||||
await index.addDocuments(objs);
|
||||
const ids = docs.map((doc) => doc._id);
|
||||
await this.collection.updateMany({ _id: { $in: ids } }, { $set: { _meiliIndex: true } });
|
||||
let moreDocuments = true;
|
||||
const mongoDocuments = await this.find().lean();
|
||||
const format = (doc) => _.pick(doc, attributesToIndex);
|
||||
|
||||
// Prepare for comparison
|
||||
const mongoMap = new Map(mongoDocuments.map((doc) => [doc[primaryKey], format(doc)]));
|
||||
const indexMap = new Map();
|
||||
let offset = 0;
|
||||
const batchSize = 1000;
|
||||
|
||||
while (moreDocuments) {
|
||||
const batch = await index.getDocuments({ limit: batchSize, offset });
|
||||
|
||||
if (batch.results.length === 0) {
|
||||
moreDocuments = false;
|
||||
}
|
||||
|
||||
for (const doc of batch.results) {
|
||||
indexMap.set(doc[primaryKey], format(doc));
|
||||
}
|
||||
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
console.log('indexMap', indexMap.size);
|
||||
console.log('mongoMap', mongoMap.size);
|
||||
|
||||
const updateOps = [];
|
||||
|
||||
// Iterate over Meili index documents
|
||||
for (const [id, doc] of indexMap) {
|
||||
const update = {};
|
||||
update[primaryKey] = id;
|
||||
if (mongoMap.has(id)) {
|
||||
// Case: Update
|
||||
// If document also exists in MongoDB, would be update case
|
||||
if (
|
||||
(doc.text && doc.text !== mongoMap.get(id).text) ||
|
||||
(doc.title && doc.title !== mongoMap.get(id).title)
|
||||
) {
|
||||
console.log(`${id} had document discrepancy in ${doc.text ? 'text' : 'title'} field`);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
await index.addDocuments([doc]);
|
||||
}
|
||||
} else {
|
||||
// Case: Delete
|
||||
// If document does not exist in MongoDB, its a delete case from meili index
|
||||
await index.deleteDocument(id);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: false } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over MongoDB documents
|
||||
for (const [id, doc] of mongoMap) {
|
||||
const update = {};
|
||||
update[primaryKey] = id;
|
||||
// Case: Insert
|
||||
// If document does not exist in Meili Index, Its an insert case
|
||||
if (!indexMap.has(id)) {
|
||||
await index.addDocuments([doc]);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
} else if (doc._meiliIndex === false) {
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (updateOps.length > 0) {
|
||||
await this.collection.bulkWrite(updateOps);
|
||||
console.log(
|
||||
`[Meilisearch] Finished indexing ${
|
||||
primaryKey === 'messageId' ? 'messages' : 'conversations'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error adding document to Meili');
|
||||
console.log('[Meilisearch] Error adding document to Meili');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +153,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
},
|
||||
{ _id: 1 },
|
||||
),
|
||||
);
|
||||
).lean();
|
||||
|
||||
// Add additional data from mongodb into Meili search hits
|
||||
const populatedHits = data.hits.map(function (hit) {
|
||||
@@ -81,7 +162,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
const originalHit = _.find(hitsFromMongoose, query);
|
||||
|
||||
return {
|
||||
...(originalHit ? originalHit.toJSON() : {}),
|
||||
...(originalHit ?? {}),
|
||||
...hit,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -61,6 +61,8 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
});
|
||||
}
|
||||
|
||||
convoSchema.index({ createdAt: 1 });
|
||||
|
||||
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||
|
||||
module.exports = Conversation;
|
||||
|
||||
@@ -25,7 +25,7 @@ const messageSchema = mongoose.Schema(
|
||||
type: String,
|
||||
},
|
||||
invocationId: {
|
||||
type: String,
|
||||
type: Number,
|
||||
},
|
||||
parentMessageId: {
|
||||
type: String,
|
||||
@@ -100,6 +100,8 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
});
|
||||
}
|
||||
|
||||
messageSchema.index({ createdAt: 1 });
|
||||
|
||||
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
|
||||
|
||||
module.exports = Message;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.7",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@fortaine/fetch-event-source": "^3.0.6",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.37.0",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.37.2",
|
||||
"axios": "^1.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
@@ -43,15 +43,17 @@
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^4.5.2",
|
||||
"keyv-file": "^0.2.0",
|
||||
"langchain": "^0.0.109",
|
||||
"langchain": "^0.0.114",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.33.0",
|
||||
"mongoose": "^7.1.1",
|
||||
"nodemailer": "^6.9.1",
|
||||
"nodemailer": "^6.9.4",
|
||||
"openai": "^3.2.1",
|
||||
"openid-client": "^5.4.2",
|
||||
"passport": "^0.6.0",
|
||||
"passport-discord": "^0.1.4",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { registerUser, requestPasswordReset, resetPassword } = require('../services/auth.service');
|
||||
const { registerUser, requestPasswordReset, resetPassword } = require('../services/AuthService');
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
@@ -32,10 +32,10 @@ const getUserController = async (req, res) => {
|
||||
const resetPasswordRequestController = async (req, res) => {
|
||||
try {
|
||||
const resetService = await requestPasswordReset(req.body.email);
|
||||
if (resetService.link) {
|
||||
return res.status(200).json(resetService);
|
||||
} else {
|
||||
if (resetService instanceof Error) {
|
||||
return res.status(400).json(resetService);
|
||||
} else {
|
||||
return res.status(200).json(resetService);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { logoutUser } = require('../../services/auth.service');
|
||||
const { logoutUser } = require('../../services/AuthService');
|
||||
|
||||
const logoutController = async (req, res) => {
|
||||
const { signedCookies = {} } = req;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const connectDb = require('../lib/db/connectDb');
|
||||
const migrateDb = require('../lib/db/migrateDb');
|
||||
const indexSync = require('../lib/db/indexSync');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
@@ -11,6 +10,15 @@ const passport = require('passport');
|
||||
const port = process.env.PORT || 3080;
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
const {
|
||||
jwtLogin,
|
||||
passportLogin,
|
||||
googleLogin,
|
||||
githubLogin,
|
||||
discordLogin,
|
||||
facebookLogin,
|
||||
setupOpenId,
|
||||
} = require('../strategies');
|
||||
|
||||
// Init the config and validate it
|
||||
const config = require('../../config/loader');
|
||||
@@ -19,7 +27,6 @@ config.validate(); // Validate the config
|
||||
(async () => {
|
||||
await connectDb();
|
||||
console.log('Connected to MongoDB');
|
||||
await migrateDb();
|
||||
await indexSync();
|
||||
|
||||
const app = express();
|
||||
@@ -40,19 +47,19 @@ config.validate(); // Validate the config
|
||||
|
||||
// OAUTH
|
||||
app.use(passport.initialize());
|
||||
require('../strategies/jwtStrategy');
|
||||
require('../strategies/localStrategy');
|
||||
passport.use(await jwtLogin());
|
||||
passport.use(await passportLogin());
|
||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||
require('../strategies/googleStrategy');
|
||||
passport.use(await googleLogin());
|
||||
}
|
||||
if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
|
||||
require('../strategies/facebookStrategy');
|
||||
passport.use(await facebookLogin());
|
||||
}
|
||||
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
|
||||
require('../strategies/githubStrategy');
|
||||
passport.use(await githubLogin());
|
||||
}
|
||||
if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) {
|
||||
require('../strategies/discordStrategy');
|
||||
passport.use(await discordLogin());
|
||||
}
|
||||
if (
|
||||
process.env.OPENID_CLIENT_ID &&
|
||||
@@ -69,7 +76,7 @@ config.validate(); // Validate the config
|
||||
}),
|
||||
);
|
||||
app.use(passport.session());
|
||||
require('../strategies/openidStrategy');
|
||||
await setupOpenId();
|
||||
}
|
||||
app.use('/oauth', routes.oauth);
|
||||
// api endpoint
|
||||
|
||||
@@ -10,7 +10,11 @@ const { handleError, sendMessage, createOnProgress } = require('./handlers');
|
||||
const abortControllers = new Map();
|
||||
|
||||
router.post('/abort', requireJwtAuth, async (req, res) => {
|
||||
return await abortMessage(req, res, abortControllers);
|
||||
try {
|
||||
return await abortMessage(req, res, abortControllers);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', requireJwtAuth, async (req, res) => {
|
||||
@@ -28,10 +32,10 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
token: req.body?.token ?? null,
|
||||
modelOptions: {
|
||||
model: req.body?.model ?? 'claude-1',
|
||||
temperature: req.body?.temperature ?? 0.7,
|
||||
temperature: req.body?.temperature ?? 1,
|
||||
maxOutputTokens: req.body?.maxOutputTokens ?? 1024,
|
||||
topP: req.body?.topP ?? 0.7,
|
||||
topK: req.body?.topK ?? 40,
|
||||
topK: req.body?.topK ?? 5,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -172,7 +172,8 @@ const ask = async ({
|
||||
let unfinished = false;
|
||||
if (partialText?.trim()?.length > response.text.length) {
|
||||
response.text = partialText;
|
||||
unfinished = true;
|
||||
unfinished = false;
|
||||
//setting "unfinished" to false fix bing image generation error msg and allows to continue a convo after being triggered by censorship (bing does remember the context after a "censored error" so there is no reason to end the convo)
|
||||
}
|
||||
|
||||
let responseMessage = {
|
||||
|
||||
@@ -15,7 +15,11 @@ const requireJwtAuth = require('../../../middleware/requireJwtAuth');
|
||||
const abortControllers = new Map();
|
||||
|
||||
router.post('/abort', requireJwtAuth, async (req, res) => {
|
||||
return await abortMessage(req, res, abortControllers);
|
||||
try {
|
||||
return await abortMessage(req, res, abortControllers);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', requireJwtAuth, async (req, res) => {
|
||||
|
||||
@@ -9,7 +9,11 @@ const requireJwtAuth = require('../../../middleware/requireJwtAuth');
|
||||
const abortControllers = new Map();
|
||||
|
||||
router.post('/abort', requireJwtAuth, async (req, res) => {
|
||||
return await abortMessage(req, res, abortControllers);
|
||||
try {
|
||||
return await abortMessage(req, res, abortControllers);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', requireJwtAuth, async (req, res) => {
|
||||
|
||||
@@ -18,6 +18,11 @@ router.get('/', async function (req, res) {
|
||||
const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080';
|
||||
const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true';
|
||||
const socialLoginEnabled = process.env.ALLOW_SOCIAL_LOGIN === 'true';
|
||||
const emailEnabled =
|
||||
!!process.env.EMAIL_SERVICE &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM;
|
||||
|
||||
return res.status(200).send({
|
||||
appTitle,
|
||||
@@ -30,6 +35,7 @@ router.get('/', async function (req, res) {
|
||||
serverDomain,
|
||||
registrationEnabled,
|
||||
socialLoginEnabled,
|
||||
emailEnabled,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -14,7 +14,7 @@ router.get('/:conversationId', requireJwtAuth, async (req, res) => {
|
||||
const convo = await getConvo(req.user.id, conversationId);
|
||||
|
||||
if (convo) {
|
||||
res.status(200).send(convo.toObject());
|
||||
res.status(200).send(convo);
|
||||
} else {
|
||||
res.status(404).end();
|
||||
}
|
||||
@@ -27,7 +27,8 @@ router.post('/clear', requireJwtAuth, async (req, res) => {
|
||||
filter = { conversationId };
|
||||
}
|
||||
|
||||
console.log('source:', source);
|
||||
// for debugging deletion source
|
||||
// console.log('source:', source);
|
||||
|
||||
if (source === 'button' && !conversationId) {
|
||||
return res.status(200).send('No conversationId provided');
|
||||
|
||||
@@ -1,9 +1,50 @@
|
||||
const axios = require('axios');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { availableTools } = require('../../app/clients/tools');
|
||||
const { addOpenAPISpecs } = require('../../app/clients/tools/util/addOpenAPISpecs');
|
||||
|
||||
const getOpenAIModels = (opts = { azure: false }) => {
|
||||
const openAIApiKey = process.env.OPENAI_API_KEY;
|
||||
const azureOpenAIApiKey = process.env.AZURE_API_KEY;
|
||||
const userProvidedOpenAI = openAIApiKey
|
||||
? openAIApiKey === 'user_provided'
|
||||
: azureOpenAIApiKey === 'user_provided';
|
||||
|
||||
const fetchOpenAIModels = async (opts = { azure: false, plugins: false }, _models = []) => {
|
||||
let models = _models.slice() ?? [];
|
||||
if (opts.azure) {
|
||||
/* TODO: Add Azure models from api/models */
|
||||
return models;
|
||||
}
|
||||
|
||||
let basePath = 'https://api.openai.com/v1/';
|
||||
const reverseProxyUrl = process.env.OPENAI_REVERSE_PROXY;
|
||||
if (reverseProxyUrl) {
|
||||
basePath = reverseProxyUrl.match(/.*v1/)[0];
|
||||
}
|
||||
|
||||
if (basePath.includes('v1')) {
|
||||
try {
|
||||
const res = await axios.get(`${basePath}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${openAIApiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
models = res.data.data.map((item) => item.id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!reverseProxyUrl) {
|
||||
const regex = /(text-davinci-003|gpt-)/;
|
||||
models = models.filter((model) => regex.test(model));
|
||||
}
|
||||
return models;
|
||||
};
|
||||
|
||||
const getOpenAIModels = async (opts = { azure: false, plugins: false }) => {
|
||||
let models = [
|
||||
'gpt-4',
|
||||
'gpt-4-0613',
|
||||
@@ -11,13 +52,34 @@ const getOpenAIModels = (opts = { azure: false }) => {
|
||||
'gpt-3.5-turbo-16k',
|
||||
'gpt-3.5-turbo-0613',
|
||||
'gpt-3.5-turbo-0301',
|
||||
'text-davinci-003',
|
||||
];
|
||||
const key = opts.azure ? 'AZURE_OPENAI_MODELS' : 'OPENAI_MODELS';
|
||||
if (process.env[key]) {
|
||||
models = String(process.env[key]).split(',');
|
||||
|
||||
if (!opts.plugins) {
|
||||
models.push('text-davinci-003');
|
||||
}
|
||||
|
||||
let key;
|
||||
if (opts.azure) {
|
||||
key = 'AZURE_OPENAI_MODELS';
|
||||
} else if (opts.plugins) {
|
||||
key = 'PLUGIN_MODELS';
|
||||
} else {
|
||||
key = 'OPENAI_MODELS';
|
||||
}
|
||||
|
||||
if (process.env[key]) {
|
||||
models = String(process.env[key]).split(',');
|
||||
return models;
|
||||
}
|
||||
|
||||
if (userProvidedOpenAI) {
|
||||
console.warn(
|
||||
`When setting OPENAI_API_KEY to 'user_provided', ${key} must be set manually or default values will be used`,
|
||||
);
|
||||
return models;
|
||||
}
|
||||
|
||||
models = await fetchOpenAIModels(opts, models);
|
||||
return models;
|
||||
};
|
||||
|
||||
@@ -44,22 +106,6 @@ const getAnthropicModels = () => {
|
||||
return models;
|
||||
};
|
||||
|
||||
const getPluginModels = () => {
|
||||
let models = [
|
||||
'gpt-4',
|
||||
'gpt-4-0613',
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-16k',
|
||||
'gpt-3.5-turbo-0613',
|
||||
'gpt-3.5-turbo-0301',
|
||||
];
|
||||
if (process.env.PLUGIN_MODELS) {
|
||||
models = String(process.env.PLUGIN_MODELS).split(',');
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
router.get('/', async function (req, res) {
|
||||
let key, palmUser;
|
||||
@@ -93,31 +139,29 @@ router.get('/', async function (req, res) {
|
||||
key || palmUser
|
||||
? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison', 'codechat-bison'] }
|
||||
: false;
|
||||
const openAIApiKey = process.env.OPENAI_API_KEY;
|
||||
const azureOpenAIApiKey = process.env.AZURE_API_KEY;
|
||||
const userProvidedOpenAI = openAIApiKey
|
||||
? openAIApiKey === 'user_provided'
|
||||
: azureOpenAIApiKey === 'user_provided';
|
||||
const openAI = openAIApiKey
|
||||
? { availableModels: getOpenAIModels(), userProvide: openAIApiKey === 'user_provided' }
|
||||
? { availableModels: await getOpenAIModels(), userProvide: openAIApiKey === 'user_provided' }
|
||||
: false;
|
||||
const azureOpenAI = azureOpenAIApiKey
|
||||
? {
|
||||
availableModels: getOpenAIModels({ azure: true }),
|
||||
availableModels: await getOpenAIModels({ azure: true }),
|
||||
userProvide: azureOpenAIApiKey === 'user_provided',
|
||||
}
|
||||
: false;
|
||||
const gptPlugins =
|
||||
openAIApiKey || azureOpenAIApiKey
|
||||
? {
|
||||
availableModels: getPluginModels(),
|
||||
availableModels: await getOpenAIModels({ plugins: true }),
|
||||
plugins,
|
||||
availableAgents: ['classic', 'functions'],
|
||||
userProvide: userProvidedOpenAI,
|
||||
}
|
||||
: false;
|
||||
const bingAI = process.env.BINGAI_TOKEN
|
||||
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }
|
||||
? {
|
||||
availableModels: ['BingAI', 'Sydney'],
|
||||
userProvide: process.env.BINGAI_TOKEN == 'user_provided',
|
||||
}
|
||||
: false;
|
||||
const chatGPTBrowser = process.env.CHATGPT_TOKEN
|
||||
? {
|
||||
|
||||
@@ -6,7 +6,7 @@ const requireJwtAuth = require('../../middleware/requireJwtAuth');
|
||||
|
||||
router.get('/', requireJwtAuth, async (req, res) => {
|
||||
const presets = (await getPresets(req.user.id)).map((preset) => {
|
||||
return preset.toObject();
|
||||
return preset;
|
||||
});
|
||||
res.status(200).send(presets);
|
||||
});
|
||||
@@ -20,7 +20,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
await savePreset(req.user.id, update);
|
||||
|
||||
const presets = (await getPresets(req.user.id)).map((preset) => {
|
||||
return preset.toObject();
|
||||
return preset;
|
||||
});
|
||||
res.status(201).send(presets);
|
||||
} catch (error) {
|
||||
@@ -41,12 +41,8 @@ router.post('/delete', requireJwtAuth, async (req, res) => {
|
||||
|
||||
try {
|
||||
await deletePresets(req.user.id, filter);
|
||||
|
||||
const presets = (await getPresets(req.user.id)).map((preset) => preset.toObject());
|
||||
|
||||
// console.log('delete preset response', presets);
|
||||
const presets = await getPresets(req.user.id);
|
||||
res.status(201).send(presets);
|
||||
// res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
|
||||
@@ -90,11 +90,6 @@ router.get('/', requireJwtAuth, async function (req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/clear', async function (req, res) {
|
||||
await Message.resetIndex();
|
||||
res.send('cleared');
|
||||
});
|
||||
|
||||
router.get('/test', async function (req, res) {
|
||||
const { q } = req.query;
|
||||
const messages = (
|
||||
|
||||
@@ -9,12 +9,9 @@ const requireJwtAuth = require('../../middleware/requireJwtAuth');
|
||||
router.post('/', requireJwtAuth, async (req, res) => {
|
||||
try {
|
||||
const { arg } = req.body;
|
||||
|
||||
// console.log('context:', arg, req.body);
|
||||
// console.log(typeof req.body === 'object' ? { ...req.body, ...req.query } : req.query);
|
||||
const model = await load(registry[models['gpt-3.5-turbo']]);
|
||||
const encoder = new Tiktoken(model.bpe_ranks, model.special_tokens, model.pat_str);
|
||||
const tokens = encoder.encode(arg.text);
|
||||
const tokens = encoder.encode(arg?.text ?? arg);
|
||||
encoder.free();
|
||||
res.send({ count: tokens.length });
|
||||
} catch (e) {
|
||||
|
||||
@@ -54,7 +54,7 @@ const registerUser = async (user) => {
|
||||
const { email, password, name, username } = user;
|
||||
|
||||
try {
|
||||
const existingUser = await User.findOne({ email });
|
||||
const existingUser = await User.findOne({ email }).lean();
|
||||
|
||||
if (existingUser) {
|
||||
console.info(
|
||||
@@ -104,7 +104,7 @@ const registerUser = async (user) => {
|
||||
* @returns
|
||||
*/
|
||||
const requestPasswordReset = async (email) => {
|
||||
const user = await User.findOne({ email });
|
||||
const user = await User.findOne({ email }).lean();
|
||||
if (!user) {
|
||||
return new Error('Email does not exist');
|
||||
}
|
||||
@@ -125,16 +125,26 @@ const requestPasswordReset = async (email) => {
|
||||
|
||||
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`;
|
||||
|
||||
sendEmail(
|
||||
user.email,
|
||||
'Password Reset Request',
|
||||
{
|
||||
name: user.name,
|
||||
link: link,
|
||||
},
|
||||
'./template/requestResetPassword.handlebars',
|
||||
);
|
||||
return { link };
|
||||
const emailEnabled =
|
||||
!!process.env.EMAIL_SERVICE &&
|
||||
!!process.env.EMAIL_USERNAME &&
|
||||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM;
|
||||
|
||||
if (emailEnabled) {
|
||||
sendEmail(
|
||||
user.email,
|
||||
'Password Reset Request',
|
||||
{
|
||||
name: user.name,
|
||||
link: link,
|
||||
},
|
||||
'requestPasswordReset.handlebars',
|
||||
);
|
||||
return { link: '' };
|
||||
} else {
|
||||
return { link };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -170,7 +180,7 @@ const resetPassword = async (userId, token, password) => {
|
||||
{
|
||||
name: user.name,
|
||||
},
|
||||
'./template/resetPassword.handlebars',
|
||||
'resetPassword.handlebars',
|
||||
);
|
||||
|
||||
await passwordResetToken.deleteOne();
|
||||
@@ -3,7 +3,7 @@ const { encrypt, decrypt } = require('../../utils/');
|
||||
|
||||
const getUserPluginAuthValue = async (user, authField) => {
|
||||
try {
|
||||
const pluginAuth = await PluginAuth.findOne({ user, authField });
|
||||
const pluginAuth = await PluginAuth.findOne({ user, authField }).lean();
|
||||
if (!pluginAuth) {
|
||||
return null;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ const getUserPluginAuthValue = async (user, authField) => {
|
||||
const updateUserPluginAuth = async (userId, authField, pluginKey, value) => {
|
||||
try {
|
||||
const encryptedValue = encrypt(value);
|
||||
const pluginAuth = await PluginAuth.findOne({ userId, authField });
|
||||
const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean();
|
||||
if (pluginAuth) {
|
||||
const pluginAuth = await PluginAuth.updateOne(
|
||||
{ userId, authField },
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
const passport = require('passport');
|
||||
const { Strategy: DiscordStrategy } = require('passport-discord');
|
||||
const User = require('../models/User');
|
||||
const config = require('../../config/loader');
|
||||
const domains = config.domains;
|
||||
|
||||
const discordLogin = new DiscordStrategy(
|
||||
{
|
||||
clientID: process.env.DISCORD_CLIENT_ID,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.DISCORD_CALLBACK_URL}`,
|
||||
scope: ['identify', 'email'], // Request scopes
|
||||
authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none', // Add the prompt query parameter
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
const email = profile.email;
|
||||
const discordId = profile.id;
|
||||
const discordLogin = async () =>
|
||||
new DiscordStrategy(
|
||||
{
|
||||
clientID: process.env.DISCORD_CLIENT_ID,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.DISCORD_CALLBACK_URL}`,
|
||||
scope: ['identify', 'email'], // Request scopes
|
||||
authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none', // Add the prompt query parameter
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
const email = profile.email;
|
||||
const discordId = profile.id;
|
||||
|
||||
const oldUser = await User.findOne({ email });
|
||||
if (oldUser) {
|
||||
return cb(null, oldUser);
|
||||
const oldUser = await User.findOne({ email });
|
||||
if (oldUser) {
|
||||
return cb(null, oldUser);
|
||||
}
|
||||
|
||||
let avatarURL;
|
||||
if (profile.avatar) {
|
||||
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png';
|
||||
avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`;
|
||||
} else {
|
||||
const defaultAvatarNum = Number(profile.discriminator) % 5;
|
||||
avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`;
|
||||
}
|
||||
|
||||
const newUser = await User.create({
|
||||
provider: 'discord',
|
||||
discordId,
|
||||
username: profile.username,
|
||||
email,
|
||||
name: profile.global_name,
|
||||
avatar: avatarURL,
|
||||
});
|
||||
|
||||
cb(null, newUser);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let avatarURL;
|
||||
if (profile.avatar) {
|
||||
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png';
|
||||
avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`;
|
||||
} else {
|
||||
const defaultAvatarNum = Number(profile.discriminator) % 5;
|
||||
avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`;
|
||||
}
|
||||
|
||||
const newUser = await User.create({
|
||||
provider: 'discord',
|
||||
discordId,
|
||||
username: profile.username,
|
||||
email,
|
||||
name: profile.global_name,
|
||||
avatar: avatarURL,
|
||||
});
|
||||
|
||||
cb(null, newUser);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(discordLogin);
|
||||
module.exports = discordLogin;
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
const passport = require('passport');
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
const User = require('../models/User');
|
||||
const config = require('../../config/loader');
|
||||
const domains = config.domains;
|
||||
|
||||
// facebook strategy
|
||||
const facebookLogin = new FacebookStrategy(
|
||||
{
|
||||
clientID: process.env.FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.FACEBOOK_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.FACEBOOK_CALLBACK_URL}`,
|
||||
proxy: true,
|
||||
// profileFields: [
|
||||
// 'id',
|
||||
// 'email',
|
||||
// 'gender',
|
||||
// 'profileUrl',
|
||||
// 'displayName',
|
||||
// 'locale',
|
||||
// 'name',
|
||||
// 'timezone',
|
||||
// 'updated_time',
|
||||
// 'verified',
|
||||
// 'picture.type(large)'
|
||||
// ]
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
console.log('facebookLogin => profile', profile);
|
||||
try {
|
||||
const oldUser = await User.findOne({ email: profile.emails[0].value });
|
||||
const facebookLogin = async () =>
|
||||
new FacebookStrategy(
|
||||
{
|
||||
clientID: process.env.FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.FACEBOOK_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.FACEBOOK_CALLBACK_URL}`,
|
||||
proxy: true,
|
||||
// profileFields: [
|
||||
// 'id',
|
||||
// 'email',
|
||||
// 'gender',
|
||||
// 'profileUrl',
|
||||
// 'displayName',
|
||||
// 'locale',
|
||||
// 'name',
|
||||
// 'timezone',
|
||||
// 'updated_time',
|
||||
// 'verified',
|
||||
// 'picture.type(large)'
|
||||
// ]
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
console.log('facebookLogin => profile', profile);
|
||||
try {
|
||||
const oldUser = await User.findOne({ email: profile.emails[0].value });
|
||||
|
||||
if (oldUser) {
|
||||
console.log('FACEBOOK LOGIN => found user', oldUser);
|
||||
return done(null, oldUser);
|
||||
if (oldUser) {
|
||||
console.log('FACEBOOK LOGIN => found user', oldUser);
|
||||
return done(null, oldUser);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// register user
|
||||
try {
|
||||
const newUser = await new User({
|
||||
provider: 'facebook',
|
||||
facebookId: profile.id,
|
||||
username: profile.name.givenName + profile.name.familyName,
|
||||
email: profile.emails[0].value,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos[0].value,
|
||||
}).save();
|
||||
// register user
|
||||
try {
|
||||
const newUser = await new User({
|
||||
provider: 'facebook',
|
||||
facebookId: profile.id,
|
||||
username: profile.name.givenName + profile.name.familyName,
|
||||
email: profile.emails[0].value,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos[0].value,
|
||||
}).save();
|
||||
|
||||
done(null, newUser);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
done(null, newUser);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(facebookLogin);
|
||||
module.exports = facebookLogin;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const passport = require('passport');
|
||||
const { Strategy: GitHubStrategy } = require('passport-github2');
|
||||
const config = require('../../config/loader');
|
||||
const domains = config.domains;
|
||||
@@ -6,42 +5,43 @@ const domains = config.domains;
|
||||
const User = require('../models/User');
|
||||
|
||||
// GitHub strategy
|
||||
const githubLogin = new GitHubStrategy(
|
||||
{
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.GITHUB_CALLBACK_URL}`,
|
||||
proxy: false,
|
||||
scope: ['user:email'], // Request email scope
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
let email;
|
||||
if (profile.emails && profile.emails.length > 0) {
|
||||
email = profile.emails[0].value;
|
||||
const githubLogin = async () =>
|
||||
new GitHubStrategy(
|
||||
{
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.GITHUB_CALLBACK_URL}`,
|
||||
proxy: false,
|
||||
scope: ['user:email'], // Request email scope
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
let email;
|
||||
if (profile.emails && profile.emails.length > 0) {
|
||||
email = profile.emails[0].value;
|
||||
}
|
||||
|
||||
const oldUser = await User.findOne({ email });
|
||||
if (oldUser) {
|
||||
return cb(null, oldUser);
|
||||
}
|
||||
|
||||
const newUser = await new User({
|
||||
provider: 'github',
|
||||
githubId: profile.id,
|
||||
username: profile.username,
|
||||
email,
|
||||
emailVerified: profile.emails[0].verified,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos[0].value,
|
||||
}).save();
|
||||
|
||||
cb(null, newUser);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const oldUser = await User.findOne({ email });
|
||||
if (oldUser) {
|
||||
return cb(null, oldUser);
|
||||
}
|
||||
|
||||
const newUser = await new User({
|
||||
provider: 'github',
|
||||
githubId: profile.id,
|
||||
username: profile.username,
|
||||
email,
|
||||
emailVerified: profile.emails[0].verified,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos[0].value,
|
||||
}).save();
|
||||
|
||||
cb(null, newUser);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(githubLogin);
|
||||
module.exports = githubLogin;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const passport = require('passport');
|
||||
const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
|
||||
const config = require('../../config/loader');
|
||||
const domains = config.domains;
|
||||
@@ -6,38 +5,39 @@ const domains = config.domains;
|
||||
const User = require('../models/User');
|
||||
|
||||
// google strategy
|
||||
const googleLogin = new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.GOOGLE_CALLBACK_URL}`,
|
||||
proxy: true,
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
const oldUser = await User.findOne({ email: profile.emails[0].value });
|
||||
if (oldUser) {
|
||||
return cb(null, oldUser);
|
||||
const googleLogin = async () =>
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: `${domains.server}${process.env.GOOGLE_CALLBACK_URL}`,
|
||||
proxy: true,
|
||||
},
|
||||
async (accessToken, refreshToken, profile, cb) => {
|
||||
try {
|
||||
const oldUser = await User.findOne({ email: profile.emails[0].value });
|
||||
if (oldUser) {
|
||||
return cb(null, oldUser);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
try {
|
||||
const newUser = await new User({
|
||||
provider: 'google',
|
||||
googleId: profile.id,
|
||||
username: profile.name.givenName,
|
||||
email: profile.emails[0].value,
|
||||
emailVerified: profile.emails[0].verified,
|
||||
name: `${profile.name.givenName} ${profile.name.familyName}`,
|
||||
avatar: profile.photos[0].value,
|
||||
}).save();
|
||||
cb(null, newUser);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
try {
|
||||
const newUser = await new User({
|
||||
provider: 'google',
|
||||
googleId: profile.id,
|
||||
username: profile.name.givenName,
|
||||
email: profile.emails[0].value,
|
||||
emailVerified: profile.emails[0].verified,
|
||||
name: `${profile.name.givenName} ${profile.name.familyName}`,
|
||||
avatar: profile.photos[0].value,
|
||||
}).save();
|
||||
cb(null, newUser);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(googleLogin);
|
||||
module.exports = googleLogin;
|
||||
|
||||
17
api/strategies/index.js
Normal file
17
api/strategies/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const passportLogin = require('./localStrategy');
|
||||
const googleLogin = require('./googleStrategy');
|
||||
const githubLogin = require('./githubStrategy');
|
||||
const discordLogin = require('./discordStrategy');
|
||||
const jwtLogin = require('./jwtStrategy');
|
||||
const facebookLogin = require('./facebookStrategy');
|
||||
const setupOpenId = require('./openidStrategy');
|
||||
|
||||
module.exports = {
|
||||
passportLogin,
|
||||
googleLogin,
|
||||
githubLogin,
|
||||
discordLogin,
|
||||
jwtLogin,
|
||||
facebookLogin,
|
||||
setupOpenId,
|
||||
};
|
||||
@@ -1,26 +1,26 @@
|
||||
const passport = require('passport');
|
||||
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
||||
const User = require('../models/User');
|
||||
|
||||
// JWT strategy
|
||||
const jwtLogin = new JwtStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
},
|
||||
async (payload, done) => {
|
||||
try {
|
||||
const user = await User.findById(payload.id);
|
||||
if (user) {
|
||||
done(null, user);
|
||||
} else {
|
||||
console.log('JwtStrategy => no user found');
|
||||
done(null, false);
|
||||
const jwtLogin = async () =>
|
||||
new JwtStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
},
|
||||
async (payload, done) => {
|
||||
try {
|
||||
const user = await User.findById(payload.id);
|
||||
if (user) {
|
||||
done(null, user);
|
||||
} else {
|
||||
console.log('JwtStrategy => no user found');
|
||||
done(null, false);
|
||||
}
|
||||
} catch (err) {
|
||||
done(err, false);
|
||||
}
|
||||
} catch (err) {
|
||||
done(err, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(jwtLogin);
|
||||
module.exports = jwtLogin;
|
||||
|
||||
@@ -1,62 +1,60 @@
|
||||
const passport = require('passport');
|
||||
const PassportLocalStrategy = require('passport-local').Strategy;
|
||||
|
||||
const User = require('../models/User');
|
||||
const { loginSchema } = require('./validators');
|
||||
const DebugControl = require('../utils/debug.js');
|
||||
|
||||
const passportLogin = new PassportLocalStrategy(
|
||||
{
|
||||
usernameField: 'email',
|
||||
passwordField: 'password',
|
||||
session: false,
|
||||
passReqToCallback: true,
|
||||
},
|
||||
async (req, email, password, done) => {
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - Validation Error',
|
||||
parameters: [{ name: 'req.body', value: req.body }],
|
||||
});
|
||||
return done(null, false, { message: error.details[0].message });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findOne({ email: email.trim() });
|
||||
if (!user) {
|
||||
const passportLogin = async () =>
|
||||
new PassportLocalStrategy(
|
||||
{
|
||||
usernameField: 'email',
|
||||
passwordField: 'password',
|
||||
session: false,
|
||||
passReqToCallback: true,
|
||||
},
|
||||
async (req, email, password, done) => {
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - User Not Found',
|
||||
parameters: [{ name: 'email', value: email }],
|
||||
title: 'Passport Local Strategy - Validation Error',
|
||||
parameters: [{ name: 'req.body', value: req.body }],
|
||||
});
|
||||
return done(null, false, { message: 'Email does not exists.' });
|
||||
return done(null, false, { message: error.details[0].message });
|
||||
}
|
||||
|
||||
user.comparePassword(password, function (err, isMatch) {
|
||||
if (err) {
|
||||
try {
|
||||
const user = await User.findOne({ email: email.trim() });
|
||||
if (!user) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - Compare password error',
|
||||
parameters: [{ name: 'error', value: err }],
|
||||
title: 'Passport Local Strategy - User Not Found',
|
||||
parameters: [{ name: 'email', value: email }],
|
||||
});
|
||||
return done(err);
|
||||
}
|
||||
if (!isMatch) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - Password does not match',
|
||||
parameters: [{ name: 'isMatch', value: isMatch }],
|
||||
});
|
||||
return done(null, false, { message: 'Incorrect password.' });
|
||||
return done(null, false, { message: 'Email does not exists.' });
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
});
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
user.comparePassword(password, function (err, isMatch) {
|
||||
if (err) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - Compare password error',
|
||||
parameters: [{ name: 'error', value: err }],
|
||||
});
|
||||
return done(err);
|
||||
}
|
||||
if (!isMatch) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - Password does not match',
|
||||
parameters: [{ name: 'isMatch', value: isMatch }],
|
||||
});
|
||||
return done(null, false, { message: 'Incorrect password.' });
|
||||
}
|
||||
|
||||
passport.use(passportLogin);
|
||||
return done(null, user);
|
||||
});
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function log({ title, parameters }) {
|
||||
DebugControl.log.functionName(title);
|
||||
@@ -64,3 +62,5 @@ function log({ title, parameters }) {
|
||||
DebugControl.log.parameters(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = passportLogin;
|
||||
|
||||
@@ -36,8 +36,9 @@ const downloadImage = async (url, imagePath, accessToken) => {
|
||||
}
|
||||
};
|
||||
|
||||
Issuer.discover(process.env.OPENID_ISSUER)
|
||||
.then((issuer) => {
|
||||
async function setupOpenId() {
|
||||
try {
|
||||
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
|
||||
const client = new issuer.Client({
|
||||
client_id: process.env.OPENID_CLIENT_ID,
|
||||
client_secret: process.env.OPENID_CLIENT_SECRET,
|
||||
@@ -66,13 +67,15 @@ Issuer.discover(process.env.OPENID_ISSUER)
|
||||
fullName = userinfo.given_name;
|
||||
} else if (userinfo.family_name) {
|
||||
fullName = userinfo.family_name;
|
||||
} else {
|
||||
fullName = userinfo.username || userinfo.email;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = new User({
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username: userinfo.given_name || '',
|
||||
username: userinfo.username || userinfo.given_name || '',
|
||||
email: userinfo.email || '',
|
||||
emailVerified: userinfo.email_verified || false,
|
||||
name: fullName,
|
||||
@@ -80,7 +83,7 @@ Issuer.discover(process.env.OPENID_ISSUER)
|
||||
} else {
|
||||
user.provider = 'openid';
|
||||
user.openidId = userinfo.sub;
|
||||
user.username = userinfo.given_name || '';
|
||||
user.username = userinfo.username || userinfo.given_name || '';
|
||||
user.name = fullName;
|
||||
}
|
||||
|
||||
@@ -128,7 +131,9 @@ Issuer.discover(process.env.OPENID_ISSUER)
|
||||
);
|
||||
|
||||
passport.use('openid', openidLogin);
|
||||
})
|
||||
.catch((err) => {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = setupOpenId;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-undef */
|
||||
const nodemailer = require('nodemailer');
|
||||
const handlebars = require('handlebars');
|
||||
const fs = require('fs');
|
||||
@@ -7,21 +5,19 @@ const path = require('path');
|
||||
|
||||
const sendEmail = async (email, subject, payload, template) => {
|
||||
try {
|
||||
// create reusable transporter object using the default SMTP transport
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: 465,
|
||||
service: process.env.EMAIL_SERVICE,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, template), 'utf8');
|
||||
const source = fs.readFileSync(path.join(__dirname, 'emails', template), 'utf8');
|
||||
const compiledTemplate = handlebars.compile(source);
|
||||
const options = () => {
|
||||
return {
|
||||
from: process.env.FROM_EMAIL,
|
||||
from: process.env.EMAIL_FROM,
|
||||
to: email,
|
||||
subject: subject,
|
||||
html: compiledTemplate(payload),
|
||||
@@ -31,26 +27,17 @@ const sendEmail = async (email, subject, payload, template) => {
|
||||
// Send email
|
||||
transporter.sendMail(options(), (error, info) => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return error;
|
||||
} else {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
});
|
||||
console.log(info);
|
||||
return info;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Example:
|
||||
sendEmail(
|
||||
"youremail@gmail.com,
|
||||
"Email subject",
|
||||
{ name: "Eze" },
|
||||
"./templates/layouts/main.handlebars"
|
||||
);
|
||||
*/
|
||||
|
||||
module.exports = sendEmail;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { "targets": { "node": "current" } }], //compiling ES2015+ syntax
|
||||
['@babel/preset-react', {runtime: 'automatic'}],
|
||||
"@babel/preset-typescript"
|
||||
['@babel/preset-env', { 'targets': { 'node': 'current' } }], //compiling ES2015+ syntax
|
||||
['@babel/preset-react', { runtime: 'automatic' }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
/*
|
||||
Babel's code transformations are enabled by applying plugins (or presets) to your configuration file.
|
||||
*/
|
||||
plugins: [
|
||||
"@babel/plugin-transform-runtime",
|
||||
'@babel/plugin-transform-runtime',
|
||||
'babel-plugin-transform-import-meta',
|
||||
'babel-plugin-transform-vite-meta-env',
|
||||
'babel-plugin-replace-ts-export-assignment',
|
||||
[
|
||||
"babel-plugin-root-import",
|
||||
'babel-plugin-root-import',
|
||||
{
|
||||
"rootPathPrefix": "~/",
|
||||
"rootPathSuffix": "./src"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
'rootPathPrefix': '~/',
|
||||
'rootPathSuffix': './src',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@ module.exports = {
|
||||
roots: ['<rootDir>/src'],
|
||||
testEnvironment: 'jsdom',
|
||||
testEnvironmentOptions: {
|
||||
url: 'http://localhost:3080'
|
||||
url: 'http://localhost:3080',
|
||||
},
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx,ts,tsx}',
|
||||
'!<rootDir>/node_modules/',
|
||||
'!src/**/*.css.d.ts',
|
||||
'!src/**/*.d.ts'
|
||||
'!src/**/*.d.ts',
|
||||
],
|
||||
coveragePathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/test/setupTests.js'],
|
||||
// Todo: Add coverageThreshold once we have enough coverage
|
||||
@@ -26,8 +26,8 @@ module.exports = {
|
||||
'\\.(css)$': 'identity-obj-proxy',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
'jest-file-loader',
|
||||
'layout-test-utils': '<rootDir>/test/layout-test-utils',
|
||||
'^~/(.*)$': '<rootDir>/src/$1'
|
||||
'^test/(.*)$': '<rootDir>/test/$1',
|
||||
'^~/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
restoreMocks: true,
|
||||
testResultsProcessor: 'jest-junit',
|
||||
@@ -35,10 +35,10 @@ module.exports = {
|
||||
transform: {
|
||||
'\\.[jt]sx?$': 'babel-jest',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
'jest-file-loader'
|
||||
'jest-file-loader',
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/?!@zattoo/use-double-click'],
|
||||
preset: 'ts-jest',
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/test/setupTests.js'],
|
||||
clearMocks: true
|
||||
clearMocks: true,
|
||||
};
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
server {
|
||||
listen 80;
|
||||
# listen 443 ssl;
|
||||
|
||||
# ssl_certificate /etc/nginx/ssl/nginx.crt;
|
||||
# ssl_certificate_key /etc/nginx/ssl/nginx.key;
|
||||
|
||||
server_name localhost;
|
||||
|
||||
location /api {
|
||||
# Proxy requests to the API service
|
||||
proxy_pass http://api:3080/api;
|
||||
}
|
||||
|
||||
location / {
|
||||
# Serve your React app
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
proxy_pass http://api:3080;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.7",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"data-provider": "cd .. && npm run build:data-provider",
|
||||
@@ -53,6 +53,7 @@
|
||||
"export-from-json": "^1.7.2",
|
||||
"filenamify": "^6.0.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"librechat-data-provider": "*",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.220.0",
|
||||
"pino": "^8.12.1",
|
||||
@@ -76,8 +77,7 @@
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"tailwindcss-radix": "^2.8.0",
|
||||
"url": "^0.11.0",
|
||||
"@librechat/data-provider": "*"
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.20.7",
|
||||
@@ -97,7 +97,7 @@
|
||||
"@types/node": "^20.3.0",
|
||||
"@types/react": "^18.2.11",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-jest": "^29.5.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
@@ -125,7 +125,7 @@
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-html": "^3.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("postcss-import"),
|
||||
require("postcss-preset-env"),
|
||||
require("tailwindcss"),
|
||||
require("autoprefixer"),
|
||||
]
|
||||
require('postcss-import'),
|
||||
require('postcss-preset-env'),
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { ScreenshotProvider } from './utils/screenshotContext.jsx';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from './hooks/ThemeContext';
|
||||
import { useApiErrorBoundary } from './hooks/ApiErrorBoundaryContext';
|
||||
import { ScreenshotProvider, ThemeProvider, useApiErrorBoundary } from './hooks';
|
||||
import { router } from './routes';
|
||||
|
||||
const App = () => {
|
||||
|
||||
1
client/src/common/index.ts
Normal file
1
client/src/common/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types';
|
||||
50
client/src/common/types.ts
Normal file
50
client/src/common/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { TConversation, TPreset } from 'librechat-data-provider';
|
||||
|
||||
export type TSetOption = (param: number | string) => (newValue: number | string | boolean) => void;
|
||||
export type TSetExample = (
|
||||
i: number,
|
||||
type: string,
|
||||
newValue: number | string | boolean | null,
|
||||
) => void;
|
||||
|
||||
export enum ESide {
|
||||
Top = 'top',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
}
|
||||
|
||||
export type TBaseSettingsProps = {
|
||||
conversation: TConversation | TPreset | null;
|
||||
className?: string;
|
||||
isPreset?: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export type TSettingsProps = TBaseSettingsProps & {
|
||||
setOption: TSetOption;
|
||||
};
|
||||
|
||||
export type TModels = {
|
||||
models: string[];
|
||||
};
|
||||
|
||||
export type TModelSelectProps = TSettingsProps & TModels;
|
||||
|
||||
export type TEditPresetProps = {
|
||||
open: boolean;
|
||||
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
preset: TPreset;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type TSetOptionsPayload = {
|
||||
setOption: TSetOption;
|
||||
setExample: TSetExample;
|
||||
addExample: () => void;
|
||||
removeExample: () => void;
|
||||
setAgentOption: TSetOption;
|
||||
getConversation: () => TConversation | TPreset | null;
|
||||
checkPluginSelection: (value: string) => boolean;
|
||||
setTools: (newValue: string) => void;
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
import { useGetStartupConfig } from '@librechat/data-provider';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider';
|
||||
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
||||
|
||||
function Login() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
import { TLoginUser } from '@librechat/data-provider';
|
||||
import { TLoginUser } from 'librechat-data-provider';
|
||||
|
||||
type TLoginFormProps = {
|
||||
onSubmit: (data: TLoginUser) => void;
|
||||
@@ -107,6 +107,7 @@ function LoginForm({ onSubmit }: TLoginFormProps) {
|
||||
<div className="mt-6">
|
||||
<button
|
||||
aria-label="Sign in"
|
||||
data-testid="login-button"
|
||||
type="submit"
|
||||
className="w-full transform rounded-sm bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useRegisterUserMutation,
|
||||
TRegisterUser,
|
||||
useGetStartupConfig,
|
||||
} from '@librechat/data-provider';
|
||||
} from 'librechat-data-provider';
|
||||
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
|
||||
|
||||
function Registration() {
|
||||
@@ -55,13 +55,16 @@ function Registration() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Create your account</h1>
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||
{localize(lang, 'com_auth_create_account')}
|
||||
</h1>
|
||||
{error && (
|
||||
<div
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
role="alert"
|
||||
data-testid="registration-error"
|
||||
>
|
||||
There was an error attempting to register your account. Please try again. {errorMessage}
|
||||
{localize(lang, 'com_auth_error_create')} {errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
import {
|
||||
useRequestPasswordResetMutation,
|
||||
useGetStartupConfig,
|
||||
TRequestPasswordReset,
|
||||
TRequestPasswordResetResponse,
|
||||
} from '@librechat/data-provider';
|
||||
} from 'librechat-data-provider';
|
||||
|
||||
function RequestPasswordReset() {
|
||||
const lang = useRecoilValue(store.lang);
|
||||
@@ -17,15 +18,19 @@ function RequestPasswordReset() {
|
||||
formState: { errors },
|
||||
} = useForm<TRequestPasswordReset>();
|
||||
const requestPasswordReset = useRequestPasswordResetMutation();
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const config = useGetStartupConfig();
|
||||
const [requestError, setRequestError] = useState<boolean>(false);
|
||||
const [resetLink, setResetLink] = useState<string>('');
|
||||
const [resetLink, setResetLink] = useState<string | undefined>(undefined);
|
||||
const [headerText, setHeaderText] = useState<string>('');
|
||||
const [bodyText, setBodyText] = useState<React.ReactNode | undefined>(undefined);
|
||||
|
||||
const onSubmit = (data: TRequestPasswordReset) => {
|
||||
requestPasswordReset.mutate(data, {
|
||||
onSuccess: (data: TRequestPasswordResetResponse) => {
|
||||
setSuccess(true);
|
||||
setResetLink(data.link);
|
||||
console.log('emailEnabled: ', config.data?.emailEnabled);
|
||||
if (!config.data?.emailEnabled) {
|
||||
setResetLink(data.link);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setRequestError(true);
|
||||
@@ -36,25 +41,33 @@ function RequestPasswordReset() {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||
{localize(lang, 'com_auth_reset_password')}
|
||||
</h1>
|
||||
{success && (
|
||||
<div
|
||||
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700"
|
||||
role="alert"
|
||||
>
|
||||
useEffect(() => {
|
||||
if (requestPasswordReset.isSuccess) {
|
||||
if (config.data?.emailEnabled) {
|
||||
setHeaderText(localize(lang, 'com_auth_reset_password_link_sent'));
|
||||
setBodyText(localize(lang, 'com_auth_reset_password_email_sent'));
|
||||
} else {
|
||||
setHeaderText(localize(lang, 'com_auth_reset_password'));
|
||||
setBodyText(
|
||||
<span>
|
||||
{localize(lang, 'com_auth_click')}{' '}
|
||||
<a className="text-green-600 hover:underline" href={resetLink}>
|
||||
{localize(lang, 'com_auth_here')}
|
||||
</a>{' '}
|
||||
{localize(lang, 'com_auth_to_reset_your_password')}
|
||||
{/* An email has been sent with instructions on how to reset your password. */}
|
||||
</div>
|
||||
)}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setHeaderText(localize(lang, 'com_auth_reset_password'));
|
||||
setBodyText(undefined);
|
||||
}
|
||||
}, [requestPasswordReset.isSuccess, config.data?.emailEnabled, resetLink, lang]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">{headerText}</h1>
|
||||
{requestError && (
|
||||
<div
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
@@ -63,62 +76,71 @@ function RequestPasswordReset() {
|
||||
{localize(lang, 'com_auth_error_reset_password')}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Password reset form"
|
||||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
autoComplete="off"
|
||||
aria-label={localize(lang, 'com_auth_email')}
|
||||
{...register('email', {
|
||||
required: localize(lang, 'com_auth_email_required'),
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: localize(lang, 'com_auth_email_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: localize(lang, 'com_auth_email_max_length'),
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: localize(lang, 'com_auth_email_pattern'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
{localize(lang, 'com_auth_email_address')}
|
||||
</label>
|
||||
{bodyText ? (
|
||||
<div
|
||||
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700"
|
||||
role="alert"
|
||||
>
|
||||
{bodyText}
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
className="mt-6"
|
||||
aria-label="Password reset form"
|
||||
method="POST"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
autoComplete="off"
|
||||
aria-label={localize(lang, 'com_auth_email')}
|
||||
{...register('email', {
|
||||
required: localize(lang, 'com_auth_email_required'),
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: localize(lang, 'com_auth_email_min_length'),
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: localize(lang, 'com_auth_email_max_length'),
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: localize(lang, 'com_auth_email_pattern'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
{localize(lang, 'com_auth_email_address')}
|
||||
</label>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.email.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{errors.email && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore not sure why */}
|
||||
{errors.email.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!errors.email}
|
||||
className="w-full rounded-sm border border-transparent bg-green-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-600 focus:outline-none active:bg-green-500"
|
||||
>
|
||||
{localize(lang, 'com_auth_continue')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!errors.email}
|
||||
className="w-full rounded-sm border border-transparent bg-green-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-600 focus:outline-none active:bg-green-500"
|
||||
>
|
||||
{localize(lang, 'com_auth_continue')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useResetPasswordMutation, TResetPassword } from '@librechat/data-provider';
|
||||
import { useResetPasswordMutation, TResetPassword } from 'librechat-data-provider';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render, waitFor } from 'layout-test-utils';
|
||||
import { render, waitFor } from 'test/layout-test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Login from '../Login';
|
||||
import * as mockDataProvider from '@librechat/data-provider';
|
||||
import * as mockDataProvider from 'librechat-data-provider';
|
||||
|
||||
jest.mock('@librechat/data-provider');
|
||||
jest.mock('librechat-data-provider');
|
||||
|
||||
const setup = ({
|
||||
useGetUserQueryReturnValue = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from 'layout-test-utils';
|
||||
import { render } from 'test/layout-test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Login from '../LoginForm';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render, waitFor } from 'layout-test-utils';
|
||||
import { render, waitFor, screen } from 'test/layout-test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Registration from '../Registration';
|
||||
import * as mockDataProvider from '@librechat/data-provider';
|
||||
import * as mockDataProvider from 'librechat-data-provider';
|
||||
|
||||
jest.mock('@librechat/data-provider');
|
||||
jest.mock('librechat-data-provider');
|
||||
|
||||
const setup = ({
|
||||
useGetUserQueryReturnValue = {
|
||||
@@ -17,6 +17,7 @@ const setup = ({
|
||||
mutate: jest.fn(),
|
||||
data: {},
|
||||
isSuccess: false,
|
||||
error: null as Error | null,
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
@@ -76,30 +77,31 @@ test('renders registration form', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('calls registerUser.mutate on registration', async () => {
|
||||
const mutate = jest.fn();
|
||||
const { getByTestId, getByRole, history } = setup({
|
||||
// @ts-ignore - we don't need all parameters of the QueryObserverResult
|
||||
useLoginUserReturnValue: {
|
||||
isLoading: false,
|
||||
mutate: mutate,
|
||||
isError: false,
|
||||
isSuccess: true,
|
||||
},
|
||||
});
|
||||
// test('calls registerUser.mutate on registration', async () => {
|
||||
// const mutate = jest.fn();
|
||||
// const { getByTestId, getByRole, history } = setup({
|
||||
// // @ts-ignore - we don't need all parameters of the QueryObserverResult
|
||||
// useLoginUserReturnValue: {
|
||||
// isLoading: false,
|
||||
// mutate: mutate,
|
||||
// isError: false,
|
||||
// isSuccess: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
await userEvent.type(getByRole('textbox', { name: /Full name/i }), 'John Doe');
|
||||
await userEvent.type(getByRole('textbox', { name: /Username/i }), 'johndoe');
|
||||
await userEvent.type(getByRole('textbox', { name: /Email/i }), 'test@test.com');
|
||||
await userEvent.type(getByTestId('password'), 'password');
|
||||
await userEvent.type(getByTestId('confirm_password'), 'password');
|
||||
await userEvent.click(getByRole('button', { name: /Submit registration/i }));
|
||||
// await userEvent.type(getByRole('textbox', { name: /Full name/i }), 'John Doe');
|
||||
// await userEvent.type(getByRole('textbox', { name: /Username/i }), 'johndoe');
|
||||
// await userEvent.type(getByRole('textbox', { name: /Email/i }), 'test@test.com');
|
||||
// await userEvent.type(getByTestId('password'), 'password');
|
||||
// await userEvent.type(getByTestId('confirm_password'), 'password');
|
||||
// await userEvent.click(getByRole('button', { name: /Submit registration/i }));
|
||||
|
||||
waitFor(() => {
|
||||
expect(mutate).toHaveBeenCalled();
|
||||
expect(history.location.pathname).toBe('/chat/new');
|
||||
});
|
||||
});
|
||||
// console.log(history);
|
||||
// waitFor(() => {
|
||||
// // expect(mutate).toHaveBeenCalled();
|
||||
// expect(history.location.pathname).toBe('/chat/new');
|
||||
// });
|
||||
// });
|
||||
|
||||
test('shows validation error messages', async () => {
|
||||
const { getByTestId, getAllByRole, getByRole } = setup();
|
||||
@@ -123,7 +125,7 @@ test('shows error message when registration fails', async () => {
|
||||
useRegisterUserMutationReturnValue: {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
mutate: mutate,
|
||||
mutate,
|
||||
error: new Error('Registration failed'),
|
||||
data: {},
|
||||
isSuccess: false,
|
||||
@@ -138,8 +140,8 @@ test('shows error message when registration fails', async () => {
|
||||
await userEvent.click(getByRole('button', { name: /Submit registration/i }));
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(
|
||||
expect(screen.getByTestId('registration-error')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('registration-error')).toHaveTextContent(
|
||||
/There was an error attempting to register your account. Please try again. Registration failed/i,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useUpdateConversationMutation } from '@librechat/data-provider';
|
||||
import { useUpdateConversationMutation } from 'librechat-data-provider';
|
||||
import RenameButton from './RenameButton';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import ConvoIcon from '../svg/ConvoIcon';
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import Conversation from './Conversation';
|
||||
import { TConversation } from 'librechat-data-provider';
|
||||
|
||||
export default function Conversations({ conversations, moveToTop }) {
|
||||
export default function Conversations({
|
||||
conversations,
|
||||
moveToTop,
|
||||
}: {
|
||||
conversations: TConversation[];
|
||||
moveToTop: () => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{conversations &&
|
||||
conversations.length > 0 &&
|
||||
conversations.map((convo) => {
|
||||
conversations.map((convo: TConversation) => {
|
||||
return (
|
||||
<Conversation key={convo.conversationId} conversation={convo} retainView={moveToTop} />
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
||||
import TrashIcon from '../svg/TrashIcon';
|
||||
import CrossIcon from '../svg/CrossIcon';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useDeleteConversationMutation } from '@librechat/data-provider';
|
||||
import { useDeleteConversationMutation } from 'librechat-data-provider';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Pages({ pageNumber, pages, nextPage, previousPage }) {
|
||||
const clickHandler = (func) => async (e) => {
|
||||
e.preventDefault();
|
||||
await func();
|
||||
};
|
||||
type TPagesProps = {
|
||||
pages: number;
|
||||
pageNumber: number;
|
||||
setPageNumber: (pageNumber: number) => void;
|
||||
nextPage: () => Promise<void>;
|
||||
previousPage: () => Promise<void>;
|
||||
};
|
||||
|
||||
export default function Pages({
|
||||
pageNumber,
|
||||
pages,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageNumber,
|
||||
}: TPagesProps) {
|
||||
const clickHandler =
|
||||
(func: () => Promise<void>) => async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
await func();
|
||||
};
|
||||
|
||||
if (pageNumber > pages) {
|
||||
setPageNumber(pages);
|
||||
}
|
||||
|
||||
return pageNumber == 1 && pages == 1 ? null : (
|
||||
<div className="m-auto mb-2 mt-4 flex items-center justify-center gap-2">
|
||||
5
client/src/components/Conversations/index.ts
Normal file
5
client/src/components/Conversations/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as Pages } from './Pages';
|
||||
export { default as Conversation } from './Conversation';
|
||||
export { default as DeleteButton } from './DeleteButton';
|
||||
export { default as RenameButton } from './RenameButton';
|
||||
export { default as Conversations } from './Conversations';
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { HoverCardPortal, HoverCardContent } from '~/components/ui/HoverCard.tsx';
|
||||
|
||||
const types = {
|
||||
temp: 'Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks. We recommend altering this or Top P but not both.',
|
||||
topp: 'Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.',
|
||||
topk: 'Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model\'s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).',
|
||||
maxoutputtokens:
|
||||
' Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses.',
|
||||
};
|
||||
|
||||
function OptionHover({ type, side }) {
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={side} className="w-80 ">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionHover;
|
||||
@@ -1,251 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import SelectDropDown from '../../ui/SelectDropDown';
|
||||
import { Input } from '~/components/ui/Input.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Slider } from '~/components/ui/Slider.tsx';
|
||||
import { InputNumber } from '~/components/ui/InputNumber.tsx';
|
||||
import OptionHover from './OptionHover';
|
||||
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
||||
import { cn } from '~/utils/';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
const optionText =
|
||||
'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function Settings(props) {
|
||||
const {
|
||||
readonly,
|
||||
model,
|
||||
modelLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
topP,
|
||||
topK,
|
||||
maxOutputTokens,
|
||||
setOption,
|
||||
} = props;
|
||||
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setModel = setOption('model');
|
||||
const setModelLabel = setOption('modelLabel');
|
||||
const setPromptPrefix = setOption('promptPrefix');
|
||||
const setTemperature = setOption('temperature');
|
||||
const setTopP = setOption('topP');
|
||||
const setTopK = setOption('topK');
|
||||
const setMaxOutputTokens = setOption('maxOutputTokens');
|
||||
|
||||
const models = endpointsConfig?.['anthropic']?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<div className={'h-[490px] overflow-y-auto md:h-[350px]'}>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
value={model}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'z-50 flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="modelLabel" className="text-left text-sm font-medium">
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="modelLabel"
|
||||
disabled={readonly}
|
||||
value={modelLabel || ''}
|
||||
onChange={(e) => setModelLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for Claude"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions or context. Ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 ',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
Temperature <small className="opacity-40">(default: 0.7)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
onChange={(value) => setTemperature(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature]}
|
||||
onValueChange={(value) => setTemperature(value[0])}
|
||||
doubleClickHandler={() => setTemperature(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="temp" side="left" />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
Top P <small className="opacity-40">(default: 0.95)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||
Top K <small className="opacity-40">(default: 40)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-k-int"
|
||||
disabled={readonly}
|
||||
value={topK}
|
||||
onChange={(value) => setTopK(value)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topK]}
|
||||
onValueChange={(value) => setTopK(value[0])}
|
||||
doubleClickHandler={() => setTopK(0)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topk" side="left" />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
||||
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="max-tokens-int"
|
||||
disabled={readonly}
|
||||
value={maxOutputTokens}
|
||||
onChange={(value) => setMaxOutputTokens(value)}
|
||||
max={1024}
|
||||
min={1}
|
||||
step={1}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[maxOutputTokens]}
|
||||
onValueChange={(value) => setMaxOutputTokens(value[0])}
|
||||
doubleClickHandler={() => setMaxOutputTokens(0)}
|
||||
max={1024}
|
||||
min={1}
|
||||
step={1}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="maxoutputtokens" side="left" />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
@@ -1,143 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
||||
import SelectDropDown from '../../ui/SelectDropDown';
|
||||
import { cn } from '~/utils/';
|
||||
import useDebounce from '~/hooks/useDebounce';
|
||||
import { useUpdateTokenCountMutation } from '@librechat/data-provider';
|
||||
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
function Settings(props) {
|
||||
const { readonly, context, systemMessage, jailbreak, toneStyle, setOption } = props;
|
||||
const [tokenCount, setTokenCount] = useState(0);
|
||||
const showSystemMessage = jailbreak;
|
||||
const setContext = setOption('context');
|
||||
const setSystemMessage = setOption('systemMessage');
|
||||
const setJailbreak = setOption('jailbreak');
|
||||
const setToneStyle = (value) => setOption('toneStyle')(value.toLowerCase());
|
||||
const debouncedContext = useDebounce(context, 250);
|
||||
const updateTokenCountMutation = useUpdateTokenCountMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedContext || debouncedContext.trim() === '') {
|
||||
setTokenCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTextChange = (context) => {
|
||||
updateTokenCountMutation.mutate(
|
||||
{ text: context },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setTokenCount(data.count);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
handleTextChange(debouncedContext);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedContext]);
|
||||
|
||||
return (
|
||||
<div className="h-[490px] overflow-y-auto md:h-[350px]">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="toneStyle-dropdown" className="text-left text-sm font-medium">
|
||||
Tone Style <small className="opacity-40">(default: creative)</small>
|
||||
</Label>
|
||||
<SelectDropDown
|
||||
id="toneStyle-dropdown"
|
||||
title={null}
|
||||
value={`${toneStyle.charAt(0).toUpperCase()}${toneStyle.slice(1)}`}
|
||||
setValue={setToneStyle}
|
||||
availableValues={['Creative', 'Fast', 'Balanced', 'Precise']}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="context" className="text-left text-sm font-medium">
|
||||
Context <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="context"
|
||||
disabled={readonly}
|
||||
value={context || ''}
|
||||
onChange={(e) => setContext(e.target.value || null)}
|
||||
placeholder="Bing can use up to 7k tokens for 'context', which it can reference for the conversation. The specific limit is not known but may run into errors exceeding 7k tokens"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2',
|
||||
)}
|
||||
/>
|
||||
<small className="mb-5 text-black dark:text-white">{`Token count: ${tokenCount}`}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="jailbreak" className="text-left text-sm font-medium">
|
||||
Enable Sydney <small className="opacity-40">(default: false)</small>
|
||||
</Label>
|
||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||
<Checkbox
|
||||
id="jailbreak"
|
||||
disabled={readonly}
|
||||
checked={jailbreak}
|
||||
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||
onCheckedChange={setJailbreak}
|
||||
/>
|
||||
<label
|
||||
htmlFor="jailbreak"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
Jailbreak <small>To enable Sydney</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{showSystemMessage && (
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="systemMessage"
|
||||
className="text-left text-sm font-medium"
|
||||
style={{ opacity: showSystemMessage ? '1' : '0' }}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/danny-avila/LibreChat/blob/main/client/defaultSystemMessage.md"
|
||||
target="_blank"
|
||||
className="text-blue-500 transition-colors duration-200 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-500"
|
||||
rel="noreferrer"
|
||||
>
|
||||
System Message
|
||||
</a>{' '}
|
||||
<small className="opacity-40 dark:text-gray-50">(default: blank)</small>
|
||||
</Label>
|
||||
|
||||
<TextareaAutosize
|
||||
id="systemMessage"
|
||||
disabled={readonly}
|
||||
value={systemMessage || ''}
|
||||
onChange={(e) => setSystemMessage(e.target.value || null)}
|
||||
placeholder="WARNING: Misuse of this feature can get you BANNED from using Bing! Click on 'System Message' for full instructions and the default message if omitted, which is the 'Sydney' preset that is considered safe."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 placeholder:text-red-400',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
@@ -1,280 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Settings from './Settings';
|
||||
import Examples from './Google/Examples.jsx';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import AgentSettings from './Plugins/AgentSettings.jsx';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import filenamify from 'filenamify';
|
||||
import {
|
||||
MessagesSquared,
|
||||
GPTIcon,
|
||||
Input,
|
||||
Label,
|
||||
Button,
|
||||
Dropdown,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogButton,
|
||||
DialogTemplate,
|
||||
} from '~/components/';
|
||||
import { cn } from '~/utils/';
|
||||
import cleanupPreset from '~/utils/cleanupPreset';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
const [preset, setPreset] = useState(_preset);
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
const [showExamples, setShowExamples] = useState(false);
|
||||
const [showAgentSettings, setShowAgentSettings] = useState(false);
|
||||
|
||||
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const triggerExamples = () => setShowExamples((prev) => !prev);
|
||||
const triggerAgentSettings = () => setShowAgentSettings((prev) => !prev);
|
||||
|
||||
const setOption = (param) => (newValue) => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setAgentOption = (param) => (newValue) => {
|
||||
let editablePreset = JSON.stringify(_preset);
|
||||
editablePreset = JSON.parse(editablePreset);
|
||||
let { agentOptions } = editablePreset;
|
||||
agentOptions[param] = newValue;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
agentOptions,
|
||||
},
|
||||
endpointsConfig,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setExample = (i, type, newValue = null) => {
|
||||
let update = {};
|
||||
let current = preset?.examples.slice() || [];
|
||||
let currentExample = { ...current[i] } || {};
|
||||
currentExample[type] = { content: newValue };
|
||||
current[i] = currentExample;
|
||||
update.examples = current;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const addExample = () => {
|
||||
let update = {};
|
||||
let current = preset?.examples.slice() || [];
|
||||
current.push({ input: { content: '' }, output: { content: '' } });
|
||||
update.examples = current;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const removeExample = () => {
|
||||
let update = {};
|
||||
let current = preset?.examples.slice() || [];
|
||||
if (current.length <= 1) {
|
||||
update.examples = [{ input: { content: '' }, output: { content: '' } }];
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
current.pop();
|
||||
update.examples = current;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update,
|
||||
},
|
||||
endpointsConfig,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
const submitPreset = () => {
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/api/presets',
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
withCredentials: true,
|
||||
}).then((res) => {
|
||||
setPresets(res?.data);
|
||||
});
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
const fileName = filenamify(preset?.title || 'preset');
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
fileName,
|
||||
exportType: exportFromJSON.types.json,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const endpoint = preset?.endpoint;
|
||||
const isGoogle = endpoint === 'google';
|
||||
const isGptPlugins = endpoint === 'gptPlugins';
|
||||
const shouldShowSettings =
|
||||
(isGoogle && !showExamples) ||
|
||||
(isGptPlugins && !showAgentSettings) ||
|
||||
(!isGoogle && !isGptPlugins);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'Edit Preset'} - ${preset?.title}`}
|
||||
className="h-[675px] max-w-full sm:max-w-4xl "
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2 md:h-[475px]">
|
||||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
Preset Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={preset?.title || ''}
|
||||
onChange={(e) => setOption('title')(e.target.value || '')}
|
||||
placeholder="Set a custom name, in case you can find this preset"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label htmlFor="endpoint" className="text-left text-sm font-medium">
|
||||
Endpoint
|
||||
</Label>
|
||||
<Dropdown
|
||||
id="endpoint"
|
||||
value={preset?.endpoint || ''}
|
||||
onChange={setOption('endpoint')}
|
||||
options={availableEndpoints}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
{preset?.endpoint === 'google' && (
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-1 flex h-auto w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={triggerExamples}
|
||||
>
|
||||
<MessagesSquared className="mr-1 w-[14px]" />
|
||||
{(showExamples ? 'Hide' : 'Show') + ' Examples'}
|
||||
</Button>
|
||||
)}
|
||||
{preset?.endpoint === 'gptPlugins' && (
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-1 flex h-auto w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={triggerAgentSettings}
|
||||
>
|
||||
<GPTIcon className="mr-1 mt-[2px] w-[14px]" size={14} />
|
||||
{`Show ${showAgentSettings ? 'Completion' : 'Agent'} Settings`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
|
||||
<div className="w-full p-0">
|
||||
{shouldShowSettings && <Settings preset={preset} setOption={setOption} />}
|
||||
{preset?.endpoint === 'google' &&
|
||||
showExamples &&
|
||||
!preset?.model?.startsWith('codechat-') && (
|
||||
<Examples
|
||||
examples={preset.examples}
|
||||
setExample={setExample}
|
||||
addExample={addExample}
|
||||
removeExample={removeExample}
|
||||
edit={true}
|
||||
/>
|
||||
)}
|
||||
{preset?.endpoint === 'gptPlugins' && showAgentSettings && (
|
||||
<AgentSettings
|
||||
agent={preset.agentOptions.agent}
|
||||
skipCompletion={preset.agentOptions.skipCompletion}
|
||||
model={preset.agentOptions.model}
|
||||
endpoint={preset.agentOptions.endpoint}
|
||||
temperature={preset.agentOptions.temperature}
|
||||
topP={preset.agentOptions.top_p}
|
||||
freqP={preset.agentOptions.presence_penalty}
|
||||
presP={preset.agentOptions.frequency_penalty}
|
||||
setOption={setAgentOption}
|
||||
tools={preset.tools}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<>
|
||||
<DialogClose
|
||||
onClick={submitPreset}
|
||||
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
Save
|
||||
</DialogClose>
|
||||
</>
|
||||
}
|
||||
leftButtons={
|
||||
<>
|
||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
||||
Export
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPresetDialog;
|
||||
146
client/src/components/Endpoints/EditPresetDialog.tsx
Normal file
146
client/src/components/Endpoints/EditPresetDialog.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import axios from 'axios';
|
||||
import { useEffect } from 'react';
|
||||
import filenamify from 'filenamify';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import { useSetRecoilState, useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { TEditPresetProps } from '~/common';
|
||||
import { useSetOptions, useLocalize } from '~/hooks';
|
||||
import { Input, Label, Dropdown, Dialog, DialogClose, DialogButton } from '~/components/';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import PopoverButtons from './PopoverButtons';
|
||||
import EndpointSettings from './EndpointSettings';
|
||||
import { cn, defaultTextProps, removeFocusOutlines, cleanupPreset } from '~/utils/';
|
||||
import store from '~/store';
|
||||
|
||||
const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }: TEditPresetProps) => {
|
||||
const [preset, setPreset] = useRecoilState(store.preset);
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
const { setOption } = useSetOptions(_preset);
|
||||
const localize = useLocalize();
|
||||
|
||||
const submitPreset = () => {
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/api/presets',
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
withCredentials: true,
|
||||
}).then((res) => {
|
||||
setPresets(res?.data);
|
||||
});
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
const fileName = filenamify(preset?.title || 'preset');
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
fileName,
|
||||
exportType: exportFromJSON.types.json,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const { endpoint } = preset || {};
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || localize('com_endpoint_edit_preset')} - ${preset?.title}`}
|
||||
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[720px] md:w-[750px] md:overflow-y-hidden lg:w-[950px] xl:h-[720px]"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2 md:h-[530px]">
|
||||
<div className="grid w-full grid-cols-5 gap-6">
|
||||
<div className="col-span-4 flex items-start justify-start gap-4">
|
||||
<div className="flex w-full flex-col">
|
||||
<Label htmlFor="preset-name" className="mb-1 text-left text-sm font-medium">
|
||||
{localize('com_endpoint_preset_name')}
|
||||
</Label>
|
||||
<Input
|
||||
id="preset-name"
|
||||
value={preset?.title || ''}
|
||||
onChange={(e) => setOption('title')(e.target.value || '')}
|
||||
placeholder={localize('com_endpoint_set_custom_name')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col">
|
||||
<Label htmlFor="endpoint" className="mb-1 text-left text-sm font-medium">
|
||||
{localize('com_endpoint')}
|
||||
</Label>
|
||||
<Dropdown
|
||||
value={endpoint || ''}
|
||||
onChange={setOption('endpoint')}
|
||||
options={availableEndpoints}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none ',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
containerClassName="flex w-full resize-none z-[51]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-start justify-start gap-4 sm:col-span-1">
|
||||
<div className="flex w-full flex-col">
|
||||
<Label
|
||||
htmlFor="endpoint"
|
||||
className="mb-1 hidden text-left text-sm font-medium sm:block"
|
||||
>
|
||||
{'ㅤ'}
|
||||
</Label>
|
||||
<PopoverButtons
|
||||
endpoint={endpoint}
|
||||
buttonClass="ml-0 w-full dark:bg-gray-700 dark:hover:bg-gray-800 p-2 h-[40px] justify-center mt-0"
|
||||
iconClass="hidden lg:block w-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
|
||||
<div className="w-full p-0">
|
||||
<EndpointSettings
|
||||
conversation={preset}
|
||||
setOption={setOption}
|
||||
isPreset={true}
|
||||
className="h-full md:mb-4 md:h-[440px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<div className="mb-6 md:mb-2">
|
||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
||||
{localize('com_endpoint_export')}
|
||||
</DialogButton>
|
||||
<DialogClose
|
||||
onClick={submitPreset}
|
||||
className="dark:hover:gray-400 ml-2 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
{localize('com_endpoint_save')}
|
||||
</DialogClose>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPresetDialog;
|
||||
@@ -1,86 +0,0 @@
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Dialog, DialogButton, DialogTemplate } from '~/components';
|
||||
import SaveAsPresetDialog from './SaveAsPresetDialog';
|
||||
import cleanupPreset from '~/utils/cleanupPreset';
|
||||
import { alternateName } from '~/utils';
|
||||
import Settings from './Settings';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
// A preset dialog to show readonly preset values.
|
||||
const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
const [preset, setPreset] = useState(_preset);
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
const endpointName = alternateName[preset?.endpoint] ?? 'Endpoint';
|
||||
|
||||
const setOption = (param) => (newValue) => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setPreset((prevState) => ({
|
||||
...prevState,
|
||||
...update,
|
||||
}));
|
||||
};
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
fileName: `${preset?.title}.json`,
|
||||
exportType: exportFromJSON.types.json,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'View Options'} - ${endpointName}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="w-full p-0">
|
||||
<Settings preset={preset} readonly={true} setOption={setOption} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<>
|
||||
<DialogButton
|
||||
onClick={saveAsPreset}
|
||||
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
Save As Preset
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
leftButtons={
|
||||
<>
|
||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
||||
Export
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={preset}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndpointOptionsDialog;
|
||||
114
client/src/components/Endpoints/EndpointOptionsDialog.tsx
Normal file
114
client/src/components/Endpoints/EndpointOptionsDialog.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import { tPresetSchema } from 'librechat-data-provider';
|
||||
import type { TSetOption, TEditPresetProps } from '~/common';
|
||||
import { Dialog, DialogButton } from '~/components/ui';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import SaveAsPresetDialog from './SaveAsPresetDialog';
|
||||
import EndpointSettings from './EndpointSettings';
|
||||
import PopoverButtons from './PopoverButtons';
|
||||
import { cleanupPreset } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
// A preset dialog to show readonly preset values.
|
||||
const EndpointOptionsDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
preset: _preset,
|
||||
title,
|
||||
}: TEditPresetProps) => {
|
||||
const [preset, setPreset] = useRecoilState(store.preset);
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
const localize = useLocalize();
|
||||
|
||||
const setOption: TSetOption = (param) => (newValue) => {
|
||||
const update = {};
|
||||
update[param] = newValue;
|
||||
setPreset((prevState) =>
|
||||
tPresetSchema.parse({
|
||||
...prevState,
|
||||
...update,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
fileName: `${preset?.title}.json`,
|
||||
exportType: exportFromJSON.types.json,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const { endpoint } = preset ?? {};
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || localize('com_endpoint_save_convo_as_preset')}`}
|
||||
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[680px] md:w-[750px] md:overflow-y-hidden lg:w-[950px]"
|
||||
// headerClassName="sm:p-2 h-16"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2 md:h-[530px]">
|
||||
<div className="w-full p-0">
|
||||
<PopoverButtons
|
||||
endpoint={endpoint}
|
||||
buttonClass="ml-0 mb-4 col-span-2 dark:bg-gray-700 dark:hover:bg-gray-800 p-2"
|
||||
/>
|
||||
<EndpointSettings
|
||||
conversation={preset}
|
||||
setOption={setOption}
|
||||
isPreset={true}
|
||||
className="h-full md:mb-0 md:h-[490px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<div className="mb-6 md:mb-2">
|
||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
||||
{localize('com_endpoint_export')}
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
onClick={saveAsPreset}
|
||||
className="dark:hover:gray-400 ml-2 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
{localize('com_endpoint_save_as_preset')}
|
||||
</DialogButton>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={preset}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndpointOptionsDialog;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../ui/Button.tsx';
|
||||
import CrossIcon from '../svg/CrossIcon';
|
||||
// import SaveIcon from '../svg/SaveIcon';
|
||||
import { Save } from 'lucide-react';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
function EndpointOptionsPopover({
|
||||
content,
|
||||
visible,
|
||||
saveAsPreset,
|
||||
switchToSimpleMode,
|
||||
additionalButton = null,
|
||||
}) {
|
||||
const cardStyle =
|
||||
'shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
' endpointOptionsPopover-container absolute bottom-[-10px] z-0 flex w-full flex-col items-center md:px-4' +
|
||||
(visible ? ' show' : '')
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
cardStyle +
|
||||
' border-d-0 flex w-full flex-col overflow-hidden rounded-none border-s-0 border-t bg-slate-200 px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]'
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center bg-slate-100 px-2 py-2 dark:bg-gray-800/60">
|
||||
{/* <span className="text-xs font-medium font-normal">Advanced settings for OpenAI endpoint</span> */}
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
Save as preset
|
||||
</Button>
|
||||
{additionalButton && (
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
additionalButton.buttonClass,
|
||||
'ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0',
|
||||
)}
|
||||
onClick={additionalButton.handler}
|
||||
>
|
||||
{additionalButton.icon}
|
||||
{additionalButton.label}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-auto h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
onClick={switchToSimpleMode}
|
||||
>
|
||||
<CrossIcon className="mr-1" />
|
||||
{/* Switch to simple mode */}
|
||||
</Button>
|
||||
</div>
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndpointOptionsPopover;
|
||||
69
client/src/components/Endpoints/EndpointOptionsPopover.tsx
Normal file
69
client/src/components/Endpoints/EndpointOptionsPopover.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Save } from 'lucide-react';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { Button } from '~/components/ui';
|
||||
import { CrossIcon } from '~/components/svg';
|
||||
import PopoverButtons from './PopoverButtons';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
type TEndpointOptionsPopoverProps = {
|
||||
children: React.ReactNode;
|
||||
visible: boolean;
|
||||
endpoint: EModelEndpoint;
|
||||
saveAsPreset: () => void;
|
||||
closePopover: () => void;
|
||||
};
|
||||
|
||||
export default function EndpointOptionsPopover({
|
||||
children,
|
||||
endpoint,
|
||||
visible,
|
||||
saveAsPreset,
|
||||
closePopover,
|
||||
}: TEndpointOptionsPopoverProps) {
|
||||
const localize = useLocalize();
|
||||
const cardStyle =
|
||||
'shadow-xl rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'endpointOptionsPopover-container absolute bottom-[-10px] z-0 flex w-full flex-col items-center md:px-4',
|
||||
visible ? ' show' : '',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'border-d-0 flex w-full flex-col overflow-hidden rounded-none border-s-0 border-t bg-white px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center bg-slate-100 px-2 py-2 dark:bg-gray-800/60">
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
{localize('com_endpoint_save_as_preset')}
|
||||
</Button>
|
||||
<PopoverButtons endpoint={endpoint} />
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
onClick={closePopover}
|
||||
>
|
||||
<CrossIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
client/src/components/Endpoints/EndpointSettings.tsx
Normal file
59
client/src/components/Endpoints/EndpointSettings.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { OpenAISettings, BingAISettings, AnthropicSettings } from './Settings';
|
||||
import { GoogleSettings, PluginsSettings } from './Settings/MultiView';
|
||||
import type { TSettingsProps, TModelSelectProps, TBaseSettingsProps, TModels } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const optionComponents: { [key: string]: React.FC<TModelSelectProps> } = {
|
||||
openAI: OpenAISettings,
|
||||
azureOpenAI: OpenAISettings,
|
||||
bingAI: BingAISettings,
|
||||
anthropic: AnthropicSettings,
|
||||
};
|
||||
|
||||
const multiViewComponents: { [key: string]: React.FC<TBaseSettingsProps & TModels> } = {
|
||||
google: GoogleSettings,
|
||||
gptPlugins: PluginsSettings,
|
||||
};
|
||||
|
||||
export default function Settings({
|
||||
conversation,
|
||||
setOption,
|
||||
isPreset = false,
|
||||
className = '',
|
||||
}: TSettingsProps) {
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
if (!conversation?.endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { endpoint } = conversation;
|
||||
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
|
||||
const OptionComponent = optionComponents[endpoint];
|
||||
|
||||
if (OptionComponent) {
|
||||
return (
|
||||
<div className={cn('h-[480px] overflow-y-auto md:mb-2 md:h-[350px]', className)}>
|
||||
<OptionComponent
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
models={models}
|
||||
isPreset={isPreset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MultiViewComponent = multiViewComponents[endpoint];
|
||||
|
||||
if (!MultiViewComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('h-[480px] overflow-y-auto md:mb-2 md:h-[350px]', className)}>
|
||||
<MultiViewComponent conversation={conversation} models={models} isPreset={isPreset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button } from '~/components/ui/Button.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Plus, Minus } from 'lucide-react';
|
||||
import { cn } from '~/utils/';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
function Examples({ readonly, examples, setExample, addExample, removeExample, edit = false }) {
|
||||
const maxHeight = edit ? 'max-h-[233px]' : 'max-h-[350px]';
|
||||
return (
|
||||
<>
|
||||
<div className={`${maxHeight} overflow-y-auto`}>
|
||||
<div id="examples-grid" className="grid gap-6 sm:grid-cols-2">
|
||||
{examples.map((example, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{/* Input */}
|
||||
<div
|
||||
className={`col-span-${
|
||||
examples.length === 1 ? '1' : 'full'
|
||||
} flex flex-col items-center justify-start gap-6 sm:col-span-1`}
|
||||
>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor={`input-${idx}`} className="text-left text-sm font-medium">
|
||||
Input <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id={`input-${idx}`}
|
||||
disabled={readonly}
|
||||
value={example?.input?.content || ''}
|
||||
onChange={(e) => setExample(idx, 'input', e.target.value || null)}
|
||||
placeholder="Set example input. Example is ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[75px] w-full resize-none px-3 py-2 ',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output */}
|
||||
<div
|
||||
className={`col-span-${
|
||||
examples.length === 1 ? '1' : 'full'
|
||||
} flex flex-col items-center justify-start gap-6 sm:col-span-1`}
|
||||
>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor={`output-${idx}`} className="text-left text-sm font-medium">
|
||||
Output <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id={`output-${idx}`}
|
||||
disabled={readonly}
|
||||
value={example?.output?.content || ''}
|
||||
onChange={(e) => setExample(idx, 'output', e.target.value || null)}
|
||||
placeholder={'Set example output. Example is ignored if empty.'}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[75px] w-full resize-none px-3 py-2 ',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
className="mr-2 mt-1 h-auto items-center justify-center bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={removeExample}
|
||||
>
|
||||
<Minus className="w-[16px]" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-1 h-auto items-center justify-center bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={addExample}
|
||||
>
|
||||
<Plus className="w-[16px]" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Examples;
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { HoverCardPortal, HoverCardContent } from '~/components/ui/HoverCard.tsx';
|
||||
|
||||
const types = {
|
||||
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
|
||||
topp: 'Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.',
|
||||
topk: 'Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model\'s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).',
|
||||
maxoutputtokens:
|
||||
' Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses.',
|
||||
};
|
||||
|
||||
function OptionHover({ type, side }) {
|
||||
// const options = {};
|
||||
// if (type === 'pres') {
|
||||
// options.sideOffset = 45;
|
||||
// }
|
||||
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
side={side}
|
||||
className="w-80 "
|
||||
// {...options}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionHover;
|
||||
@@ -1,260 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import SelectDropDown from '../../ui/SelectDropDown';
|
||||
import { Input } from '~/components/ui/Input.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Slider } from '~/components/ui/Slider.tsx';
|
||||
import { InputNumber } from '~/components/ui/InputNumber.tsx';
|
||||
import OptionHover from './OptionHover';
|
||||
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
||||
import { cn } from '~/utils/';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
const optionText =
|
||||
'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function Settings(props) {
|
||||
const {
|
||||
readonly,
|
||||
model,
|
||||
modelLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
topP,
|
||||
topK,
|
||||
maxOutputTokens,
|
||||
setOption,
|
||||
} = props;
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setModel = setOption('model');
|
||||
const setModelLabel = setOption('modelLabel');
|
||||
const setPromptPrefix = setOption('promptPrefix');
|
||||
const setTemperature = setOption('temperature');
|
||||
const setTopP = setOption('topP');
|
||||
const setTopK = setOption('topK');
|
||||
const setMaxOutputTokens = setOption('maxOutputTokens');
|
||||
|
||||
const models = endpointsConfig?.['google']?.['availableModels'] || [];
|
||||
|
||||
const codeChat = model.startsWith('codechat-');
|
||||
|
||||
return (
|
||||
<div className={'h-[490px] overflow-y-auto md:h-[350px]'}>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
value={model}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'z-50 flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
{!codeChat && (
|
||||
<>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="modelLabel" className="text-left text-sm font-medium">
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="modelLabel"
|
||||
disabled={readonly}
|
||||
value={modelLabel || ''}
|
||||
onChange={(e) => setModelLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for PaLM2"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions or context. Ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 ',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
Temperature <small className="opacity-40">(default: 0.2)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
onChange={(value) => setTemperature(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature]}
|
||||
onValueChange={(value) => setTemperature(value[0])}
|
||||
doubleClickHandler={() => setTemperature(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="temp" side="left" />
|
||||
</HoverCard>
|
||||
{!codeChat && (
|
||||
<>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
Top P <small className="opacity-40">(default: 0.95)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||
Top K <small className="opacity-40">(default: 40)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-k-int"
|
||||
disabled={readonly}
|
||||
value={topK}
|
||||
onChange={(value) => setTopK(value)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topK]}
|
||||
onValueChange={(value) => setTopK(value[0])}
|
||||
doubleClickHandler={() => setTopK(0)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topk" side="left" />
|
||||
</HoverCard>
|
||||
</>
|
||||
)}
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
||||
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="max-tokens-int"
|
||||
disabled={readonly}
|
||||
value={maxOutputTokens}
|
||||
onChange={(value) => setMaxOutputTokens(value)}
|
||||
max={1024}
|
||||
min={1}
|
||||
step={1}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[maxOutputTokens]}
|
||||
onValueChange={(value) => setMaxOutputTokens(value[0])}
|
||||
doubleClickHandler={() => setMaxOutputTokens(0)}
|
||||
max={1024}
|
||||
min={1}
|
||||
step={1}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="maxoutputtokens" side="left" />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user