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

* wip: proof of concept for openapi chain

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

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

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

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

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

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

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

* feat(api): add new tools

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

feat(api): update OpenAPIPlugin.js

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

fix(api): fix loadTools.js

- Pass the user parameter to the loadSpecs function.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Update chatgpt_plugins_openapi.md

* chore: rebuild package-lock file

* chore: format/lint all files with new rules

* chore: format all files

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

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

* fix(Plugin.tsx): type issue

* feat(tools): add new tool WebPilot

feat(tools): remove tool Weather Report

feat(tools): add new tool Prompt Perfect

feat(tools): add new tool Scholarly Graph Link

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

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

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

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

* fix: package file resolution bug

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

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

* Update introduction.md

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

View file

@ -44,8 +44,10 @@ module.exports = {
},
],
'linebreak-style': 0,
'object-curly-spacing': ['error', 'always'],
'curly': ['error', 'all'],
'semi': ['error', 'always'],
'no-trailing-spaces': 'error',
'object-curly-spacing': ['error', 'always'],
'no-multiple-empty-lines': ['error', { max: 1 }],
'comma-dangle': ['error', 'always-multiline'],
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],

View file

@ -36,7 +36,7 @@ https://github.com/danny-avila/LibreChat/assets/110412045/c1eb0c0f-41f6-4335-b98
# Features
- Response streaming identical to ChatGPT through server-sent events
- UI from original ChatGPT, including Dark mode
- AI model selection (through 6 endpoints: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Claude, Plugins)
- AI model selection: OpenAI API, BingAI, ChatGPT Browser, PaLM2, Anthropic (Claude), Plugins
- Create, Save, & Share custom presets - [More info on prompt presets here](https://github.com/danny-avila/LibreChat/releases/tag/v0.3.0)
- Edit and Resubmit messages with conversation branching
- Search all messages/conversations - [More info here](https://github.com/danny-avila/LibreChat/releases/tag/v0.1.0)

View file

@ -39,7 +39,7 @@ const askBing = async ({
jailbreakConversationId = false;
}
if (jailbreak)
if (jailbreak) {
options = {
jailbreakConversationId: jailbreakConversationId || jailbreak,
context,
@ -48,7 +48,7 @@ const askBing = async ({
toneStyle,
onProgress,
};
else {
} else {
options = {
conversationId,
context,

View file

@ -2,6 +2,7 @@ const OpenAIClient = require('./OpenAIClient');
const { ChatOpenAI } = require('langchain/chat_models/openai');
const { CallbackManager } = require('langchain/callbacks');
const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents/');
const { findMessageContent } = require('../../utils');
const { loadTools } = require('./tools/util');
const { SelfReflectionTool } = require('./tools/');
const { HumanChatMessage, AIChatMessage } = require('langchain/schema');
@ -193,6 +194,8 @@ Only respond with your conversational reply to the following User Message:
functions: this.functionsAgent,
options: {
openAIApiKey: this.openAIApiKey,
debug: this.options?.debug,
message,
},
});
// load tools
@ -266,6 +269,15 @@ 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) {
@ -290,6 +302,11 @@ Only respond with your conversational reply to the following User Message:
} catch (err) {
console.error(err);
errorMessage = err.message;
const content = findMessageContent(message);
if (content) {
errorMessage = content;
break;
}
if (attempts === maxAttempts) {
this.result.output = `Encountered an error while attempting to respond. Error: ${err.message}`;
this.result.intermediateSteps = this.actions;
@ -408,7 +425,7 @@ Only respond with your conversational reply to the following User Message:
if (this.agentOptions.skipCompletion && this.result.output) {
responseMessage.text = this.result.output;
this.addImages(this.result.intermediateSteps, responseMessage);
await this.generateTextStream(this.result.output, opts.onProgress);
await this.generateTextStream(this.result.output, opts.onProgress, { delay: 8 });
return await this.handleResponseMessage(responseMessage, saveOptions, user);
}

View file

@ -0,0 +1,18 @@
{
"schema_version": "v1",
"name_for_human": "Ai PDF",
"name_for_model": "Ai_PDF",
"description_for_human": "Super-fast, interactive chats with PDFs of any size, complete with page references for fact checking.",
"description_for_model": "Provide a URL to a PDF and search the document. Break the user question in multiple semantic search queries and calls as needed. Think step by step.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/logo.png",
"contact_email": "support@promptapps.ai",
"legal_info_url": "https://plugin-3c56b9d4c8a6465998395f28b6a445b2-jexkai4vea-uc.a.run.app/legal.html"
}

View file

@ -0,0 +1,22 @@
{
"schema_version": "v1",
"name_for_human": "VoxScript",
"name_for_model": "VoxScript",
"description_for_human": "Enables searching of YouTube transcripts, financial data sources Google Search results, and more!",
"description_for_model": "Plugin for searching through varius data sources.",
"auth": {
"type": "service_http",
"authorization_type": "bearer",
"verification_tokens": {
"openai": "ffc5226d1af346c08a98dee7deec9f76"
}
},
"api": {
"type": "openapi",
"url": "https://voxscript.awt.icu/swagger/v1/swagger.yaml",
"is_user_authenticated": false
},
"logo_url": "https://voxscript.awt.icu/images/VoxScript_logo_32x32.png",
"contact_email": "voxscript@allwiretech.com",
"legal_info_url": "https://voxscript.awt.icu/legal/"
}

View file

@ -0,0 +1,18 @@
{
"schema_version": "v1",
"name_for_model": "askyourpdf",
"name_for_human": "AskYourPDF",
"description_for_model": "This plugin is designed to expedite the extraction of information from PDF documents. It works by accepting a URL link to a PDF or a document ID (doc_id) from the user. If a URL is provided, the plugin first validates that it is a correct URL. \\nAfter validating the URL, the plugin proceeds to download the PDF and store its content in a vector database. If the user provides a doc_id, the plugin directly retrieves the document from the database. The plugin then scans through the stored PDFs to find answers to user queries or retrieve specific details.\\n\\nHowever, if an error occurs while querying the API, the user is prompted to download their document first, then manually upload it to [![Upload Document](https://raw.githubusercontent.com/AskYourPdf/ask-plugin/main/upload.png)](https://askyourpdf.com/upload). Once the upload is complete, the user should copy the resulting doc_id and paste it back into the chat for further interaction.\nThe plugin is particularly useful when the user's question pertains to content within a PDF document. When providing answers, the plugin also specifies the page number (highlighted in bold) where the relevant information was found. Remember, the URL must be valid for a successful query. Failure to validate the URL may lead to errors or unsuccessful queries.",
"description_for_human": "Unlock the power of your PDFs!, dive into your documents, find answers, and bring information to your fingertips.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "askyourpdf.yaml",
"has_user_authentication": false
},
"logo_url": "https://plugin.askyourpdf.com/.well-known/logo.png",
"contact_email": "plugin@askyourpdf.com",
"legal_info_url": "https://askyourpdf.com/terms"
}

View file

@ -0,0 +1,18 @@
{
"schema_version": "v1",
"name_for_human": "Scholarly Graph Link",
"name_for_model": "scholarly_graph_link",
"description_for_human": "You can search papers, authors, datasets and software. It has access to Figshare, Arxiv, and many others.",
"description_for_model": "Run GraphQL queries against an API hosted by DataCite API. The API supports most GraphQL query but does not support mutations statements. Use `{ __schema { types { name kind } } }` to get all the types in the GraphQL schema. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{ datasets { nodes { id sizes citations { nodes { id titles { title } } } } } }` to get all the citations of all datasets in the API. Use `{person(id:ORCID) {works(first:50) {nodes {id titles(first: 1){title} publicationYear}}}}` to get the first 50 works of a person based on their ORCID. All Ids are urls, e.g., https://orcid.org/0012-0000-1012-1110. Mutations statements are not allowed.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://api.datacite.org/graphql-openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://raw.githubusercontent.com/kjgarza/scholarly_graph_link/master/logo.png",
"contact_email": "kj.garza@gmail.com",
"legal_info_url": "https://github.com/kjgarza/scholarly_graph_link/blob/master/LICENSE"
}

View file

@ -0,0 +1,24 @@
{
"schema_version": "v1",
"name_for_human": "WebPilot",
"name_for_model": "web_pilot",
"description_for_human": "Browse & QA Webpage/PDF/Data. Generate articles, from one or more URLs.",
"description_for_model": "This tool allows users to provide a URL(or URLs) and optionally requests for interacting with, extracting specific information or how to do with the content from the URL. Requests may include rewrite, translate, and others. If there any requests, when accessing the /api/visit-web endpoint, the parameter 'user_has_request' should be set to 'true. And if there's no any requests, 'user_has_request' should be set to 'false'.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://webreader.webpilotai.com/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://webreader.webpilotai.com/logo.png",
"contact_email": "dev@webpilot.ai",
"legal_info_url": "https://webreader.webpilotai.com/legal_info.html",
"headers": {
"id": "WebPilot-Friend-UID"
},
"params": {
"user_has_request": true
}
}

View file

@ -0,0 +1,157 @@
openapi: 3.0.2
info:
title: FastAPI
version: 0.1.0
servers:
- url: https://plugin.askyourpdf.com
paths:
/api/download_pdf:
post:
summary: Download Pdf
description: Download a PDF file from a URL and save it to the vector database.
operationId: download_pdf_api_download_pdf_post
parameters:
- required: true
schema:
title: Url
type: string
name: url
in: query
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/FileResponse'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/query:
post:
summary: Perform Query
description: Perform a query on a document.
operationId: perform_query_query_post
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InputData'
required: true
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseModel'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
DocumentMetadata:
title: DocumentMetadata
required:
- source
- page_number
- author
type: object
properties:
source:
title: Source
type: string
page_number:
title: Page Number
type: integer
author:
title: Author
type: string
FileResponse:
title: FileResponse
required:
- docId
type: object
properties:
docId:
title: Docid
type: string
error:
title: Error
type: string
HTTPValidationError:
title: HTTPValidationError
type: object
properties:
detail:
title: Detail
type: array
items:
$ref: '#/components/schemas/ValidationError'
InputData:
title: InputData
required:
- doc_id
- query
type: object
properties:
doc_id:
title: Doc Id
type: string
query:
title: Query
type: string
ResponseModel:
title: ResponseModel
required:
- results
type: object
properties:
results:
title: Results
type: array
items:
$ref: '#/components/schemas/SearchResult'
SearchResult:
title: SearchResult
required:
- doc_id
- text
- metadata
type: object
properties:
doc_id:
title: Doc Id
type: string
text:
title: Text
type: string
metadata:
$ref: '#/components/schemas/DocumentMetadata'
ValidationError:
title: ValidationError
required:
- loc
- msg
- type
type: object
properties:
loc:
title: Location
type: array
items:
anyOf:
- type: string
- type: integer
msg:
title: Message
type: string
type:
title: Error Type
type: string

View file

@ -0,0 +1,185 @@
openapi: 3.0.1
info:
title: ScholarAI
description: Allows the user to search facts and findings from scientific articles
version: 'v1'
servers:
- url: https://scholar-ai.net
paths:
/api/abstracts:
get:
operationId: searchAbstracts
summary: Get relevant paper abstracts by keywords search
parameters:
- name: keywords
in: query
description: Keywords of inquiry which should appear in article. Must be in English.
required: true
schema:
type: string
- name: sort
in: query
description: The sort order for results. Valid values are cited_by_count or publication_date. Excluding this value does a relevance based search.
required: false
schema:
type: string
enum:
- cited_by_count
- publication_date
- name: query
in: query
description: The user query
required: true
schema:
type: string
- name: peer_reviewed_only
in: query
description: Whether to only return peer reviewed articles. Defaults to true, ChatGPT should cautiously suggest this value can be set to false
required: false
schema:
type: string
- name: start_year
in: query
description: The first year, inclusive, to include in the search range. Excluding this value will include all years.
required: false
schema:
type: string
- name: end_year
in: query
description: The last year, inclusive, to include in the search range. Excluding this value will include all years.
required: false
schema:
type: string
- name: offset
in: query
description: The offset of the first result to return. Defaults to 0.
required: false
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/searchAbstractsResponse'
/api/fulltext:
get:
operationId: getFullText
summary: Get full text of a paper by URL for PDF
parameters:
- name: pdf_url
in: query
description: URL for PDF
required: true
schema:
type: string
- name: chunk
in: query
description: chunk number to retrieve, defaults to 1
required: false
schema:
type: number
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/getFullTextResponse'
/api/save-citation:
get:
operationId: saveCitation
summary: Save citation to reference manager
parameters:
- name: doi
in: query
description: Digital Object Identifier (DOI) of article
required: true
schema:
type: string
- name: zotero_user_id
in: query
description: Zotero User ID
required: true
schema:
type: string
- name: zotero_api_key
in: query
description: Zotero API Key
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/saveCitationResponse'
components:
schemas:
searchAbstractsResponse:
type: object
properties:
next_offset:
type: number
description: The offset of the next page of results.
total_num_results:
type: number
description: The total number of results.
abstracts:
type: array
items:
type: object
properties:
title:
type: string
abstract:
type: string
description: Summary of the context, methods, results, and conclusions of the paper.
doi:
type: string
description: The DOI of the paper.
landing_page_url:
type: string
description: Link to the paper on its open-access host.
pdf_url:
type: string
description: Link to the paper PDF.
publicationDate:
type: string
description: The date the paper was published in YYYY-MM-DD format.
relevance:
type: number
description: The relevance of the paper to the search query. 1 is the most relevant.
creators:
type: array
items:
type: string
description: The name of the creator.
cited_by_count:
type: number
description: The number of citations of the article.
description: The list of relevant abstracts.
getFullTextResponse:
type: object
properties:
full_text:
type: string
description: The full text of the paper.
pdf_url:
type: string
description: The PDF URL of the paper.
chunk:
type: number
description: The chunk of the paper.
total_chunk_num:
type: number
description: The total chunks of the paper.
saveCitationResponse:
type: object
properties:
message:
type: string
description: Confirmation of successful save or error message.

View file

@ -0,0 +1,18 @@
{
"schema_version": "v1",
"name_for_human": "Prompt Perfect",
"name_for_model": "rephrase",
"description_for_human": "Type 'perfect' to craft the perfect prompt, every time.",
"description_for_model": "Plugin that can rephrase user inputs to improve the quality of ChatGPT's responses. The plugin evaluates user inputs and, if necessary, transforms them into clearer, more specific, and contextual prompts. It processes a JSON object containing the user input to be rephrased and uses the GPT-3.5-turbo model for the rephrasing process. The rephrased input is then returned as raw data to be incorporated into ChatGPT's response. The user can initiate the plugin by typing 'perfect'.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "https://promptperfect.xyz/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "https://promptperfect.xyz/static/prompt_perfect_logo.png",
"contact_email": "heyo@promptperfect.xyz",
"legal_info_url": "https://promptperfect.xyz/static/terms.html"
}

View file

@ -0,0 +1,22 @@
{
"schema_version": "v1",
"name_for_human": "ScholarAI",
"name_for_model": "scholarai",
"description_for_human": "Unleash scientific research: search 40M+ peer-reviewed papers, explore scientific PDFs, and save to reference managers.",
"description_for_model": "Access open access scientific literature from peer-reviewed journals. The abstract endpoint finds relevant papers based on 2 to 6 keywords. After getting abstracts, ALWAYS prompt the user offering to go into more detail. Use the fulltext endpoint to retrieve the entire paper's text and access specific details using the provided pdf_url, if available. ALWAYS hyperlink the pdf_url from the responses if available. Offer to dive into the fulltext or search for additional papers. Always ask if the user wants save any paper to the users Zotero reference manager by using the save-citation endpoint and providing the doi and requesting the users zotero_user_id and zotero_api_key.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "scholarai.yaml",
"is_user_authenticated": false
},
"params": {
"sort": "cited_by_count"
},
"logo_url": "https://scholar-ai.net/logo.png",
"contact_email": "lakshb429@gmail.com",
"legal_info_url": "https://scholar-ai.net/legal.txt",
"HttpAuthorizationType": "basic"
}

View file

@ -0,0 +1,139 @@
require('dotenv').config();
const { z } = require('zod');
const fs = require('fs');
const yaml = require('js-yaml');
const path = require('path');
const { DynamicStructuredTool } = require('langchain/tools');
const { createOpenAPIChain } = require('langchain/chains');
const SUFFIX = 'Prioritize using responses for subsequent requests to better fulfill the query.';
const AuthBearer = z
.object({
type: z.string().includes('service_http'),
authorization_type: z.string().includes('bearer'),
verification_tokens: z.object({
openai: z.string(),
}),
})
.catch(() => false);
const AuthDefinition = z
.object({
type: z.string(),
authorization_type: z.string(),
verification_tokens: z.object({
openai: z.string(),
}),
})
.catch(() => false);
async function readSpecFile(filePath) {
try {
const fileContents = await fs.promises.readFile(filePath, 'utf8');
if (path.extname(filePath) === '.json') {
return JSON.parse(fileContents);
}
return yaml.load(fileContents);
} catch (e) {
console.error(e);
return false;
}
}
async function getSpec(url) {
const RegularUrl = z
.string()
.url()
.catch(() => false);
if (RegularUrl.parse(url) && path.extname(url) === '.json') {
const response = await fetch(url);
return await response.json();
}
const ValidSpecPath = z
.string()
.url()
.catch(async () => {
const spec = path.join(__dirname, '..', '.well-known', 'openapi', url);
if (!fs.existsSync(spec)) {
return false;
}
return await readSpecFile(spec);
});
return ValidSpecPath.parse(url);
}
async function createOpenAPIPlugin({ data, llm, user, message, verbose = false }) {
let spec;
try {
spec = await getSpec(data.api.url, verbose);
} catch (error) {
verbose && console.debug('getSpec error', error);
return null;
}
if (!spec) {
verbose && console.debug('No spec found');
return null;
}
const headers = {};
const { auth, description_for_model } = data;
if (auth && AuthDefinition.parse(auth)) {
verbose && console.debug('auth detected', auth);
const { openai } = auth.verification_tokens;
if (AuthBearer.parse(auth)) {
headers.authorization = `Bearer ${openai}`;
verbose && console.debug('added auth bearer', headers);
}
}
return new DynamicStructuredTool({
name: data.name_for_model,
description: `${data.description_for_human} ${SUFFIX}`,
schema: z.object({
query: z
.string()
.describe(
'For the query, be specific in a conversational manner. It will be interpreted by a human.',
),
}),
func: async () => {
const chainOptions = {
llm,
verbose,
};
if (data.headers && data.headers['librechat_user_id']) {
verbose && console.debug('id detected', headers);
headers[data.headers['librechat_user_id']] = user;
}
if (Object.keys(headers).length > 0) {
verbose && console.debug('headers detected', headers);
chainOptions.headers = headers;
}
if (data.params) {
verbose && console.debug('params detected', data.params);
chainOptions.params = data.params;
}
const chain = await createOpenAPIChain(spec, chainOptions);
const result = await chain.run(
`${message}\n\n||>Instructions: ${description_for_model}\n${SUFFIX}`,
);
console.log('api chain run result', result);
return result;
},
});
}
module.exports = {
getSpec,
readSpecFile,
createOpenAPIPlugin,
};

View file

@ -0,0 +1,65 @@
const fs = require('fs');
const { createOpenAPIPlugin, getSpec, readSpecFile } = require('./OpenAPIPlugin');
jest.mock('node-fetch');
jest.mock('fs', () => ({
promises: {
readFile: jest.fn(),
},
existsSync: jest.fn(),
}));
describe('readSpecFile', () => {
it('reads JSON file correctly', async () => {
fs.promises.readFile.mockResolvedValue(JSON.stringify({ test: 'value' }));
const result = await readSpecFile('test.json');
expect(result).toEqual({ test: 'value' });
});
it('reads YAML file correctly', async () => {
fs.promises.readFile.mockResolvedValue('test: value');
const result = await readSpecFile('test.yaml');
expect(result).toEqual({ test: 'value' });
});
it('handles error correctly', async () => {
fs.promises.readFile.mockRejectedValue(new Error('test error'));
const result = await readSpecFile('test.json');
expect(result).toBe(false);
});
});
describe('getSpec', () => {
it('fetches spec from url correctly', async () => {
const parsedJson = await getSpec('https://www.instacart.com/.well-known/ai-plugin.json');
const isObject = typeof parsedJson === 'object';
expect(isObject).toEqual(true);
});
it('reads spec from file correctly', async () => {
fs.existsSync.mockReturnValue(true);
fs.promises.readFile.mockResolvedValue(JSON.stringify({ test: 'value' }));
const result = await getSpec('test.json');
expect(result).toEqual({ test: 'value' });
});
it('returns false when file does not exist', async () => {
fs.existsSync.mockReturnValue(false);
const result = await getSpec('test.json');
expect(result).toBe(false);
});
});
describe('createOpenAPIPlugin', () => {
it('returns null when getSpec throws an error', async () => {
const result = await createOpenAPIPlugin({ data: { api: { url: 'invalid' } } });
expect(result).toBe(null);
});
it('returns null when no spec is found', async () => {
const result = await createOpenAPIPlugin({});
expect(result).toBe(null);
});
// Add more tests here for different scenarios
});

View file

@ -32,7 +32,7 @@
},
{
"name": "Browser",
"pluginKey": "browser",
"pluginKey": "web-browser",
"description": "Scrape and summarize webpage data",
"icon": "/assets/web-browser.svg",
"authConfig": [

View file

@ -0,0 +1,31 @@
const { loadSpecs } = require('./loadSpecs');
function transformSpec(input) {
return {
name: input.name_for_human,
pluginKey: input.name_for_model,
description: input.description_for_human,
icon: input?.logo_url ?? 'https://placehold.co/70x70.png',
// TODO: add support for authentication
isAuthRequired: 'false',
authConfig: [],
};
}
async function addOpenAPISpecs(availableTools) {
try {
const specs = (await loadSpecs({})).map(transformSpec);
if (specs.length > 0) {
return [...specs, ...availableTools];
}
return availableTools;
} catch (error) {
console.log('addOpenAPISpecs error', error);
return availableTools;
}
}
module.exports = {
transformSpec,
addOpenAPISpecs,
};

View file

@ -0,0 +1,76 @@
const { addOpenAPISpecs, transformSpec } = require('./addOpenAPISpecs');
const { loadSpecs } = require('./loadSpecs');
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
jest.mock('./loadSpecs');
jest.mock('../dynamic/OpenAPIPlugin');
describe('transformSpec', () => {
it('should transform input spec to a desired format', () => {
const input = {
name_for_human: 'Human Name',
name_for_model: 'Model Name',
description_for_human: 'Human Description',
logo_url: 'https://example.com/logo.png',
};
const expectedOutput = {
name: 'Human Name',
pluginKey: 'Model Name',
description: 'Human Description',
icon: 'https://example.com/logo.png',
isAuthRequired: 'false',
authConfig: [],
};
expect(transformSpec(input)).toEqual(expectedOutput);
});
it('should use default icon if logo_url is not provided', () => {
const input = {
name_for_human: 'Human Name',
name_for_model: 'Model Name',
description_for_human: 'Human Description',
};
const expectedOutput = {
name: 'Human Name',
pluginKey: 'Model Name',
description: 'Human Description',
icon: 'https://placehold.co/70x70.png',
isAuthRequired: 'false',
authConfig: [],
};
expect(transformSpec(input)).toEqual(expectedOutput);
});
});
describe('addOpenAPISpecs', () => {
it('should add specs to available tools', async () => {
const availableTools = ['Tool1', 'Tool2'];
const specs = [
{
name_for_human: 'Human Name',
name_for_model: 'Model Name',
description_for_human: 'Human Description',
logo_url: 'https://example.com/logo.png',
},
];
loadSpecs.mockResolvedValue(specs);
createOpenAPIPlugin.mockReturnValue('Plugin');
const result = await addOpenAPISpecs(availableTools);
expect(result).toEqual([...specs.map(transformSpec), ...availableTools]);
});
it('should return available tools if specs loading fails', async () => {
const availableTools = ['Tool1', 'Tool2'];
loadSpecs.mockRejectedValue(new Error('Failed to load specs'));
const result = await addOpenAPISpecs(availableTools);
expect(result).toEqual(availableTools);
});
});

View file

@ -16,6 +16,7 @@ const {
StableDiffusionAPI,
StructuredSD,
} = require('../');
const { loadSpecs } = require('./loadSpecs');
const validateTools = async (user, tools = []) => {
try {
@ -80,7 +81,7 @@ const loadTools = async ({ user, model, functions = null, tools = [], options =
};
const customConstructors = {
browser: async () => {
'web-browser': async () => {
let openAIApiKey = options.openAIApiKey ?? process.env.OPENAI_API_KEY;
openAIApiKey = openAIApiKey === 'user_provided' ? null : openAIApiKey;
openAIApiKey = openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY'));
@ -117,6 +118,17 @@ const loadTools = async ({ user, model, functions = null, tools = [], options =
};
const requestedTools = {};
let specs = null;
if (functions) {
specs = await loadSpecs({
llm: model,
user,
message: options.message,
map: true,
verbose: options?.debug,
});
console.dir(specs, { depth: null });
}
const toolOptions = {
serpapi: { location: 'Austin,Texas,United States', hl: 'en', gl: 'us' },
@ -138,6 +150,11 @@ const loadTools = async ({ user, model, functions = null, tools = [], options =
continue;
}
if (specs && specs[tool]) {
requestedTools[tool] = specs[tool];
continue;
}
if (toolConstructors[tool]) {
const options = toolOptions[tool] || {};
const toolInstance = await loadToolWithAuth(

View file

@ -0,0 +1,104 @@
const fs = require('fs');
const path = require('path');
const { z } = require('zod');
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
// The minimum Manifest definition
const ManifestDefinition = z.object({
schema_version: z.string().optional(),
name_for_human: z.string(),
name_for_model: z.string(),
description_for_human: z.string(),
description_for_model: z.string(),
auth: z.object({}).optional(),
api: z.object({
// Spec URL or can be the filename of the OpenAPI spec yaml file,
// located in api\app\clients\tools\.well-known\openapi
url: z.string(),
type: z.string().optional(),
is_user_authenticated: z.boolean().nullable().optional(),
has_user_authentication: z.boolean().nullable().optional(),
}),
// use to override any params that the LLM will consistently get wrong
params: z.object({}).optional(),
logo_url: z.string().optional(),
contact_email: z.string().optional(),
legal_info_url: z.string().optional(),
});
function validateJson(json, verbose = true) {
try {
return ManifestDefinition.parse(json);
} catch (error) {
if (verbose) {
console.debug('validateJson error', error);
}
return false;
}
}
// omit the LLM to return the well known jsons as objects
async function loadSpecs({ llm, user, message, map = false, verbose = false }) {
const directoryPath = path.join(__dirname, '..', '.well-known');
const files = (await fs.promises.readdir(directoryPath)).filter(
(file) => path.extname(file) === '.json',
);
const validJsons = [];
const constructorMap = {};
if (verbose) {
console.debug('files', files);
}
for (const file of files) {
if (path.extname(file) === '.json') {
const filePath = path.join(directoryPath, file);
const fileContent = await fs.promises.readFile(filePath, 'utf8');
const json = JSON.parse(fileContent);
if (!validateJson(json)) {
verbose && console.debug('Invalid json', json);
continue;
}
if (llm && map) {
constructorMap[json.name_for_model] = async () =>
await createOpenAPIPlugin({
data: json,
llm,
message,
user,
verbose,
});
continue;
}
if (llm) {
validJsons.push(createOpenAPIPlugin({ data: json, llm, verbose }));
continue;
}
validJsons.push(json);
}
}
if (map) {
return constructorMap;
}
const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin);
// if (verbose) {
// console.debug('plugins', plugins);
// console.debug(plugins[0].name);
// }
return plugins;
}
module.exports = {
loadSpecs,
validateJson,
ManifestDefinition,
};

View file

@ -0,0 +1,101 @@
const fs = require('fs');
const { validateJson, loadSpecs, ManifestDefinition } = require('./loadSpecs');
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
jest.mock('../dynamic/OpenAPIPlugin');
describe('ManifestDefinition', () => {
it('should validate correct json', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 'http://test.com',
},
};
expect(() => ManifestDefinition.parse(json)).not.toThrow();
});
it('should not validate incorrect json', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 123, // incorrect type
},
};
expect(() => ManifestDefinition.parse(json)).toThrow();
});
});
describe('validateJson', () => {
it('should return parsed json if valid', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 'http://test.com',
},
};
expect(validateJson(json)).toEqual(json);
});
it('should return false if json is not valid', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 123, // incorrect type
},
};
expect(validateJson(json)).toEqual(false);
});
});
describe('loadSpecs', () => {
beforeEach(() => {
jest.spyOn(fs.promises, 'readdir').mockResolvedValue(['test.json']);
jest.spyOn(fs.promises, 'readFile').mockResolvedValue(
JSON.stringify({
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 'http://test.com',
},
}),
);
createOpenAPIPlugin.mockResolvedValue({});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return plugins', async () => {
const plugins = await loadSpecs({ llm: true, verbose: false });
expect(plugins).toHaveLength(1);
expect(createOpenAPIPlugin).toHaveBeenCalledTimes(1);
});
it('should return constructorMap if map is true', async () => {
const plugins = await loadSpecs({ llm: {}, map: true, verbose: false });
expect(plugins).toHaveProperty('Test');
expect(createOpenAPIPlugin).not.toHaveBeenCalled();
});
});

View file

@ -6,7 +6,9 @@ const migrateToStrictFollowParentMessageIdChain = async () => {
try {
const conversations = await Conversation.find({ endpoint: null, model: null }).exec();
if (!conversations || conversations.length === 0) return { noNeed: true };
if (!conversations || conversations.length === 0) {
return { noNeed: true };
}
console.log('Migration: To strict follow the parentMessageId chain.');
@ -64,7 +66,9 @@ const migrateToSupportBetterCustomization = async () => {
try {
const conversations = await Conversation.find({ endpoint: null }).exec();
if (!conversations || conversations.length === 0) return { noNeed: true };
if (!conversations || conversations.length === 0) {
return { noNeed: true };
}
console.log('Migration: To support better customization.');
@ -112,7 +116,9 @@ async function migrateDb() {
const isMigrated = !!ret.find((element) => !element?.noNeed);
if (!isMigrated) console.log('[Migrate] Nothing to migrate');
if (!isMigrated) {
console.log('[Migrate] Nothing to migrate');
}
}
module.exports = migrateDb;

View file

@ -3,7 +3,9 @@ const citationRegex = /\[\^\d+?\^\]/g;
const citeText = (res, noLinks = false) => {
let result = res.text || res;
const citations = Array.from(new Set(result.match(citationRegex)));
if (citations?.length === 0) return result;
if (citations?.length === 0) {
return result;
}
if (noLinks) {
citations.forEach((citation) => {
@ -16,7 +18,9 @@ const citeText = (res, noLinks = false) => {
}
let sources = res.details.sourceAttributions;
if (sources?.length === 0) return result;
if (sources?.length === 0) {
return result;
}
sources = sources.map((source) => source.seeMoreUrl);
citations.forEach((citation) => {

View file

@ -4,9 +4,13 @@ const regex = / \[.*?]\(.*?\)/g;
const getCitations = (res) => {
const adaptiveCards = res.details.adaptiveCards;
const textBlocks = adaptiveCards && adaptiveCards[0].body;
if (!textBlocks) return '';
if (!textBlocks) {
return '';
}
let links = textBlocks[textBlocks.length - 1]?.text.match(regex);
if (links?.length === 0 || !links) return '';
if (links?.length === 0 || !links) {
return '';
}
links = links.map((link) => link.trim());
return links.join('\n - ');
};

View file

@ -4,7 +4,9 @@ const cleanUpPrimaryKeyValue = (value) => {
};
function replaceSup(text) {
if (!text.includes('<sup>')) return text;
if (!text.includes('<sup>')) {
return text;
}
const replacedText = text.replace(/<sup>/g, '^').replace(/\s+<\/sup>/g, '^');
return replacedText;
}

View file

@ -56,11 +56,7 @@ module.exports = {
async updateMessage(message) {
try {
const { messageId, ...update } = message;
const updatedMessage = await Message.findOneAndUpdate(
{ messageId },
update,
{ new: true },
);
const updatedMessage = await Message.findOneAndUpdate({ messageId }, update, { new: true });
if (!updatedMessage) {
throw new Error('Message not found.');

View file

@ -145,7 +145,9 @@ userSchema.methods.generateRefreshToken = function () {
userSchema.methods.comparePassword = function (candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
if (err) return callback(err);
if (err) {
return callback(err);
}
callback(null, isMatch);
});
};
@ -153,8 +155,11 @@ userSchema.methods.comparePassword = function (candidatePassword, callback) {
module.exports.hashPassword = async (password) => {
const hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, 10, function (err, hash) {
if (err) reject(err);
else resolve(hash);
if (err) {
reject(err);
} else {
resolve(hash);
}
});
});

View file

@ -1,4 +1,10 @@
const { getMessages, saveMessage, updateMessage, deleteMessagesSince, deleteMessages } = require('./Message');
const {
getMessages,
saveMessage,
updateMessage,
deleteMessagesSince,
deleteMessages,
} = require('./Message');
const { getConvoTitle, getConvo, saveConvo } = require('./Conversation');
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');

View file

@ -8,7 +8,9 @@ const meiliEnabled = process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY && s
const validateOptions = function (options) {
const requiredKeys = ['host', 'apiKey', 'indexName'];
requiredKeys.forEach((key) => {
if (!options[key]) throw new Error(`Missing mongoMeili Option: ${key}`);
if (!options[key]) {
throw new Error(`Missing mongoMeili Option: ${key}`);
}
});
};
@ -96,12 +98,12 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
if (object.conversationId && object.conversationId.includes('|')) {
object.conversationId = object.conversationId.replace(/\|/g, '--');
}
return object
return object;
}
// Push new document to Meili
async addObjectToMeili() {
const object = this.preprocessObjectForIndex()
const object = this.preprocessObjectForIndex();
try {
// console.log('Adding document to Meili', object);
await index.addDocuments([object]);
@ -228,7 +230,9 @@ module.exports = function mongoMeili(schema, options) {
return next();
} catch (error) {
if (meiliEnabled) {
console.log('[Meilisearch] There was an issue deleting conversation indexes upon deletion, next startup may be slow due to syncing');
console.log(
'[Meilisearch] There was an issue deleting conversation indexes upon deletion, next startup may be slow due to syncing',
);
console.error(error);
}
return next();

View file

@ -43,7 +43,7 @@
"jsonwebtoken": "^9.0.0",
"keyv": "^4.5.2",
"keyv-file": "^0.2.0",
"langchain": "^0.0.103",
"langchain": "^0.0.109",
"lodash": "^4.17.21",
"meilisearch": "^0.33.0",
"mongoose": "^7.1.1",

View file

@ -1,8 +1,4 @@
const {
registerUser,
requestPasswordReset,
resetPassword,
} = require('../services/auth.service');
const { registerUser, requestPasswordReset, resetPassword } = require('../services/auth.service');
const isProduction = process.env.NODE_ENV === 'production';

View file

@ -25,8 +25,12 @@ const handleValidationError = (err, res) => {
module.exports = (err, req, res, next) => {
try {
console.log('congrats you hit the error middleware');
if (err.name === 'ValidationError') return (err = handleValidationError(err, res));
if (err.code && err.code == 11000) return (err = handleDuplicateKeyError(err, res));
if (err.name === 'ValidationError') {
return (err = handleValidationError(err, res));
}
if (err.code && err.code == 11000) {
return (err = handleDuplicateKeyError(err, res));
}
} catch (err) {
res.status(500).send('An unknown error occurred.');
}

View file

@ -1,6 +1,6 @@
// const { getAvailableToolsService } = require('../services/PluginService');
const fs = require('fs');
const { promises: fs } = require('fs');
const path = require('path');
const { addOpenAPISpecs } = require('../../app/clients/tools/util/addOpenAPISpecs');
const filterUniquePlugins = (plugins) => {
const seen = new Set();
@ -27,26 +27,22 @@ const isPluginAuthenticated = (plugin) => {
const getAvailablePluginsController = async (req, res) => {
try {
fs.readFile(
const manifestFile = await fs.readFile(
path.join(__dirname, '..', '..', 'app', 'clients', 'tools', 'manifest.json'),
'utf8',
(err, data) => {
if (err) {
res.status(500).json({ message: err.message });
} else {
const jsonData = JSON.parse(data);
const uniquePlugins = filterUniquePlugins(jsonData);
const authenticatedPlugins = uniquePlugins.map((plugin) => {
if (isPluginAuthenticated(plugin)) {
return { ...plugin, authenticated: true };
} else {
return plugin;
}
});
res.status(200).json(authenticatedPlugins);
}
},
);
const jsonData = JSON.parse(manifestFile);
const uniquePlugins = filterUniquePlugins(jsonData);
const authenticatedPlugins = uniquePlugins.map((plugin) => {
if (isPluginAuthenticated(plugin)) {
return { ...plugin, authenticated: true };
} else {
return plugin;
}
});
const plugins = await addOpenAPISpecs(authenticatedPlugins);
res.status(200).json(plugins);
} catch (error) {
res.status(500).json({ message: error.message });
}

View file

@ -2,12 +2,11 @@ const User = require('../../../models/User');
const loginController = async (req, res) => {
try {
const user = await User.findById(
req.user._id,
);
const user = await User.findById(req.user._id);
// If user doesn't exist, return error
if (!user) { // typeof user !== User) { // this doesn't seem to resolve the User type ??
if (!user) {
// typeof user !== User) { // this doesn't seem to resolve the User type ??
return res.status(400).json({ message: 'Invalid credentials' });
}
@ -15,15 +14,11 @@ const loginController = async (req, res) => {
const expires = eval(process.env.SESSION_EXPIRY);
// Add token to cookie
res.cookie(
'token',
token,
{
expires: new Date(Date.now() + expires),
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
},
);
res.cookie('token', token, {
expires: new Date(Date.now() + expires),
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
});
return res.status(200).send({ token, user });
} catch (err) {

View file

@ -9,7 +9,6 @@ const logoutController = async (req, res) => {
res.clearCookie('token');
res.clearCookie('refreshToken');
return res.status(status).send({ message });
} catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });

View file

@ -33,7 +33,9 @@ config.validate(); // Validate the config
app.use(cors());
if (!process.env.ALLOW_SOCIAL_LOGIN) {
console.warn('Social logins are disabled. Set Envrionment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.')
console.warn(
'Social logins are disabled. Set Envrionment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.',
);
}
// OAUTH
@ -52,14 +54,20 @@ config.validate(); // Validate the config
if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) {
require('../strategies/discordStrategy');
}
if (process.env.OPENID_CLIENT_ID && process.env.OPENID_CLIENT_SECRET &&
process.env.OPENID_ISSUER && process.env.OPENID_SCOPE &&
process.env.OPENID_SESSION_SECRET) {
app.use(session({
secret: process.env.OPENID_SESSION_SECRET,
resave: false,
saveUninitialized: false,
}));
if (
process.env.OPENID_CLIENT_ID &&
process.env.OPENID_CLIENT_SECRET &&
process.env.OPENID_ISSUER &&
process.env.OPENID_SCOPE &&
process.env.OPENID_SESSION_SECRET
) {
app.use(
session({
secret: process.env.OPENID_SESSION_SECRET,
resave: false,
saveUninitialized: false,
}),
);
app.use(passport.session());
require('../strategies/openidStrategy');
}
@ -84,12 +92,13 @@ config.validate(); // Validate the config
});
app.listen(port, host, () => {
if (host == '0.0.0.0')
if (host == '0.0.0.0') {
console.log(
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`,
);
else
} else {
console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
}
});
})();

View file

@ -31,16 +31,16 @@ describe.skip('GET /', () => {
process.env.APP_TITLE = 'Test Title';
process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id';
process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret';
process.env.OPENID_CLIENT_ID= 'Test OpenID Id';
process.env.OPENID_CLIENT_SECRET= 'Test OpenID Secret';
process.env.OPENID_ISSUER= 'Test OpenID Issuer';
process.env.OPENID_SESSION_SECRET= 'Test Secret';
process.env.OPENID_BUTTON_LABEL= 'Test OpenID';
process.env.OPENID_AUTH_URL= 'http://test-server.com';
process.env.OPENID_CLIENT_ID = 'Test OpenID Id';
process.env.OPENID_CLIENT_SECRET = 'Test OpenID Secret';
process.env.OPENID_ISSUER = 'Test OpenID Issuer';
process.env.OPENID_SESSION_SECRET = 'Test Secret';
process.env.OPENID_BUTTON_LABEL = 'Test OpenID';
process.env.OPENID_AUTH_URL = 'http://test-server.com';
process.env.GITHUB_CLIENT_ID = 'Test Github client Id';
process.env.GITHUB_CLIENT_SECRET= 'Test Github client Secret';
process.env.GITHUB_CLIENT_SECRET = 'Test Github client Secret';
process.env.DISCORD_CLIENT_ID = 'Test Discord client Id';
process.env.DISCORD_CLIENT_SECRET= 'Test Discord client Secret';
process.env.DISCORD_CLIENT_SECRET = 'Test Discord client Secret';
process.env.DOMAIN_SERVER = 'http://test-server.com';
process.env.ALLOW_REGISTRATION = 'true';
process.env.ALLOW_SOCIAL_LOGIN = 'true';

View file

@ -15,8 +15,12 @@ router.post('/abort', requireJwtAuth, async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const { endpoint, text, parentMessageId, conversationId: oldConversationId } = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'anthropic') return handleError(res, { text: 'Illegal request' });
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
if (endpoint !== 'anthropic') {
return handleError(res, { text: 'Illegal request' });
}
const endpointOption = {
promptPrefix: req.body?.promptPrefix ?? null,
@ -117,7 +121,7 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
const onStart = (userMessage) => {
sendMessage(res, { message: userMessage, created: true });
abortControllers.set(userMessage.conversationId, { abortController, ...endpointOption });
}
};
const client = new AnthropicClient(endpointOption.token);

View file

@ -15,8 +15,12 @@ router.post('/', requireJwtAuth, async (req, res) => {
parentMessageId,
conversationId: oldConversationId,
} = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'chatGPTBrowser') return handleError(res, { text: 'Illegal request' });
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
if (endpoint !== 'chatGPTBrowser') {
return handleError(res, { text: 'Illegal request' });
}
// build user message
const conversationId = oldConversationId || crypto.randomUUID();
@ -167,7 +171,7 @@ const ask = async ({
// First update conversationId if needed
let conversationUpdate = { conversationId: newConversationId, endpoint: 'chatGPTBrowser' };
if (conversationId != newConversationId)
if (conversationId != newConversationId) {
if (isNewConversation) {
// change the conversationId to new one
conversationUpdate = {
@ -182,6 +186,7 @@ const ask = async ({
...endpointOption,
};
}
}
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
@ -191,12 +196,13 @@ const ask = async ({
userMessage.messageId = newUserMassageId;
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
if (!overrideParentMessageId)
if (!overrideParentMessageId) {
await saveMessage({
...userMessage,
messageId: userMessageId,
newMessageId: newUserMassageId,
});
}
userMessageId = newUserMassageId;
sendMessage(res, {

View file

@ -15,8 +15,12 @@ router.post('/', requireJwtAuth, async (req, res) => {
parentMessageId,
conversationId: oldConversationId,
} = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'bingAI') return handleError(res, { text: 'Illegal request' });
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
if (endpoint !== 'bingAI') {
return handleError(res, { text: 'Illegal request' });
}
// build user message
const conversationId = oldConversationId || crypto.randomUUID();
@ -34,7 +38,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
// build endpoint option
let endpointOption = {};
if (req.body?.jailbreak)
if (req.body?.jailbreak) {
endpointOption = {
jailbreak: req.body?.jailbreak ?? false,
jailbreakConversationId: req.body?.jailbreakConversationId ?? null,
@ -43,7 +47,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
toneStyle: req.body?.toneStyle ?? 'creative',
token: req.body?.token ?? null,
};
else
} else {
endpointOption = {
jailbreak: req.body?.jailbreak ?? false,
systemMessage: req.body?.systemMessage ?? null,
@ -54,6 +58,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
toneStyle: req.body?.toneStyle ?? 'creative',
token: req.body?.token ?? null,
};
}
console.log('ask log', {
userMessage,
@ -106,7 +111,9 @@ const ask = async ({
'X-Accel-Buffering': 'no',
});
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
if (preSendRequest) {
sendMessage(res, { message: userMessage, created: true });
}
let lastSavedTimestamp = 0;
const { onProgress: progressCallback, getPartialText } = createOnProgress({
@ -207,12 +214,13 @@ const ask = async ({
userMessage.messageId = newUserMessageId;
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
if (!overrideParentMessageId)
if (!overrideParentMessageId) {
await saveMessage({
...userMessage,
messageId: userMessageId,
newMessageId: newUserMessageId,
});
}
userMessageId = newUserMessageId;
sendMessage(res, {

View file

@ -9,8 +9,12 @@ const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', requireJwtAuth, async (req, res) => {
const { endpoint, text, parentMessageId, conversationId: oldConversationId } = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'google') return handleError(res, { text: 'Illegal request' });
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
if (endpoint !== 'google') {
return handleError(res, { text: 'Illegal request' });
}
// build endpoint option
const endpointOption = {

View file

@ -20,8 +20,12 @@ router.post('/abort', requireJwtAuth, async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const { endpoint, text, parentMessageId, conversationId } = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'gptPlugins') return handleError(res, { text: 'Illegal request' });
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
if (endpoint !== 'gptPlugins') {
return handleError(res, { text: 'Illegal request' });
}
const agentOptions = req.body?.agentOptions ?? {
agent: 'functions',
@ -67,7 +71,15 @@ router.post('/', requireJwtAuth, async (req, res) => {
});
});
const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, conversationId, req, res }) => {
const ask = async ({
text,
endpoint,
endpointOption,
parentMessageId = null,
conversationId,
req,
res,
}) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
@ -100,7 +112,11 @@ const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, con
}
};
const { onProgress: progressCallback, sendIntermediateMessage, getPartialText } = createOnProgress({
const {
onProgress: progressCallback,
sendIntermediateMessage,
getPartialText,
} = createOnProgress({
onProgress: ({ text: partialText }) => {
const currentTimestamp = Date.now();
@ -156,7 +172,7 @@ const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, con
const onStart = (userMessage) => {
sendMessage(res, { message: userMessage, created: true });
abortControllers.set(userMessage.conversationId, { abortController, ...endpointOption });
}
};
endpointOption.tools = await validateTools(user, endpointOption.tools);
const clientOptions = {
@ -179,11 +195,13 @@ const ask = async ({ text, endpoint, endpointOption, parentMessageId = null, con
}
const chatAgent = new PluginsClient(openAIApiKey, clientOptions);
const onAgentAction = (action) => {
const onAgentAction = (action, start = false) => {
const formattedAction = formatAction(action);
plugin.inputs.push(formattedAction);
plugin.latest = formattedAction.plugin;
saveMessage(userMessage);
if (!start) {
saveMessage(userMessage);
}
sendIntermediateMessage(res, { plugin });
// console.log('PLUGIN ACTION', formattedAction);
};

View file

@ -61,7 +61,12 @@ const createOnProgress = ({ onProgress: _onProgress }) => {
};
const sendIntermediateMessage = (res, payload) => {
sendMessage(res, { text: tokens?.length === 0 ? cursor : tokens, message: true, initial: i === 0, ...payload });
sendMessage(res, {
text: tokens?.length === 0 ? cursor : tokens,
message: true,
initial: i === 0,
...payload,
});
i++;
};
@ -92,7 +97,7 @@ const handleText = async (response, bing = false) => {
};
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item);
const getString = (input) => isObject(input) ? JSON.stringify(input) : input ;
const getString = (input) => (isObject(input) ? JSON.stringify(input) : input);
function formatSteps(steps) {
let output = '';
@ -117,20 +122,8 @@ function formatSteps(steps) {
}
function formatAction(action) {
const capitalizeWords = (input) => {
if (input === 'dall-e') {
return 'DALL-E';
}
return input
.replace(/-/g, ' ')
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
const formattedAction = {
plugin: capitalizeWords(action.tool) || action.tool,
plugin: action.tool,
input: getString(action.toolInput),
thought: action.log.includes('Thought: ')
? action.log.split('\n')[0].replace('Thought: ', '')

View file

@ -3,11 +3,7 @@ const router = express.Router();
const { titleConvo, OpenAIClient } = require('../../../app');
const { getAzureCredentials, abortMessage } = require('../../../utils');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const {
handleError,
sendMessage,
createOnProgress,
} = require('./handlers');
const { handleError, sendMessage, createOnProgress } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
const abortControllers = new Map();
@ -18,9 +14,13 @@ router.post('/abort', requireJwtAuth, async (req, res) => {
router.post('/', requireJwtAuth, async (req, res) => {
const { endpoint, text, parentMessageId, conversationId } = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
const isOpenAI = endpoint === 'openAI' || endpoint === 'azureOpenAI';
if (!isOpenAI) return handleError(res, { text: 'Illegal request' });
if (!isOpenAI) {
return handleError(res, { text: 'Illegal request' });
}
// build endpoint option
const endpointOption = {
@ -50,7 +50,15 @@ router.post('/', requireJwtAuth, async (req, res) => {
});
});
const ask = async ({ text, endpointOption, parentMessageId = null, endpoint, conversationId, req, res }) => {
const ask = async ({
text,
endpointOption,
parentMessageId = null,
endpoint,
conversationId,
req,
res,
}) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
@ -166,7 +174,11 @@ const ask = async ({ text, endpointOption, parentMessageId = null, endpoint, con
response.parentMessageId = overrideParentMessageId;
}
console.log('promptTokens, completionTokens:', response.promptTokens, response.completionTokens);
console.log(
'promptTokens, completionTokens:',
response.promptTokens,
response.completionTokens,
);
await saveMessage(response);
sendMessage(res, {

View file

@ -5,14 +5,16 @@ router.get('/', async function (req, res) {
try {
const appTitle = process.env.APP_TITLE || 'LibreChat';
const googleLoginEnabled = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
const openidLoginEnabled = !!process.env.OPENID_CLIENT_ID
&& !!process.env.OPENID_CLIENT_SECRET
&& !!process.env.OPENID_ISSUER
&& !!process.env.OPENID_SESSION_SECRET;
const openidLoginEnabled =
!!process.env.OPENID_CLIENT_ID &&
!!process.env.OPENID_CLIENT_SECRET &&
!!process.env.OPENID_ISSUER &&
!!process.env.OPENID_SESSION_SECRET;
const openidLabel = process.env.OPENID_BUTTON_LABEL || 'Login with OpenID';
const openidImageUrl = process.env.OPENID_IMAGE_URL;
const githubLoginEnabled = !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET;
const discordLoginEnabled = !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET;
const discordLoginEnabled =
!!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET;
const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080';
const registrationEnabled = process.env.ALLOW_REGISTRATION === 'true';
const socialLoginEnabled = process.env.ALLOW_SOCIAL_LOGIN === 'true';
@ -29,7 +31,6 @@ router.get('/', async function (req, res) {
registrationEnabled,
socialLoginEnabled,
});
} catch (err) {
console.error(err);
return res.status(500).send({ error: err.message });

View file

@ -13,8 +13,11 @@ router.get('/:conversationId', requireJwtAuth, async (req, res) => {
const { conversationId } = req.params;
const convo = await getConvo(req.user.id, conversationId);
if (convo) res.status(200).send(convo.toObject());
else res.status(404).end();
if (convo) {
res.status(200).send(convo.toObject());
} else {
res.status(404).end();
}
});
router.post('/clear', requireJwtAuth, async (req, res) => {

View file

@ -1,31 +1,61 @@
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 }) => {
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', 'text-davinci-003' ];
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',
'text-davinci-003',
];
const key = opts.azure ? 'AZURE_OPENAI_MODELS' : 'OPENAI_MODELS';
if (process.env[key]) models = String(process.env[key]).split(',');
if (process.env[key]) {
models = String(process.env[key]).split(',');
}
return models;
};
const getChatGPTBrowserModels = () => {
let models = ['text-davinci-002-render-sha', 'gpt-4'];
if (process.env.CHATGPT_MODELS) models = String(process.env.CHATGPT_MODELS).split(',');
if (process.env.CHATGPT_MODELS) {
models = String(process.env.CHATGPT_MODELS).split(',');
}
return models;
};
const getAnthropicModels = () => {
let models = ['claude-1', 'claude-1-100k', 'claude-instant-1', 'claude-instant-1-100k', 'claude-2'];
if (process.env.ANTHROPIC_MODELS) models = String(process.env.ANTHROPIC_MODELS).split(',');
let models = [
'claude-1',
'claude-1-100k',
'claude-instant-1',
'claude-instant-1-100k',
'claude-2',
];
if (process.env.ANTHROPIC_MODELS) {
models = String(process.env.ANTHROPIC_MODELS).split(',');
}
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(',');
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;
};
@ -50,22 +80,42 @@ router.get('/', async function (req, res) {
}
}
const tools = await addOpenAPISpecs(availableTools);
function transformToolsToMap(tools) {
return tools.reduce((map, obj) => {
map[obj.pluginKey] = obj.name;
return map;
}, {});
}
const plugins = transformToolsToMap(tools);
const google =
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 userProvidedOpenAI = openAIApiKey
? openAIApiKey === 'user_provided'
: azureOpenAIApiKey === 'user_provided';
const openAI = openAIApiKey
? { availableModels: getOpenAIModels(), userProvide: openAIApiKey === 'user_provided' }
: false;
const azureOpenAI = azureOpenAIApiKey
? { availableModels: getOpenAIModels({ azure: true }), userProvide: azureOpenAIApiKey === 'user_provided' }
: false;
const gptPlugins = openAIApiKey || azureOpenAIApiKey
? { availableModels: getPluginModels(), availableTools, availableAgents: ['classic', 'functions'], userProvide: userProvidedOpenAI }
? {
availableModels: getOpenAIModels({ azure: true }),
userProvide: azureOpenAIApiKey === 'user_provided',
}
: false;
const gptPlugins =
openAIApiKey || azureOpenAIApiKey
? {
availableModels: getPluginModels(),
plugins,
availableAgents: ['classic', 'functions'],
userProvide: userProvidedOpenAI,
}
: false;
const bingAI = process.env.BINGAI_TOKEN
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }
: false;
@ -82,7 +132,9 @@ router.get('/', async function (req, res) {
}
: false;
res.send(JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser, gptPlugins, anthropic }));
res.send(
JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser, gptPlugins, anthropic }),
);
});
module.exports = { router, getOpenAIModels, getChatGPTBrowserModels };

View file

@ -33,7 +33,9 @@ router.post('/delete', requireJwtAuth, async (req, res) => {
let filter = {};
const { presetId } = req.body.arg || {};
if (presetId) filter = { presetId };
if (presetId) {
filter = { presetId };
}
console.log('delete preset filter', filter);

View file

@ -110,7 +110,9 @@ const requestPasswordReset = async (email) => {
}
let token = await Token.findOne({ userId: user._id });
if (token) await token.deleteOne();
if (token) {
await token.deleteOne();
}
let resetToken = crypto.randomBytes(32).toString('hex');
const hash = await bcrypt.hashSync(resetToken, 10);

View file

@ -19,7 +19,7 @@ const downloadImage = async (url, imagePath, accessToken) => {
try {
const response = await axios.get(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
Authorization: `Bearer ${accessToken}`,
},
responseType: 'arraybuffer',
});
@ -37,7 +37,7 @@ const downloadImage = async (url, imagePath, accessToken) => {
};
Issuer.discover(process.env.OPENID_ISSUER)
.then(issuer => {
.then((issuer) => {
const client = new issuer.Client({
client_id: process.env.OPENID_CLIENT_ID,
client_secret: process.env.OPENID_CLIENT_SECRET,
@ -96,9 +96,22 @@ Issuer.discover(process.env.OPENID_ISSUER)
fileName = userinfo.sub + '.png';
}
const imagePath = path.join(__dirname, '..', '..', 'client', 'public', 'images', 'openid', fileName);
const imagePath = path.join(
__dirname,
'..',
'..',
'client',
'public',
'images',
'openid',
fileName,
);
const imagePathOrEmpty = await downloadImage(imageUrl, imagePath, tokenset.access_token);
const imagePathOrEmpty = await downloadImage(
imageUrl,
imagePath,
tokenset.access_token,
);
user.avatar = imagePathOrEmpty;
} else {
@ -115,8 +128,7 @@ Issuer.discover(process.env.OPENID_ISSUER)
);
passport.use('openid', openidLogin);
})
.catch(err => {
.catch((err) => {
console.error(err);
});

View file

@ -68,45 +68,65 @@ module.exports = {
setLevel: (l) => (level = l),
log: {
trace: (msg) => {
if (level <= levels.TRACE) return;
if (level <= levels.TRACE) {
return;
}
logger.trace(msg);
},
debug: (msg) => {
if (level <= levels.DEBUG) return;
if (level <= levels.DEBUG) {
return;
}
logger.debug(msg);
},
info: (msg) => {
if (level <= levels.INFO) return;
if (level <= levels.INFO) {
return;
}
logger.info(msg);
},
warn: (msg) => {
if (level <= levels.WARN) return;
if (level <= levels.WARN) {
return;
}
logger.warn(msg);
},
error: (msg) => {
if (level <= levels.ERROR) return;
if (level <= levels.ERROR) {
return;
}
logger.error(msg);
},
fatal: (msg) => {
if (level <= levels.FATAL) return;
if (level <= levels.FATAL) {
return;
}
logger.fatal(msg);
},
// Custom loggers
parameters: (parameters) => {
if (level <= levels.TRACE) return;
if (level <= levels.TRACE) {
return;
}
logger.debug({ parameters }, 'Function Parameters');
},
functionName: (name) => {
if (level <= levels.TRACE) return;
if (level <= levels.TRACE) {
return;
}
logger.debug(`EXECUTING: ${name}`);
},
flow: (flow) => {
if (level <= levels.INFO) return;
if (level <= levels.INFO) {
return;
}
logger.debug(`BEGIN FLOW: ${flow}`);
},
variable: ({ name, value }) => {
if (level <= levels.DEBUG) return;
if (level <= levels.DEBUG) {
return;
}
// Check if the variable name matches any of the redact patterns and redact the value
let sanitizedValue = value;
for (const pattern of redactPatterns) {
@ -118,7 +138,9 @@ module.exports = {
logger.debug({ variable: { name, value: sanitizedValue } }, `VARIABLE ${name}`);
},
request: () => (req, res, next) => {
if (level < levels.DEBUG) return next();
if (level < levels.DEBUG) {
return next();
}
logger.debug({ query: req.query, body: req.body }, `Hit URL ${req.url} with following`);
return next();
},

View file

@ -1,6 +1,6 @@
const genAzureEndpoint = ({ azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName }) => {
return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}`;
}
};
const genAzureChatCompletion = ({
azureOpenAIApiInstanceName,
@ -8,7 +8,7 @@ const genAzureChatCompletion = ({
azureOpenAIApiVersion,
}) => {
return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}/chat/completions?api-version=${azureOpenAIApiVersion}`;
}
};
const getAzureCredentials = () => {
return {
@ -16,7 +16,7 @@ const getAzureCredentials = () => {
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
}
}
};
};
module.exports = { genAzureEndpoint, genAzureChatCompletion, getAzureCredentials };

View file

@ -12,21 +12,29 @@ module.exports = {
setLevel: (l) => (level = l),
log: {
parameters: (parameters) => {
if (levels.HIGH > level) return;
if (levels.HIGH > level) {
return;
}
console.group();
parameters.forEach((p) => console.log(`${p.name}:`, p.value));
console.groupEnd();
},
functionName: (name) => {
if (levels.MEDIUM > level) return;
if (levels.MEDIUM > level) {
return;
}
console.log(`\nEXECUTING: ${name}\n`);
},
flow: (flow) => {
if (levels.LOW > level) return;
if (levels.LOW > level) {
return;
}
console.log(`\n\n\nBEGIN FLOW: ${flow}\n\n\n`);
},
variable: ({ name, value }) => {
if (levels.HIGH > level) return;
if (levels.HIGH > level) {
return;
}
console.group();
console.group();
console.log(`VARIABLE ${name}:`, value);
@ -34,7 +42,9 @@ module.exports = {
console.groupEnd();
},
request: () => (req, res, next) => {
if (levels.HIGH > level) return next();
if (levels.HIGH > level) {
return next();
}
console.log('Hit URL', req.url, 'with following:');
console.group();
console.log('Query:', req.query);

View file

@ -0,0 +1,33 @@
function findContent(obj) {
if (obj && typeof obj === 'object') {
if ('kwargs' in obj && 'content' in obj.kwargs) {
return obj.kwargs.content;
}
for (let key in obj) {
let content = findContent(obj[key]);
if (content) {
return content;
}
}
}
return null;
}
function findMessageContent(message) {
let startIndex = Math.min(message.indexOf('{'), message.indexOf('['));
let jsonString = message.substring(startIndex);
let jsonObjectOrArray;
try {
jsonObjectOrArray = JSON.parse(jsonString);
} catch (error) {
console.error('Failed to parse JSON:', error);
return null;
}
let content = findContent(jsonObjectOrArray);
return content;
}
module.exports = findMessageContent;

View file

@ -3,6 +3,7 @@ const cryptoUtils = require('./crypto');
const { tiktokenModels, maxTokensMap } = require('./tokens');
const sendEmail = require('./sendEmail');
const abortMessage = require('./abortMessage');
const findMessageContent = require('./findMessageContent');
module.exports = {
...cryptoUtils,
@ -11,4 +12,5 @@ module.exports = {
tiktokenModels,
sendEmail,
abortMessage,
}
findMessageContent,
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -155,7 +155,9 @@ export default function NewConversationMenu() {
<Button
id="new-conversation-menu"
variant="outline"
className={'group relative mb-[-12px] ml-0 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-[-12px] md:pl-1'}
className={
'group relative mb-[-12px] ml-0 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-[-12px] md:pl-1'
}
>
{icon}
<span className="max-w-0 overflow-hidden whitespace-nowrap px-0 text-slate-600 transition-all group-hover:max-w-[80px] group-hover:px-2 group-data-[state=open]:max-w-[80px] group-data-[state=open]:px-2 dark:text-slate-300">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more