diff --git a/.eslintrc.js b/.eslintrc.js index eabd948379..ab02a90903 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,13 @@ module.exports = { 'plugin:jest/recommended', 'prettier', ], - ignorePatterns: ['client/dist/**/*', 'client/public/**/*', 'e2e/playwright-report/**/*'], + ignorePatterns: [ + 'client/dist/**/*', + 'client/public/**/*', + 'e2e/playwright-report/**/*', + 'packages/data-provider/types/**/*', + 'packages/data-provider/dist/**/*', + ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 'latest', @@ -105,6 +111,18 @@ module.exports = { 'plugin:@typescript-eslint/recommended', ], }, + { + files: './packages/data-provider/**/*.ts', + overrides: [ + { + files: '**/*.ts', + parser: '@typescript-eslint/parser', + parserOptions: { + project: './packages/data-provider/tsconfig.json', + }, + }, + ], + }, ], settings: { react: { diff --git a/packages/data-provider/.gitignore b/packages/data-provider/.gitignore new file mode 100644 index 0000000000..40b878db5b --- /dev/null +++ b/packages/data-provider/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/packages/data-provider/babel.config.js b/packages/data-provider/babel.config.js new file mode 100644 index 0000000000..7d5344d252 --- /dev/null +++ b/packages/data-provider/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], + plugins: ['babel-plugin-replace-ts-export-assignment'], +}; diff --git a/packages/data-provider/jest.config.js b/packages/data-provider/jest.config.js new file mode 100644 index 0000000000..6b8c4abe79 --- /dev/null +++ b/packages/data-provider/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!/node_modules/'], + coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + coverageReporters: ['text', 'cobertura'], + testResultsProcessor: 'jest-junit', + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + }, + // coverageThreshold: { + // global: { + // statements: 58, + // branches: 49, + // functions: 50, + // lines: 57, + // }, + // }, + restoreMocks: true, +}; diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json new file mode 100644 index 0000000000..40fe8314bc --- /dev/null +++ b/packages/data-provider/package.json @@ -0,0 +1,49 @@ +{ + "name": "librechat-data-provider", + "version": "0.1.1", + "description": "data services for librechat apps", + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "types/index.d.ts", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && rollup -c --silent --bundleConfigAsCjs", + "build:watch": "rollup -c -w", + "test": "jest --coverage --watch", + "test:ci": "jest --coverage --ci", + "verify": "npm run test:ci" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/danny-avila/librechat-data-provider.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/danny-avila/librechat-data-provider/issues" + }, + "homepage": "https://github.com/danny-avila/LibreChat#readme", + "dependencies": { + "@tanstack/react-query": "^4.28.0", + "axios": "^1.3.4" + }, + "devDependencies": { + "@babel/preset-env": "^7.21.5", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-node-resolve": "^15.1.0", + "@tanstack/query-core": "^4.29.19", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.0", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "rimraf": "^5.0.1", + "rollup": "^3.26.0", + "rollup-plugin-typescript2": "^0.35.0", + "typescript": "^5.0.4" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/data-provider/rollup.config.js b/packages/data-provider/rollup.config.js new file mode 100644 index 0000000000..5ff739fce9 --- /dev/null +++ b/packages/data-provider/rollup.config.js @@ -0,0 +1,31 @@ +import typescript from 'rollup-plugin-typescript2'; +import resolve from '@rollup/plugin-node-resolve'; +import pkg from './package.json'; + +export default [ + { + input: 'src/index.ts', + output: [ + { + file: pkg.main, + format: 'cjs', + }, + { + file: pkg.module, + format: 'esm', + }, + ], + ...{ + external: [ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.devDependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), + ], + preserveSymlinks: true, + plugins: [ + resolve(), + typescript({ useTsconfigDeclarationDir: true, tsconfig: './tsconfig.json' }), + ], + }, + }, +]; diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts new file mode 100644 index 0000000000..fe1579cd54 --- /dev/null +++ b/packages/data-provider/src/api-endpoints.ts @@ -0,0 +1,95 @@ +export const user = () => { + return '/api/user'; +}; + +export const userPlugins = () => { + return '/api/user/plugins'; +}; + +export const messages = (id: string) => { + return `/api/messages/${id}`; +}; + +export const abortRequest = (endpoint: string) => { + return `/api/ask/${endpoint}/abort`; +}; + +export const conversations = (pageNumber: string) => { + return `/api/convos?pageNumber=${pageNumber}`; +}; + +export const conversationById = (id: string) => { + return `/api/convos/${id}`; +}; + +export const updateConversation = () => { + return '/api/convos/update'; +}; + +export const deleteConversation = () => { + return '/api/convos/clear'; +}; + +export const search = (q: string, pageNumber: string) => { + return `/api/search?q=${q}&pageNumber=${pageNumber}`; +}; + +export const searchEnabled = () => { + return '/api/search/enable'; +}; + +export const presets = () => { + return '/api/presets'; +}; + +export const deletePreset = () => { + return '/api/presets/delete'; +}; + +export const aiEndpoints = () => { + return '/api/endpoints'; +}; + +export const tokenizer = () => { + return '/api/tokenizer'; +}; + +export const login = () => { + return '/api/auth/login'; +}; + +export const logout = () => { + return '/api/auth/logout'; +}; + +export const register = () => { + return '/api/auth/register'; +}; + +export const loginFacebook = () => { + return '/api/auth/facebook'; +}; + +export const loginGoogle = () => { + return '/api/auth/google'; +}; + +export const refreshToken = () => { + return '/api/auth/refresh'; +}; + +export const requestPasswordReset = () => { + return '/api/auth/requestPasswordReset'; +}; + +export const resetPassword = () => { + return '/api/auth/resetPassword'; +}; + +export const plugins = () => { + return '/api/plugins'; +}; + +export const config = () => { + return '/api/config'; +}; diff --git a/packages/data-provider/src/createPayload.ts b/packages/data-provider/src/createPayload.ts new file mode 100644 index 0000000000..4372416fcd --- /dev/null +++ b/packages/data-provider/src/createPayload.ts @@ -0,0 +1,28 @@ +import type { TConversation, TSubmission, EModelEndpoint } from './types'; + +export default function createPayload(submission: TSubmission) { + const { conversation, message, endpointOption } = submission; + const { conversationId } = conversation as TConversation; + const { endpoint } = endpointOption as { endpoint: EModelEndpoint }; + + const endpointUrlMap = { + azureOpenAI: '/api/ask/azureOpenAI', + openAI: '/api/ask/openAI', + google: '/api/ask/google', + bingAI: '/api/ask/bingAI', + chatGPT: '/api/ask/chatGPT', + chatGPTBrowser: '/api/ask/chatGPTBrowser', + gptPlugins: '/api/ask/gptPlugins', + anthropic: '/api/ask/anthropic', + }; + + const server = endpointUrlMap[endpoint]; + + const payload = { + ...message, + ...endpointOption, + conversationId, + }; + + return { server, payload }; +} diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts new file mode 100644 index 0000000000..0f38578019 --- /dev/null +++ b/packages/data-provider/src/data-service.ts @@ -0,0 +1,119 @@ +import * as t from './types'; +import request from './request'; +import * as endpoints from './api-endpoints'; + +export function getConversations(pageNumber: string): Promise { + return request.get(endpoints.conversations(pageNumber)); +} + +export function abortRequestWithMessage( + endpoint: string, + abortKey: string, + message: string, +): Promise { + return request.post(endpoints.abortRequest(endpoint), { arg: { abortKey, message } }); +} + +export function deleteConversation(payload: t.TDeleteConversationRequest) { + //todo: this should be a DELETE request + return request.post(endpoints.deleteConversation(), { arg: payload }); +} + +export function clearAllConversations(): Promise { + return request.post(endpoints.deleteConversation(), { arg: {} }); +} + +export function getMessagesByConvoId(id: string): Promise { + return request.get(endpoints.messages(id)); +} + +export function getConversationById(id: string): Promise { + return request.get(endpoints.conversationById(id)); +} + +export function updateConversation( + payload: t.TUpdateConversationRequest, +): Promise { + return request.post(endpoints.updateConversation(), { arg: payload }); +} + +export function getPresets(): Promise { + return request.get(endpoints.presets()); +} + +export function createPreset(payload: t.TPreset): Promise { + return request.post(endpoints.presets(), payload); +} + +export function updatePreset(payload: t.TPreset): Promise { + return request.post(endpoints.presets(), payload); +} + +export function deletePreset(arg: t.TPreset | object): Promise { + return request.post(endpoints.deletePreset(), arg); +} + +export function getSearchEnabled(): Promise { + return request.get(endpoints.searchEnabled()); +} + +export function getUser(): Promise { + return request.get(endpoints.user()); +} + +export const searchConversations = async ( + q: string, + pageNumber: string, +): Promise => { + return request.get(endpoints.search(q, pageNumber)); +}; + +export const getAIEndpoints = () => { + return request.get(endpoints.aiEndpoints()); +}; + +export const updateTokenCount = (text: string) => { + return request.post(endpoints.tokenizer(), { arg: text }); +}; + +export const login = (payload: t.TLoginUser) => { + return request.post(endpoints.login(), payload); +}; + +export const logout = () => { + return request.post(endpoints.logout()); +}; + +export const register = (payload: t.TRegisterUser) => { + return request.post(endpoints.register(), payload); +}; + +export const refreshToken = () => { + return request.post(endpoints.refreshToken()); +}; + +export const getLoginGoogle = () => { + return request.get(endpoints.loginGoogle()); +}; + +export const requestPasswordReset = ( + payload: t.TRequestPasswordReset, +): Promise => { + return request.post(endpoints.requestPasswordReset(), payload); +}; + +export const resetPassword = (payload: t.TResetPassword) => { + return request.post(endpoints.resetPassword(), payload); +}; + +export const getAvailablePlugins = (): Promise => { + return request.get(endpoints.plugins()); +}; + +export const updateUserPlugins = (payload: t.TUpdateUserPlugins) => { + return request.post(endpoints.userPlugins(), payload); +}; + +export const getStartupConfig = (): Promise => { + return request.get(endpoints.config()); +}; diff --git a/packages/data-provider/src/headers-helpers.ts b/packages/data-provider/src/headers-helpers.ts new file mode 100644 index 0000000000..195f7c7912 --- /dev/null +++ b/packages/data-provider/src/headers-helpers.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; + +export function setAcceptLanguageHeader(value: string): void { + axios.defaults.headers.common['Accept-Language'] = value; +} + +export function setTokenHeader(token: string) { + axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; +} diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts new file mode 100644 index 0000000000..717f107b4e --- /dev/null +++ b/packages/data-provider/src/index.ts @@ -0,0 +1,7 @@ +export * from './data-service'; +export * from './request'; +export * from './types'; +export * from './react-query-service'; +export * from './headers-helpers'; +export * from './sse'; +export { default as createPayload } from './createPayload'; diff --git a/packages/data-provider/src/react-query-service.ts b/packages/data-provider/src/react-query-service.ts new file mode 100644 index 0000000000..01c13c36bd --- /dev/null +++ b/packages/data-provider/src/react-query-service.ts @@ -0,0 +1,362 @@ +import { + UseQueryOptions, + useQuery, + useMutation, + useQueryClient, + UseMutationResult, + QueryObserverResult, +} from '@tanstack/react-query'; +import * as t from './types'; +import * as dataService from './data-service'; + +export enum QueryKeys { + messages = 'messsages', + allConversations = 'allConversations', + conversation = 'conversation', + searchEnabled = 'searchEnabled', + user = 'user', + endpoints = 'endpoints', + presets = 'presets', + searchResults = 'searchResults', + tokenCount = 'tokenCount', + availablePlugins = 'availablePlugins', + startupConfig = 'startupConfig', +} + +export const useAbortRequestWithMessage = (): UseMutationResult< + void, + Error, + { endpoint: string; abortKey: string; message: string } +> => { + return useMutation(({ endpoint, abortKey, message }) => + dataService.abortRequestWithMessage(endpoint, abortKey, message), + ); +}; + +export const useGetUserQuery = ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery([QueryKeys.user], () => dataService.getUser(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + retry: false, + ...config, + }); +}; + +export const useGetMessagesByConvoId = ( + id: string, + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery( + [QueryKeys.messages, id], + () => dataService.getMessagesByConvoId(id), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }, + ); +}; + +export const useGetConversationByIdQuery = ( + id: string, + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery( + [QueryKeys.conversation, id], + () => dataService.getConversationById(id), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }, + ); +}; + +//This isn't ideal because its just a query and we're using mutation, but it was the only way +//to make it work with how the Chat component is structured +export const useGetConversationByIdMutation = (id: string): UseMutationResult => { + const queryClient = useQueryClient(); + return useMutation(() => dataService.getConversationById(id), { + // onSuccess: (res: t.TConversation) => { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.conversation, id]); + }, + }); +}; + +export const useUpdateConversationMutation = ( + id: string, +): UseMutationResult< + t.TUpdateConversationResponse, + unknown, + t.TUpdateConversationRequest, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation( + (payload: t.TUpdateConversationRequest) => dataService.updateConversation(payload), + { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.conversation, id]); + queryClient.invalidateQueries([QueryKeys.allConversations]); + }, + }, + ); +}; + +export const useDeleteConversationMutation = ( + id?: string, +): UseMutationResult< + t.TDeleteConversationResponse, + unknown, + t.TDeleteConversationRequest, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation( + (payload: t.TDeleteConversationRequest) => dataService.deleteConversation(payload), + { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.conversation, id]); + queryClient.invalidateQueries([QueryKeys.allConversations]); + }, + }, + ); +}; + +export const useClearConversationsMutation = (): UseMutationResult => { + const queryClient = useQueryClient(); + return useMutation(() => dataService.clearAllConversations(), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.allConversations]); + }, + }); +}; + +export const useGetConversationsQuery = ( + pageNumber: string, + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery( + [QueryKeys.allConversations, pageNumber], + () => dataService.getConversations(pageNumber), + { + refetchOnReconnect: false, + refetchOnMount: false, + retry: 1, + ...config, + }, + ); +}; + +export const useGetSearchEnabledQuery = ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery([QueryKeys.searchEnabled], () => dataService.getSearchEnabled(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }); +}; + +export const useGetEndpointsQuery = (): QueryObserverResult => { + return useQuery([QueryKeys.endpoints], () => dataService.getAIEndpoints(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + }); +}; + +export const useCreatePresetMutation = (): UseMutationResult< + t.TPreset[], + unknown, + t.TPreset, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TPreset) => dataService.createPreset(payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.presets]); + }, + }); +}; + +export const useUpdatePresetMutation = (): UseMutationResult< + t.TPreset[], + unknown, + t.TPreset, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TPreset) => dataService.updatePreset(payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.presets]); + }, + }); +}; + +export const useGetPresetsQuery = ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery([QueryKeys.presets], () => dataService.getPresets(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }); +}; + +export const useDeletePresetMutation = (): UseMutationResult< + t.TPreset[], + unknown, + t.TPreset | object, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TPreset | object) => dataService.deletePreset(payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.presets]); + }, + }); +}; + +export const useSearchQuery = ( + searchQuery: string, + pageNumber: string, + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery( + [QueryKeys.searchResults, pageNumber, searchQuery], + () => dataService.searchConversations(searchQuery, pageNumber), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + }, + ); +}; + +export const useUpdateTokenCountMutation = (): UseMutationResult< + t.TUpdateTokenCountResponse, + unknown, + string, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((text: string) => dataService.updateTokenCount(text), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.tokenCount]); + }, + }); +}; + +export const useLoginUserMutation = (): UseMutationResult< + t.TLoginResponse, + unknown, + t.TLoginUser, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TLoginUser) => dataService.login(payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.user]); + }, + }); +}; + +export const useRegisterUserMutation = (): UseMutationResult< + unknown, + unknown, + t.TRegisterUser, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TRegisterUser) => dataService.register(payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.user]); + }, + }); +}; + +export const useLogoutUserMutation = (): UseMutationResult => { + const queryClient = useQueryClient(); + return useMutation(() => dataService.logout(), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.user]); + }, + }); +}; + +export const useRefreshTokenMutation = (): UseMutationResult< + t.TRefreshTokenResponse, + unknown, + unknown, + unknown +> => { + return useMutation(() => dataService.refreshToken(), {}); +}; + +export const useRequestPasswordResetMutation = (): UseMutationResult< + t.TRequestPasswordResetResponse, + unknown, + t.TRequestPasswordReset, + unknown +> => { + return useMutation((payload: t.TRequestPasswordReset) => + dataService.requestPasswordReset(payload), + ); +}; + +export const useResetPasswordMutation = (): UseMutationResult< + unknown, + unknown, + t.TResetPassword, + unknown +> => { + return useMutation((payload: t.TResetPassword) => dataService.resetPassword(payload)); +}; + +export const useAvailablePluginsQuery = (): QueryObserverResult => { + return useQuery( + [QueryKeys.availablePlugins], + () => dataService.getAvailablePlugins(), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + }, + ); +}; + +export const useUpdateUserPluginsMutation = (): UseMutationResult< + t.TUser, + unknown, + t.TUpdateUserPlugins, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TUpdateUserPlugins) => dataService.updateUserPlugins(payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.user]); + }, + }); +}; + +export const useGetStartupConfig = (): QueryObserverResult => { + return useQuery( + [QueryKeys.startupConfig], + () => dataService.getStartupConfig(), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + }, + ); +}; diff --git a/packages/data-provider/src/request.ts b/packages/data-provider/src/request.ts new file mode 100644 index 0000000000..07e93e76ad --- /dev/null +++ b/packages/data-provider/src/request.ts @@ -0,0 +1,55 @@ +import axios, { AxiosRequestConfig } from 'axios'; + +async function _get(url: string, options?: AxiosRequestConfig): Promise { + const response = await axios.get(url, { ...options }); + return response.data; +} + +async function _post(url: string, data?: any) { + const response = await axios.post(url, JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + return response.data; +} + +async function _postMultiPart(url: string, formData: FormData, options?: AxiosRequestConfig) { + const response = await axios.post(url, formData, { + ...options, + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; +} + +async function _put(url: string, data?: any) { + const response = await axios.put(url, JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + return response.data; +} + +async function _delete(url: string): Promise { + const response = await axios.delete(url); + return response.data; +} + +async function _deleteWithOptions(url: string, options?: AxiosRequestConfig): Promise { + const response = await axios.delete(url, { ...options }); + return response.data; +} + +async function _patch(url: string, data?: any) { + const response = await axios.patch(url, JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + return response.data; +} + +export default { + get: _get, + post: _post, + postMultiPart: _postMultiPart, + put: _put, + delete: _delete, + deleteWithOptions: _deleteWithOptions, + patch: _patch, +}; diff --git a/packages/data-provider/src/sse.js b/packages/data-provider/src/sse.js new file mode 100644 index 0000000000..7f350f1ffb --- /dev/null +++ b/packages/data-provider/src/sse.js @@ -0,0 +1,224 @@ +/* eslint-disable */ +/** + * Copyright (C) 2016 Maxime Petazzoni . + * All rights reserved. + */ + +var SSE = function (url, options) { + if (!(this instanceof SSE)) { + return new SSE(url, options); + } + + this.INITIALIZING = -1; + this.CONNECTING = 0; + this.OPEN = 1; + this.CLOSED = 2; + + this.url = url; + + options = options || {}; + this.headers = options.headers || {}; + this.payload = options.payload !== undefined ? options.payload : ''; + this.method = options.method || (this.payload && 'POST') || 'GET'; + this.withCredentials = !!options.withCredentials; + + this.FIELD_SEPARATOR = ':'; + this.listeners = {}; + + this.xhr = null; + this.readyState = this.INITIALIZING; + this.progress = 0; + this.chunk = ''; + + this.addEventListener = function (type, listener) { + if (this.listeners[type] === undefined) { + this.listeners[type] = []; + } + + if (this.listeners[type].indexOf(listener) === -1) { + this.listeners[type].push(listener); + } + }; + + this.removeEventListener = function (type, listener) { + if (this.listeners[type] === undefined) { + return; + } + + var filtered = []; + this.listeners[type].forEach(function (element) { + if (element !== listener) { + filtered.push(element); + } + }); + if (filtered.length === 0) { + delete this.listeners[type]; + } else { + this.listeners[type] = filtered; + } + }; + + this.dispatchEvent = function (e) { + if (!e) { + return true; + } + + e.source = this; + + var onHandler = 'on' + e.type; + if (this.hasOwnProperty(onHandler)) { + this[onHandler].call(this, e); + if (e.defaultPrevented) { + return false; + } + } + + if (this.listeners[e.type]) { + return this.listeners[e.type].every(function (callback) { + callback(e); + return !e.defaultPrevented; + }); + } + + return true; + }; + + this._setReadyState = function (state) { + var event = new CustomEvent('readystatechange'); + event.readyState = state; + this.readyState = state; + this.dispatchEvent(event); + }; + + this._onStreamFailure = function (e) { + var event = new CustomEvent('error'); + event.data = e.currentTarget.response; + this.dispatchEvent(event); + this.close(); + }; + + this._onStreamAbort = function (e) { + this.dispatchEvent(new CustomEvent('abort')); + this.close(); + }; + + this._onStreamProgress = function (e) { + if (!this.xhr) { + return; + } + + if (this.xhr.status !== 200) { + this._onStreamFailure(e); + return; + } + + if (this.readyState == this.CONNECTING) { + this.dispatchEvent(new CustomEvent('open')); + this._setReadyState(this.OPEN); + } + + var data = this.xhr.responseText.substring(this.progress); + this.progress += data.length; + data.split(/(\r\n|\r|\n){2}/g).forEach( + function (part) { + if (part.trim().length === 0) { + this.dispatchEvent(this._parseEventChunk(this.chunk.trim())); + this.chunk = ''; + } else { + this.chunk += part; + } + }.bind(this), + ); + }; + + this._onStreamLoaded = function (e) { + this._onStreamProgress(e); + + // Parse the last chunk. + this.dispatchEvent(this._parseEventChunk(this.chunk)); + this.chunk = ''; + }; + + /** + * Parse a received SSE event chunk into a constructed event object. + */ + this._parseEventChunk = function (chunk) { + if (!chunk || chunk.length === 0) { + return null; + } + + var e = { id: null, retry: null, data: '', event: 'message' }; + chunk.split(/\n|\r\n|\r/).forEach( + function (line) { + line = line.trimRight(); + var index = line.indexOf(this.FIELD_SEPARATOR); + if (index <= 0) { + // Line was either empty, or started with a separator and is a comment. + // Either way, ignore. + return; + } + + var field = line.substring(0, index); + if (!(field in e)) { + return; + } + + var value = line.substring(index + 1).trimLeft(); + if (field === 'data') { + e[field] += value; + } else { + e[field] = value; + } + }.bind(this), + ); + + var event = new CustomEvent(e.event); + event.data = e.data; + event.id = e.id; + return event; + }; + + this._checkStreamClosed = function () { + if (!this.xhr) { + return; + } + + if (this.xhr.readyState === XMLHttpRequest.DONE) { + this._setReadyState(this.CLOSED); + } + }; + + this.stream = function () { + this._setReadyState(this.CONNECTING); + + this.xhr = new XMLHttpRequest(); + this.xhr.addEventListener('progress', this._onStreamProgress.bind(this)); + this.xhr.addEventListener('load', this._onStreamLoaded.bind(this)); + this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this)); + this.xhr.addEventListener('error', this._onStreamFailure.bind(this)); + this.xhr.addEventListener('abort', this._onStreamAbort.bind(this)); + this.xhr.open(this.method, this.url); + for (var header in this.headers) { + this.xhr.setRequestHeader(header, this.headers[header]); + } + this.xhr.withCredentials = this.withCredentials; + this.xhr.send(this.payload); + }; + + this.close = function () { + if (this.readyState === this.CLOSED) { + return; + } + + this.xhr.abort(); + this.xhr = null; + this._setReadyState(this.CLOSED); + }; +}; + +export { SSE }; +// Export our SSE module for npm.js +// if (typeof exports !== 'undefined') { +// // exports.SSE = SSE; +// module.exports = { SSE }; +// } diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts new file mode 100644 index 0000000000..76997a63e1 --- /dev/null +++ b/packages/data-provider/src/types.ts @@ -0,0 +1,275 @@ +export type TMessage = { + messageId: string; + conversationId: string; + clientId: string; + parentMessageId: string; + sender: string; + text: string; + isCreatedByUser: boolean; + error: boolean; + createdAt: string; + updatedAt: string; +}; + +export type TExample = { + input: string; + output: string; +}; + +export enum EModelEndpoint { + azureOpenAI = 'azureOpenAI', + openAI = 'openAI', + bingAI = 'bingAI', + chatGPT = 'chatGPT', + chatGPTBrowser = 'chatGPTBrowser', + google = 'google', + gptPlugins = 'gptPlugins', + anthropic = 'anthropic', +} + +export type TSubmission = { + clientId?: string; + context?: string; + conversationId?: string; + conversationSignature?: string; + current: boolean; + endpoint: EModelEndpoint; + invocationId: number; + isCreatedByUser: boolean; + jailbreak: boolean; + jailbreakConversationId?: string; + messageId: string; + overrideParentMessageId?: string | boolean; + parentMessageId?: string; + sender: string; + systemMessage?: string; + text: string; + toneStyle?: string; + model?: string; + promptPrefix?: string; + temperature?: number; + top_p?: number; + presence_penalty?: number; + frequence_penalty?: number; + conversation: TConversation; + message: TMessage; + endpointOption: TEndpointOption; +}; + +export type TEndpointOption = { + endpoint: EModelEndpoint; + model?: string; + promptPrefix?: string; + temperature?: number; +}; + +export type TPluginAuthConfig = { + authField: string; + label: string; + description: string; +}; + +export type TPlugin = { + name: string; + pluginKey: string; + description: string; + icon: string; + authConfig: TPluginAuthConfig[]; + authenticated: boolean; +}; + +export type TUpdateUserPlugins = { + pluginKey: string; + action: string; + auth?: unknown; +}; + +export type TConversation = { + conversationId: string; + title: string; + user?: string; + endpoint: EModelEndpoint; + suggestions?: string[]; + messages?: TMessage[]; + tools?: TPlugin[]; + createdAt: string; + updatedAt: string; + // google only + modelLabel?: string; + examples?: TExample[]; + // for azureOpenAI, openAI only + chatGptLabel?: string; + userLabel?: string; + model?: string; + promptPrefix?: string; + temperature?: number; + topP?: number; + topK?: number; + // bing and google + context?: string; + top_p?: number; + presence_penalty?: number; + // for bingAI only + jailbreak?: boolean; + jailbreakConversationId?: string; + conversationSignature?: string; + parentMessageId?: string; + clientId?: string; + invocationId?: string; + toneStyle?: string; +}; + +export type TPreset = { + title: string; + endpoint: EModelEndpoint; + conversationSignature?: string; + createdAt?: string; + updatedAt?: string; + presetId?: string; + user?: string; + // for azureOpenAI, openAI only + chatGptLabel?: string; + frequence_penalty?: number; + model?: string; + presence_penalty?: number; + promptPrefix?: string; + temperature?: number; + top_p?: number; + //for BingAI + clientId?: string; + invocationId?: number; + jailbreak?: boolean; + jailbreakPresetId?: string; + presetSignature?: string; + toneStyle?: string; +}; + +export type TUser = { + id: string; + username: string; + email: string; + name: string; + avatar: string; + role: string; + provider: string; + plugins: string[]; + createdAt: string; + updatedAt: string; +}; + +export type TGetConversationsResponse = { + conversations: TConversation[]; + pageNumber: string; + pageSize: string | number; + pages: string | number; +}; + +export type TUpdateConversationRequest = { + conversationId: string; + title: string; +}; + +export type TUpdateConversationResponse = { + data: TConversation; +}; + +export type TDeleteConversationRequest = { + conversationId?: string; + source?: string; +}; + +export type TDeleteConversationResponse = { + acknowledged: boolean; + deletedCount: number; + messages: { + acknowledged: boolean; + deletedCount: number; + }; +}; + +export type TSearchResults = { + conversations: TConversation[]; + messages: TMessage[]; + pageNumber: string; + pageSize: string | number; + pages: string | number; + filter: object; +}; + +export type TEndpoints = { + azureOpenAI: boolean; + bingAI: boolean; + ChatGptBrowser: { + availableModels: []; + }; + OpenAI: { + availableModels: []; + }; +}; + +export type TUpdateTokenCountResponse = { + count: number; +}; + +export type TMessageTreeNode = object; + +export type TSearchMessage = object; + +export type TSearchMessageTreeNode = object; + +export type TRegisterUser = { + name: string; + email: string; + username: string; + password: string; + confirm_password?: string; +}; + +export type TLoginUser = { + email: string; + password: string; +}; + +export type TLoginResponse = { + token: string; + user: TUser; +}; + +export type TRequestPasswordReset = { + email: string; +}; + +export type TResetPassword = { + userId: string; + token: string; + password: string; + confirm_password?: string; +}; + +export type TStartupConfig = { + appTitle: boolean; + googleLoginEnabled: boolean; + openidLoginEnabled: boolean; + githubLoginEnabled: boolean; + openidLabel: string; + openidImageUrl: string; + discordLoginEnabled: boolean; + serverDomain: string; + registrationEnabled: boolean; + socialLoginEnabled: boolean; +}; + +export type TRefreshTokenResponse = { + token: string; + user: TUser; +}; + +export type TRequestPasswordResetResponse = { + link: string; +}; + +export type File = { + name: string; + date: number; + size: number; +}; diff --git a/packages/data-provider/tsconfig.json b/packages/data-provider/tsconfig.json new file mode 100644 index 0000000000..a21afa2b87 --- /dev/null +++ b/packages/data-provider/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "./types", + "module": "esnext", + "noImplicitAny": true, + "outDir": "./types", + "target": "es5", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "lib": ["es2017", "dom"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": "src", + "paths": { + "@src/*": ["./*"] + } + }, + "exclude": ["node_modules", "dist", "types"], + "include": ["src/**/*", "types/index.d.ts"] +}