mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
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:
parent
39ac8d3858
commit
514f625b8f
165 changed files with 3002 additions and 712 deletions
|
@ -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 }],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
18
api/app/clients/tools/.well-known/Ai_PDF.json
Normal file
18
api/app/clients/tools/.well-known/Ai_PDF.json
Normal 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"
|
||||
}
|
22
api/app/clients/tools/.well-known/VoxScript.json
Normal file
22
api/app/clients/tools/.well-known/VoxScript.json
Normal 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/"
|
||||
}
|
18
api/app/clients/tools/.well-known/askyourpdf.json
Normal file
18
api/app/clients/tools/.well-known/askyourpdf.json
Normal 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 [](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"
|
||||
}
|
|
@ -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"
|
||||
}
|
24
api/app/clients/tools/.well-known/has-issues/web_pilot.json
Normal file
24
api/app/clients/tools/.well-known/has-issues/web_pilot.json
Normal 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
|
||||
}
|
||||
}
|
157
api/app/clients/tools/.well-known/openapi/askyourpdf.yaml
Normal file
157
api/app/clients/tools/.well-known/openapi/askyourpdf.yaml
Normal 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
|
185
api/app/clients/tools/.well-known/openapi/scholarai.yaml
Normal file
185
api/app/clients/tools/.well-known/openapi/scholarai.yaml
Normal 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.
|
18
api/app/clients/tools/.well-known/rephrase.json
Normal file
18
api/app/clients/tools/.well-known/rephrase.json
Normal 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"
|
||||
}
|
22
api/app/clients/tools/.well-known/scholarai.json
Normal file
22
api/app/clients/tools/.well-known/scholarai.json
Normal 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 user’s Zotero reference manager by using the save-citation endpoint and providing the doi and requesting the user’s 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"
|
||||
}
|
139
api/app/clients/tools/dynamic/OpenAPIPlugin.js
Normal file
139
api/app/clients/tools/dynamic/OpenAPIPlugin.js
Normal 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,
|
||||
};
|
65
api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js
Normal file
65
api/app/clients/tools/dynamic/OpenAPIPlugin.spec.js
Normal 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
|
||||
});
|
|
@ -32,7 +32,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Browser",
|
||||
"pluginKey": "browser",
|
||||
"pluginKey": "web-browser",
|
||||
"description": "Scrape and summarize webpage data",
|
||||
"icon": "/assets/web-browser.svg",
|
||||
"authConfig": [
|
||||
|
|
31
api/app/clients/tools/util/addOpenAPISpecs.js
Normal file
31
api/app/clients/tools/util/addOpenAPISpecs.js
Normal 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,
|
||||
};
|
76
api/app/clients/tools/util/addOpenAPISpecs.spec.js
Normal file
76
api/app/clients/tools/util/addOpenAPISpecs.spec.js
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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(
|
||||
|
|
104
api/app/clients/tools/util/loadSpecs.js
Normal file
104
api/app/clients/tools/util/loadSpecs.js
Normal 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,
|
||||
};
|
101
api/app/clients/tools/util/loadSpecs.spec.js
Normal file
101
api/app/clients/tools/util/loadSpecs.spec.js
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 - ');
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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: ', '')
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
|
|
33
api/utils/findMessageContent.js
Normal file
33
api/utils/findMessageContent.js
Normal 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;
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -61,7 +61,7 @@ const Settings = ({ preset, ...props }) => {
|
|||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (endpoint === 'gptPlugins') {
|
||||
} else if (endpoint === 'gptPlugins') {
|
||||
return (
|
||||
<PluginsSettings
|
||||
model={preset?.model}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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'] || [];
|
||||
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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');
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -32,6 +32,6 @@ const InputWithLabel: FC<InputWithLabelProps> = ({ value, onChange, label, id })
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default InputWithLabel;
|
||||
|
|
|
@ -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'}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -39,7 +39,7 @@ export default function SubmitButton({
|
|||
</div>
|
||||
</button>
|
||||
);
|
||||
} else if (!isTokenProvided && (!endpointsToHideSetTokens.has(endpoint))) {
|
||||
} else if (!isTokenProvided && !endpointsToHideSetTokens.has(endpoint)) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
|
|
@ -131,7 +131,9 @@ export default function TextChat({ isSearchView = false }) {
|
|||
setShowBingToneSetting((show) => !show);
|
||||
};
|
||||
|
||||
if (isSearchView) return <></>;
|
||||
if (isSearchView) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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}
|
|
@ -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';
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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))}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
146
client/src/components/Messages/Plugin.tsx
Normal file
146
client/src/components/Messages/Plugin.tsx
Normal 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);
|
|
@ -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 ? (
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -18,7 +18,9 @@ const ExportConversation = forwardRef(() => {
|
|||
conversation?.conversationId !== 'search';
|
||||
|
||||
const clickHandler = () => {
|
||||
if (exportable) setOpen(true);
|
||||
if (exportable) {
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue