From 81c5e30fda160e881e157ed2a8b716b7eefb5bdd Mon Sep 17 00:00:00 2001 From: Jin Yucong Date: Fri, 5 Jul 2024 15:49:43 +0800 Subject: [PATCH] chore: Update dependencies and fix import paths --- .eslintrc.json | 23 +- package.json | 3 + src/agents/academicSearchAgent.ts | 32 +- src/agents/imageSearchAgent.ts | 8 +- src/agents/redditSearchAgent.ts | 34 +- src/agents/videoSearchAgent.ts | 8 +- src/agents/webSearchAgent.ts | 32 +- src/agents/wolframAlphaSearchAgent.ts | 20 +- src/agents/writingAssistant.ts | 10 +- src/agents/youtubeSearchAgent.ts | 34 +- src/app.ts | 2 +- src/config.ts | 7 +- src/db/index.ts | 4 +- src/lib/huggingfaceTransformer.ts | 20 +- src/lib/outputParsers/listLineOutputParser.ts | 6 +- src/lib/providers.ts | 38 +- src/lib/searxng.ts | 16 +- src/routes/chats.ts | 36 +- src/routes/config.ts | 4 +- src/routes/images.ts | 18 +- src/routes/models.ts | 6 +- src/routes/suggestions.ts | 18 +- src/routes/videos.ts | 18 +- src/websocket/connectionManager.ts | 20 +- src/websocket/index.ts | 2 +- src/websocket/messageHandler.ts | 35 +- src/websocket/websocketServer.ts | 2 +- tsconfig.json | 4 +- ui/.eslintrc.json | 22 +- ui/app/library/page.tsx | 4 +- ui/components/Chat.tsx | 24 +- ui/components/ChatWindow.tsx | 101 +- ui/components/DeleteChat.tsx | 5 +- ui/components/EmptyChatMessageInput.tsx | 6 +- ui/components/MessageActions/Copy.tsx | 3 +- ui/components/MessageBox.tsx | 10 +- ui/components/MessageInput.tsx | 10 +- ui/components/MessageInputActions/Focus.tsx | 10 +- ui/components/MessageSources.tsx | 23 +- ui/components/Navbar.tsx | 2 +- ui/components/SearchImages.tsx | 22 +- ui/components/SearchVideos.tsx | 21 +- ui/components/SettingsDialog.tsx | 17 +- ui/components/Sidebar.tsx | 8 +- ui/lib/utils.ts | 15 +- yarn.lock | 1234 ++++++++++++++++- 46 files changed, 1626 insertions(+), 371 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 65d539e..4c53579 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,11 +3,30 @@ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" + "plugin:prettier/recommended", + "plugin:unicorn/recommended" ], "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint", - "prettier" + "prettier", + "unicorn" + ], + "rules": { + "unicorn/filename-case": [ + "error", + { + "cases": { + "camelCase": true, + "pascalCase": true + } + } + ], + "unicorn/prevent-abbreviations": "warn", + "unicorn/no-null": "off", + "@typescript-eslint/no-unused-vars": "off" + }, + "overrides": [ + ] } \ No newline at end of file diff --git a/package.json b/package.json index b6f3d68..7f534d7 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "eslint": "^8", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-unicorn": "^54.0.0", "nodemon": "^3.1.0", "prettier": "^3.2.5", "ts-node": "^10.9.2", diff --git a/src/agents/academicSearchAgent.ts b/src/agents/academicSearchAgent.ts index 870981c..e8544ab 100644 --- a/src/agents/academicSearchAgent.ts +++ b/src/agents/academicSearchAgent.ts @@ -8,7 +8,7 @@ import type { StreamEvent } from "@langchain/core/tracers/log_stream"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import formatChatHistoryAsString from "../utils/formatHistory"; -import eventEmitter from "events"; +import eventEmitter from "node:events"; import computeSimilarity from "../utils/computeSimilarity"; import logger from "../utils/logger"; @@ -55,7 +55,7 @@ const basicAcademicSearchResponsePrompt = ` Anything between the \`context\` is retrieved from a search engine and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()} `; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const handleStream = async (stream: AsyncGenerator, emitter: eventEmitter) => { for await (const event of stream) { @@ -80,7 +80,7 @@ const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => { return RunnableSequence.from([ PromptTemplate.fromTemplate(basicAcademicSearchRetrieverPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { if (input === "not_needed") { return { query: "", docs: [] }; @@ -108,30 +108,30 @@ const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => { ]); }; +const processDocs = async (docs: Document[]) => { + return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); +}; + const createBasicAcademicSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => { const basicAcademicSearchRetrieverChain = createBasicAcademicSearchRetrieverChain(llm); - const processDocs = async (docs: Document[]) => { - return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); - }; - const rerankDocs = async ({ query, docs }: { query: string; docs: Document[] }) => { if (docs.length === 0) { return docs; } - const docsWithContent = docs.filter(doc => doc.pageContent && doc.pageContent.length > 0); + const docsWithContent = docs.filter(document => document.pageContent && document.pageContent.length > 0); - const [docEmbeddings, queryEmbedding] = await Promise.all([ - embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)), + const [documentEmbeddings, queryEmbedding] = await Promise.all([ + embeddings.embedDocuments(docsWithContent.map(document => document.pageContent)), embeddings.embedQuery(query), ]); - const similarity = docEmbeddings.map((docEmbedding, i) => { - const sim = computeSimilarity(queryEmbedding, docEmbedding); + const similarity = documentEmbeddings.map((documentEmbedding, index) => { + const sim = computeSimilarity(queryEmbedding, documentEmbedding); return { - index: i, + index: index, similarity: sim, }; }); @@ -167,7 +167,7 @@ const createBasicAcademicSearchAnsweringChain = (llm: BaseChatModel, embeddings: ["user", "{query}"], ]), llm, - strParser, + stringParser, ]).withConfig({ runName: "FinalResponseGenerator", }); @@ -190,9 +190,9 @@ const basicAcademicSearch = (query: string, history: BaseMessage[], llm: BaseCha ); handleStream(stream, emitter); - } catch (err) { + } catch (error) { emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" })); - logger.error(`Error in academic search: ${err}`); + logger.error(`Error in academic search: ${error}`); } return emitter; diff --git a/src/agents/imageSearchAgent.ts b/src/agents/imageSearchAgent.ts index 08b7ca1..196f26e 100644 --- a/src/agents/imageSearchAgent.ts +++ b/src/agents/imageSearchAgent.ts @@ -32,7 +32,7 @@ type ImageSearchChainInput = { query: string; }; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const createImageSearchChain = (llm: BaseChatModel) => { return RunnableSequence.from([ @@ -46,7 +46,7 @@ const createImageSearchChain = (llm: BaseChatModel) => { }), PromptTemplate.fromTemplate(imageSearchChainPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { const res = await searchSearxng(input, { engines: ["bing images", "google images"], @@ -54,7 +54,7 @@ const createImageSearchChain = (llm: BaseChatModel) => { const images = []; - res.results.forEach(result => { + for (const result of res.results) { if (result.img_src && result.url && result.title) { images.push({ img_src: result.img_src, @@ -62,7 +62,7 @@ const createImageSearchChain = (llm: BaseChatModel) => { title: result.title, }); } - }); + } return images.slice(0, 10); }), diff --git a/src/agents/redditSearchAgent.ts b/src/agents/redditSearchAgent.ts index 1bf9baa..15c654b 100644 --- a/src/agents/redditSearchAgent.ts +++ b/src/agents/redditSearchAgent.ts @@ -8,7 +8,7 @@ import type { StreamEvent } from "@langchain/core/tracers/log_stream"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import formatChatHistoryAsString from "../utils/formatHistory"; -import eventEmitter from "events"; +import eventEmitter from "node:events"; import computeSimilarity from "../utils/computeSimilarity"; import logger from "../utils/logger"; @@ -55,7 +55,7 @@ const basicRedditSearchResponsePrompt = ` Anything between the \`context\` is retrieved from Reddit and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()} `; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const handleStream = async (stream: AsyncGenerator, emitter: eventEmitter) => { for await (const event of stream) { @@ -80,7 +80,7 @@ const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => { return RunnableSequence.from([ PromptTemplate.fromTemplate(basicRedditSearchRetrieverPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { if (input === "not_needed") { return { query: "", docs: [] }; @@ -94,7 +94,7 @@ const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => { const documents = res.results.map( result => new Document({ - pageContent: result.content ? result.content : result.title, + pageContent: result.content ?? result.title, metadata: { title: result.title, url: result.url, @@ -108,30 +108,30 @@ const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => { ]); }; +const processDocs = async (docs: Document[]) => { + return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); +}; + const createBasicRedditSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => { const basicRedditSearchRetrieverChain = createBasicRedditSearchRetrieverChain(llm); - const processDocs = async (docs: Document[]) => { - return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); - }; - const rerankDocs = async ({ query, docs }: { query: string; docs: Document[] }) => { if (docs.length === 0) { return docs; } - const docsWithContent = docs.filter(doc => doc.pageContent && doc.pageContent.length > 0); + const docsWithContent = docs.filter(document => document.pageContent && document.pageContent.length > 0); - const [docEmbeddings, queryEmbedding] = await Promise.all([ - embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)), + const [documentEmbeddings, queryEmbedding] = await Promise.all([ + embeddings.embedDocuments(docsWithContent.map(document => document.pageContent)), embeddings.embedQuery(query), ]); - const similarity = docEmbeddings.map((docEmbedding, i) => { - const sim = computeSimilarity(queryEmbedding, docEmbedding); + const similarity = documentEmbeddings.map((documentEmbedding, index) => { + const sim = computeSimilarity(queryEmbedding, documentEmbedding); return { - index: i, + index: index, similarity: sim, }; }); @@ -168,7 +168,7 @@ const createBasicRedditSearchAnsweringChain = (llm: BaseChatModel, embeddings: E ["user", "{query}"], ]), llm, - strParser, + stringParser, ]).withConfig({ runName: "FinalResponseGenerator", }); @@ -190,9 +190,9 @@ const basicRedditSearch = (query: string, history: BaseMessage[], llm: BaseChatM ); handleStream(stream, emitter); - } catch (err) { + } catch (error) { emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" })); - logger.error(`Error in RedditSearch: ${err}`); + logger.error(`Error in RedditSearch: ${error}`); } return emitter; diff --git a/src/agents/videoSearchAgent.ts b/src/agents/videoSearchAgent.ts index ebab1d8..ced2a80 100644 --- a/src/agents/videoSearchAgent.ts +++ b/src/agents/videoSearchAgent.ts @@ -32,7 +32,7 @@ type VideoSearchChainInput = { query: string; }; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const createVideoSearchChain = (llm: BaseChatModel) => { return RunnableSequence.from([ @@ -46,7 +46,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => { }), PromptTemplate.fromTemplate(VideoSearchChainPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { const res = await searchSearxng(input, { engines: ["youtube"], @@ -54,7 +54,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => { const videos = []; - res.results.forEach(result => { + for (const result of res.results) { if (result.thumbnail && result.url && result.title && result.iframe_src) { videos.push({ img_src: result.thumbnail, @@ -63,7 +63,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => { iframe_src: result.iframe_src, }); } - }); + } return videos.slice(0, 10); }), diff --git a/src/agents/webSearchAgent.ts b/src/agents/webSearchAgent.ts index b90f509..3e3eb9c 100644 --- a/src/agents/webSearchAgent.ts +++ b/src/agents/webSearchAgent.ts @@ -8,7 +8,7 @@ import type { StreamEvent } from "@langchain/core/tracers/log_stream"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import formatChatHistoryAsString from "../utils/formatHistory"; -import eventEmitter from "events"; +import eventEmitter from "node:events"; import computeSimilarity from "../utils/computeSimilarity"; import logger from "../utils/logger"; @@ -55,7 +55,7 @@ const basicWebSearchResponsePrompt = ` Anything between the \`context\` is retrieved from a search engine and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()} `; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const handleStream = async (stream: AsyncGenerator, emitter: eventEmitter) => { for await (const event of stream) { @@ -80,7 +80,7 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => { return RunnableSequence.from([ PromptTemplate.fromTemplate(basicSearchRetrieverPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { if (input === "not_needed") { return { query: "", docs: [] }; @@ -107,30 +107,30 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => { ]); }; +const processDocs = async (docs: Document[]) => { + return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); +}; + const createBasicWebSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => { const basicWebSearchRetrieverChain = createBasicWebSearchRetrieverChain(llm); - const processDocs = async (docs: Document[]) => { - return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); - }; - const rerankDocs = async ({ query, docs }: { query: string; docs: Document[] }) => { if (docs.length === 0) { return docs; } - const docsWithContent = docs.filter(doc => doc.pageContent && doc.pageContent.length > 0); + const docsWithContent = docs.filter(document => document.pageContent && document.pageContent.length > 0); - const [docEmbeddings, queryEmbedding] = await Promise.all([ - embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)), + const [documentEmbeddings, queryEmbedding] = await Promise.all([ + embeddings.embedDocuments(docsWithContent.map(document => document.pageContent)), embeddings.embedQuery(query), ]); - const similarity = docEmbeddings.map((docEmbedding, i) => { - const sim = computeSimilarity(queryEmbedding, docEmbedding); + const similarity = documentEmbeddings.map((documentEmbedding, index) => { + const sim = computeSimilarity(queryEmbedding, documentEmbedding); return { - index: i, + index: index, similarity: sim, }; }); @@ -167,7 +167,7 @@ const createBasicWebSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embe ["user", "{query}"], ]), llm, - strParser, + stringParser, ]).withConfig({ runName: "FinalResponseGenerator", }); @@ -190,9 +190,9 @@ const basicWebSearch = (query: string, history: BaseMessage[], llm: BaseChatMode ); handleStream(stream, emitter); - } catch (err) { + } catch (error) { emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" })); - logger.error(`Error in websearch: ${err}`); + logger.error(`Error in websearch: ${error}`); } return emitter; diff --git a/src/agents/wolframAlphaSearchAgent.ts b/src/agents/wolframAlphaSearchAgent.ts index d8a07e7..a033084 100644 --- a/src/agents/wolframAlphaSearchAgent.ts +++ b/src/agents/wolframAlphaSearchAgent.ts @@ -8,7 +8,7 @@ import type { StreamEvent } from "@langchain/core/tracers/log_stream"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import formatChatHistoryAsString from "../utils/formatHistory"; -import eventEmitter from "events"; +import eventEmitter from "node:events"; import logger from "../utils/logger"; const basicWolframAlphaSearchRetrieverPrompt = ` @@ -54,7 +54,7 @@ const basicWolframAlphaSearchResponsePrompt = ` Anything between the \`context\` is retrieved from Wolfram Alpha and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()} `; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const handleStream = async (stream: AsyncGenerator, emitter: eventEmitter) => { for await (const event of stream) { @@ -79,7 +79,7 @@ const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => { return RunnableSequence.from([ PromptTemplate.fromTemplate(basicWolframAlphaSearchRetrieverPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { if (input === "not_needed") { return { query: "", docs: [] }; @@ -107,13 +107,13 @@ const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => { ]); }; +const processDocs = (docs: Document[]) => { + return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); +}; + const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => { const basicWolframAlphaSearchRetrieverChain = createBasicWolframAlphaSearchRetrieverChain(llm); - const processDocs = (docs: Document[]) => { - return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); - }; - return RunnableSequence.from([ RunnableMap.from({ query: (input: BasicChainInput) => input.query, @@ -139,7 +139,7 @@ const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => { ["user", "{query}"], ]), llm, - strParser, + stringParser, ]).withConfig({ runName: "FinalResponseGenerator", }); @@ -161,9 +161,9 @@ const basicWolframAlphaSearch = (query: string, history: BaseMessage[], llm: Bas ); handleStream(stream, emitter); - } catch (err) { + } catch (error) { emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" })); - logger.error(`Error in WolframAlphaSearch: ${err}`); + logger.error(`Error in WolframAlphaSearch: ${error}`); } return emitter; diff --git a/src/agents/writingAssistant.ts b/src/agents/writingAssistant.ts index 3c96167..7493abe 100644 --- a/src/agents/writingAssistant.ts +++ b/src/agents/writingAssistant.ts @@ -3,7 +3,7 @@ import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts import { RunnableSequence } from "@langchain/core/runnables"; import { StringOutputParser } from "@langchain/core/output_parsers"; import type { StreamEvent } from "@langchain/core/tracers/log_stream"; -import eventEmitter from "events"; +import eventEmitter from "node:events"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import logger from "../utils/logger"; @@ -13,7 +13,7 @@ You are Perplexica, an AI model who is expert at searching the web and answering Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode. `; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const handleStream = async (stream: AsyncGenerator, emitter: eventEmitter) => { for await (const event of stream) { @@ -34,7 +34,7 @@ const createWritingAssistantChain = (llm: BaseChatModel) => { ["user", "{query}"], ]), llm, - strParser, + stringParser, ]).withConfig({ runName: "FinalResponseGenerator", }); @@ -62,9 +62,9 @@ const handleWritingAssistant = ( ); handleStream(stream, emitter); - } catch (err) { + } catch (error) { emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" })); - logger.error(`Error in writing assistant: ${err}`); + logger.error(`Error in writing assistant: ${error}`); } return emitter; diff --git a/src/agents/youtubeSearchAgent.ts b/src/agents/youtubeSearchAgent.ts index db8640e..c235a32 100644 --- a/src/agents/youtubeSearchAgent.ts +++ b/src/agents/youtubeSearchAgent.ts @@ -8,7 +8,7 @@ import type { StreamEvent } from "@langchain/core/tracers/log_stream"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import formatChatHistoryAsString from "../utils/formatHistory"; -import eventEmitter from "events"; +import eventEmitter from "node:events"; import computeSimilarity from "../utils/computeSimilarity"; import logger from "../utils/logger"; @@ -55,7 +55,7 @@ const basicYoutubeSearchResponsePrompt = ` Anything between the \`context\` is retrieved from Youtube and is not a part of the conversation with the user. Today's date is ${new Date().toISOString()} `; -const strParser = new StringOutputParser(); +const stringParser = new StringOutputParser(); const handleStream = async (stream: AsyncGenerator, emitter: eventEmitter) => { for await (const event of stream) { @@ -80,7 +80,7 @@ const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => { return RunnableSequence.from([ PromptTemplate.fromTemplate(basicYoutubeSearchRetrieverPrompt), llm, - strParser, + stringParser, RunnableLambda.from(async (input: string) => { if (input === "not_needed") { return { query: "", docs: [] }; @@ -94,7 +94,7 @@ const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => { const documents = res.results.map( result => new Document({ - pageContent: result.content ? result.content : result.title, + pageContent: result.content ?? result.title, metadata: { title: result.title, url: result.url, @@ -108,30 +108,30 @@ const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => { ]); }; +const processDocs = async (docs: Document[]) => { + return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); +}; + const createBasicYoutubeSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => { const basicYoutubeSearchRetrieverChain = createBasicYoutubeSearchRetrieverChain(llm); - const processDocs = async (docs: Document[]) => { - return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n"); - }; - const rerankDocs = async ({ query, docs }: { query: string; docs: Document[] }) => { if (docs.length === 0) { return docs; } - const docsWithContent = docs.filter(doc => doc.pageContent && doc.pageContent.length > 0); + const docsWithContent = docs.filter(document => document.pageContent && document.pageContent.length > 0); - const [docEmbeddings, queryEmbedding] = await Promise.all([ - embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)), + const [documentEmbeddings, queryEmbedding] = await Promise.all([ + embeddings.embedDocuments(docsWithContent.map(document => document.pageContent)), embeddings.embedQuery(query), ]); - const similarity = docEmbeddings.map((docEmbedding, i) => { - const sim = computeSimilarity(queryEmbedding, docEmbedding); + const similarity = documentEmbeddings.map((documentEmbedding, index) => { + const sim = computeSimilarity(queryEmbedding, documentEmbedding); return { - index: i, + index: index, similarity: sim, }; }); @@ -168,7 +168,7 @@ const createBasicYoutubeSearchAnsweringChain = (llm: BaseChatModel, embeddings: ["user", "{query}"], ]), llm, - strParser, + stringParser, ]).withConfig({ runName: "FinalResponseGenerator", }); @@ -191,9 +191,9 @@ const basicYoutubeSearch = (query: string, history: BaseMessage[], llm: BaseChat ); handleStream(stream, emitter); - } catch (err) { + } catch (error) { emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" })); - logger.error(`Error in youtube search: ${err}`); + logger.error(`Error in youtube search: ${error}`); } return emitter; diff --git a/src/app.ts b/src/app.ts index 05be420..7032e9e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,7 @@ import { startWebSocketServer } from "./websocket"; import express from "express"; import cors from "cors"; -import http from "http"; +import http from "node:http"; import routes from "./routes"; import { getPort } from "./config"; import logger from "./utils/logger"; diff --git a/src/config.ts b/src/config.ts index 82764d5..50a456e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ -import fs from "fs"; -import path from "path"; +/* eslint-disable unicorn/prefer-module */ +import fs from "node:fs"; +import path from "node:path"; import toml from "@iarna/toml"; const configFileName = "config.toml"; @@ -24,7 +25,7 @@ type RecursivePartial = { }; const loadConfig = () => - toml.parse(fs.readFileSync(path.join(__dirname, `../${configFileName}`), "utf-8")) as unknown as Config; + toml.parse(fs.readFileSync(path.join(__dirname, `../${configFileName}`), "utf8")) as unknown as Config; export const getPort = () => loadConfig().GENERAL.PORT; diff --git a/src/db/index.ts b/src/db/index.ts index 77dc199..75980ed 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -3,8 +3,8 @@ import Database from "better-sqlite3"; import * as schema from "./schema"; const sqlite = new Database("data/db.sqlite"); -const db = drizzle(sqlite, { +const database = drizzle(sqlite, { schema: schema, }); -export default db; +export default database; diff --git a/src/lib/huggingfaceTransformer.ts b/src/lib/huggingfaceTransformer.ts index ff77f22..3807ac3 100644 --- a/src/lib/huggingfaceTransformer.ts +++ b/src/lib/huggingfaceTransformer.ts @@ -1,7 +1,7 @@ import { Embeddings, type EmbeddingsParams } from "@langchain/core/embeddings"; import { chunkArray } from "@langchain/core/utils/chunk_array"; -export interface HuggingFaceTransformersEmbeddingsParams extends EmbeddingsParams { +export interface HuggingFaceTransformersEmbeddingsParameters extends EmbeddingsParams { modelName: string; model: string; @@ -13,7 +13,10 @@ export interface HuggingFaceTransformersEmbeddingsParams extends EmbeddingsParam stripNewLines?: boolean; } -export class HuggingFaceTransformersEmbeddings extends Embeddings implements HuggingFaceTransformersEmbeddingsParams { +export class HuggingFaceTransformersEmbeddings + extends Embeddings + implements HuggingFaceTransformersEmbeddingsParameters +{ modelName = "Xenova/all-MiniLM-L6-v2"; model = "Xenova/all-MiniLM-L6-v2"; @@ -27,7 +30,7 @@ export class HuggingFaceTransformersEmbeddings extends Embeddings implements Hug // eslint-disable-next-line @typescript-eslint/no-explicit-any private pipelinePromise: Promise; - constructor(fields?: Partial) { + constructor(fields?: Partial) { super(fields ?? {}); this.modelName = fields?.model ?? fields?.modelName ?? this.model; @@ -37,16 +40,15 @@ export class HuggingFaceTransformersEmbeddings extends Embeddings implements Hug } async embedDocuments(texts: string[]): Promise { - const batches = chunkArray(this.stripNewLines ? texts.map(t => t.replace(/\n/g, " ")) : texts, this.batchSize); + const batches = chunkArray(this.stripNewLines ? texts.map(t => t.replaceAll("\n", " ")) : texts, this.batchSize); const batchRequests = batches.map(batch => this.runEmbedding(batch)); const batchResponses = await Promise.all(batchRequests); const embeddings: number[][] = []; - for (let i = 0; i < batchResponses.length; i += 1) { - const batchResponse = batchResponses[i]; - for (let j = 0; j < batchResponse.length; j += 1) { - embeddings.push(batchResponse[j]); + for (const batchResponse of batchResponses) { + for (const element of batchResponse) { + embeddings.push(element); } } @@ -54,7 +56,7 @@ export class HuggingFaceTransformersEmbeddings extends Embeddings implements Hug } async embedQuery(text: string): Promise { - const data = await this.runEmbedding([this.stripNewLines ? text.replace(/\n/g, " ") : text]); + const data = await this.runEmbedding([this.stripNewLines ? text.replaceAll("\n", " ") : text]); return data[0]; } diff --git a/src/lib/outputParsers/listLineOutputParser.ts b/src/lib/outputParsers/listLineOutputParser.ts index 2cb5fd5..966ebfa 100644 --- a/src/lib/outputParsers/listLineOutputParser.ts +++ b/src/lib/outputParsers/listLineOutputParser.ts @@ -1,15 +1,15 @@ import { BaseOutputParser } from "@langchain/core/output_parsers"; -interface LineListOutputParserArgs { +interface LineListOutputParserArguments { key?: string; } class LineListOutputParser extends BaseOutputParser { private key = "questions"; - constructor(args?: LineListOutputParserArgs) { + constructor(arguments_?: LineListOutputParserArguments) { super(); - this.key = args.key ?? this.key; + this.key = arguments_.key ?? this.key; } static lc_name() { diff --git a/src/lib/providers.ts b/src/lib/providers.ts index 26c4b52..7a7c506 100644 --- a/src/lib/providers.ts +++ b/src/lib/providers.ts @@ -36,8 +36,8 @@ export const getAvailableChatModelProviders = async () => { temperature: 0.7, }), }; - } catch (err) { - logger.error(`Error loading OpenAI models: ${err}`); + } catch (error) { + logger.error(`Error loading OpenAI models: ${error}`); } } @@ -85,8 +85,8 @@ export const getAvailableChatModelProviders = async () => { }, ), }; - } catch (err) { - logger.error(`Error loading Groq models: ${err}`); + } catch (error) { + logger.error(`Error loading Groq models: ${error}`); } } @@ -101,16 +101,17 @@ export const getAvailableChatModelProviders = async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { models: ollamaModels } = (await response.json()) as any; - models["ollama"] = ollamaModels.reduce((acc, model) => { - acc[model.model] = new ChatOllama({ + // eslint-disable-next-line unicorn/no-array-reduce + models["ollama"] = ollamaModels.reduce((accumulator, model) => { + accumulator[model.model] = new ChatOllama({ baseUrl: ollamaEndpoint, model: model.model, temperature: 0.7, }); - return acc; + return accumulator; }, {}); - } catch (err) { - logger.error(`Error loading Ollama models: ${err}`); + } catch (error) { + logger.error(`Error loading Ollama models: ${error}`); } } @@ -137,8 +138,8 @@ export const getAvailableEmbeddingModelProviders = async () => { modelName: "text-embedding-3-large", }), }; - } catch (err) { - logger.error(`Error loading OpenAI embeddings: ${err}`); + } catch (error) { + logger.error(`Error loading OpenAI embeddings: ${error}`); } } @@ -153,15 +154,16 @@ export const getAvailableEmbeddingModelProviders = async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { models: ollamaModels } = (await response.json()) as any; - models["ollama"] = ollamaModels.reduce((acc, model) => { - acc[model.model] = new OllamaEmbeddings({ + // eslint-disable-next-line unicorn/no-array-reduce + models["ollama"] = ollamaModels.reduce((accumulator, model) => { + accumulator[model.model] = new OllamaEmbeddings({ baseUrl: ollamaEndpoint, model: model.model, }); - return acc; + return accumulator; }, {}); - } catch (err) { - logger.error(`Error loading Ollama embeddings: ${err}`); + } catch (error) { + logger.error(`Error loading Ollama embeddings: ${error}`); } } @@ -177,8 +179,8 @@ export const getAvailableEmbeddingModelProviders = async () => { modelName: "Xenova/bert-base-multilingual-uncased", }), }; - } catch (err) { - logger.error(`Error loading local embeddings: ${err}`); + } catch (error) { + logger.error(`Error loading local embeddings: ${error}`); } return models; diff --git a/src/lib/searxng.ts b/src/lib/searxng.ts index d32c590..67beae2 100644 --- a/src/lib/searxng.ts +++ b/src/lib/searxng.ts @@ -19,20 +19,20 @@ interface SearxngSearchResult { iframe_src?: string; } -export const searchSearxng = async (query: string, opts?: SearxngSearchOptions) => { +export const searchSearxng = async (query: string, options?: SearxngSearchOptions) => { const searxngURL = getSearxngApiEndpoint(); const url = new URL(`${searxngURL}/search?format=json`); url.searchParams.append("q", query); - if (opts) { - Object.keys(opts).forEach(key => { - if (Array.isArray(opts[key])) { - url.searchParams.append(key, opts[key].join(",")); - return; + if (options) { + for (const key of Object.keys(options)) { + if (Array.isArray(options[key])) { + url.searchParams.append(key, options[key].join(",")); + continue; } - url.searchParams.append(key, opts[key]); - }); + url.searchParams.append(key, options[key]); + } } const res = await axios.get(url.toString()); diff --git a/src/routes/chats.ts b/src/routes/chats.ts index effe615..62eb0cc 100644 --- a/src/routes/chats.ts +++ b/src/routes/chats.ts @@ -1,6 +1,6 @@ import express from "express"; import logger from "../utils/logger"; -import db from "../db/index"; +import database from "../db/index"; import { eq } from "drizzle-orm"; import { chats, messages } from "../db/schema"; @@ -8,55 +8,55 @@ const router = express.Router(); router.get("/", async (_, res) => { try { - let chats = await db.query.chats.findMany(); + let chats = await database.query.chats.findMany(); chats = chats.reverse(); return res.status(200).json({ chats: chats }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(`Error in getting chats: ${err.message}`); + logger.error(`Error in getting chats: ${error.message}`); } }); -router.get("/:id", async (req, res) => { +router.get("/:id", async (request, res) => { try { - const chatExists = await db.query.chats.findFirst({ - where: eq(chats.id, req.params.id), + const chatExists = await database.query.chats.findFirst({ + where: eq(chats.id, request.params.id), }); if (!chatExists) { return res.status(404).json({ message: "Chat not found" }); } - const chatMessages = await db.query.messages.findMany({ - where: eq(messages.chatId, req.params.id), + const chatMessages = await database.query.messages.findMany({ + where: eq(messages.chatId, request.params.id), }); return res.status(200).json({ chat: chatExists, messages: chatMessages }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(`Error in getting chat: ${err.message}`); + logger.error(`Error in getting chat: ${error.message}`); } }); -router.delete(`/:id`, async (req, res) => { +router.delete(`/:id`, async (request, res) => { try { - const chatExists = await db.query.chats.findFirst({ - where: eq(chats.id, req.params.id), + const chatExists = await database.query.chats.findFirst({ + where: eq(chats.id, request.params.id), }); if (!chatExists) { return res.status(404).json({ message: "Chat not found" }); } - await db.delete(chats).where(eq(chats.id, req.params.id)).execute(); - await db.delete(messages).where(eq(messages.chatId, req.params.id)).execute(); + await database.delete(chats).where(eq(chats.id, request.params.id)).execute(); + await database.delete(messages).where(eq(messages.chatId, request.params.id)).execute(); return res.status(200).json({ message: "Chat deleted successfully" }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(`Error in deleting chat: ${err.message}`); + logger.error(`Error in deleting chat: ${error.message}`); } }); diff --git a/src/routes/config.ts b/src/routes/config.ts index 867ed02..bf727ea 100644 --- a/src/routes/config.ts +++ b/src/routes/config.ts @@ -30,8 +30,8 @@ router.get("/", async (_, res) => { res.status(200).json(config); }); -router.post("/", async (req, res) => { - const config = req.body; +router.post("/", async (request, res) => { + const config = request.body; const updatedConfig = { API_KEYS: { diff --git a/src/routes/images.ts b/src/routes/images.ts index 17a1b5d..ce3db10 100644 --- a/src/routes/images.ts +++ b/src/routes/images.ts @@ -7,16 +7,16 @@ import logger from "../utils/logger"; const router = express.Router(); -router.post("/", async (req, res) => { +router.post("/", async (request, res) => { try { - const { query, chat_history: raw_chat_history, chat_model_provider, chat_model } = req.body; + const { query, chat_history: raw_chat_history, chat_model_provider, chat_model } = request.body; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const chat_history = raw_chat_history.map((msg: any) => { - if (msg.role === "user") { - return new HumanMessage(msg.content); - } else if (msg.role === "assistant") { - return new AIMessage(msg.content); + const chat_history = raw_chat_history.map((message: any) => { + if (message.role === "user") { + return new HumanMessage(message.content); + } else if (message.role === "assistant") { + return new AIMessage(message.content); } }); @@ -38,9 +38,9 @@ router.post("/", async (req, res) => { const images = await handleImageSearch({ query, chat_history }, llm); res.status(200).json({ images }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(`Error in image search: ${err.message}`); + logger.error(`Error in image search: ${error.message}`); } }); diff --git a/src/routes/models.ts b/src/routes/models.ts index 43d48c8..7338ba0 100644 --- a/src/routes/models.ts +++ b/src/routes/models.ts @@ -4,7 +4,7 @@ import { getAvailableChatModelProviders, getAvailableEmbeddingModelProviders } f const router = express.Router(); -router.get("/", async (req, res) => { +router.get("/", async (request, res) => { try { const [chatModelProviders, embeddingModelProviders] = await Promise.all([ getAvailableChatModelProviders(), @@ -12,9 +12,9 @@ router.get("/", async (req, res) => { ]); res.status(200).json({ chatModelProviders, embeddingModelProviders }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(err.message); + logger.error(error.message); } }); diff --git a/src/routes/suggestions.ts b/src/routes/suggestions.ts index 34a4fb0..046e75b 100644 --- a/src/routes/suggestions.ts +++ b/src/routes/suggestions.ts @@ -7,16 +7,16 @@ import logger from "../utils/logger"; const router = express.Router(); -router.post("/", async (req, res) => { +router.post("/", async (request, res) => { try { - const { chat_history: raw_chat_history, chat_model, chat_model_provider } = req.body; + const { chat_history: raw_chat_history, chat_model, chat_model_provider } = request.body; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const chat_history = raw_chat_history.map((msg: any) => { - if (msg.role === "user") { - return new HumanMessage(msg.content); - } else if (msg.role === "assistant") { - return new AIMessage(msg.content); + const chat_history = raw_chat_history.map((message: any) => { + if (message.role === "user") { + return new HumanMessage(message.content); + } else if (message.role === "assistant") { + return new AIMessage(message.content); } }); @@ -38,9 +38,9 @@ router.post("/", async (req, res) => { const suggestions = await generateSuggestions({ chat_history }, llm); res.status(200).json({ suggestions: suggestions }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(`Error in generating suggestions: ${err.message}`); + logger.error(`Error in generating suggestions: ${error.message}`); } }); diff --git a/src/routes/videos.ts b/src/routes/videos.ts index e5ca446..2a97a87 100644 --- a/src/routes/videos.ts +++ b/src/routes/videos.ts @@ -7,16 +7,16 @@ import handleVideoSearch from "../agents/videoSearchAgent"; const router = express.Router(); -router.post("/", async (req, res) => { +router.post("/", async (request, res) => { try { - const { query, chat_history: raw_chat_history, chat_model_provider, chat_model } = req.body; + const { query, chat_history: raw_chat_history, chat_model_provider, chat_model } = request.body; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const chat_history = raw_chat_history.map((msg: any) => { - if (msg.role === "user") { - return new HumanMessage(msg.content); - } else if (msg.role === "assistant") { - return new AIMessage(msg.content); + const chat_history = raw_chat_history.map((message: any) => { + if (message.role === "user") { + return new HumanMessage(message.content); + } else if (message.role === "assistant") { + return new AIMessage(message.content); } }); @@ -38,9 +38,9 @@ router.post("/", async (req, res) => { const videos = await handleVideoSearch({ chat_history, query }, llm); res.status(200).json({ videos }); - } catch (err) { + } catch (error) { res.status(500).json({ message: "An error has occurred." }); - logger.error(`Error in video search: ${err.message}`); + logger.error(`Error in video search: ${error.message}`); } }); diff --git a/src/websocket/connectionManager.ts b/src/websocket/connectionManager.ts index 3dac8ca..3cb10b9 100644 --- a/src/websocket/connectionManager.ts +++ b/src/websocket/connectionManager.ts @@ -3,26 +3,26 @@ import { handleMessage } from "./messageHandler"; import { getAvailableEmbeddingModelProviders, getAvailableChatModelProviders } from "../lib/providers"; import { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; -import type { IncomingMessage } from "http"; +import type { IncomingMessage } from "node:http"; import logger from "../utils/logger"; import { ChatOpenAI } from "@langchain/openai"; export const handleConnection = async (ws: WebSocket, request: IncomingMessage) => { try { - const searchParams = new URL(request.url, `http://${request.headers.host}`).searchParams; + const searchParameters = new URL(request.url, `http://${request.headers.host}`).searchParams; const [chatModelProviders, embeddingModelProviders] = await Promise.all([ getAvailableChatModelProviders(), getAvailableEmbeddingModelProviders(), ]); - const chatModelProvider = searchParams.get("chatModelProvider") || Object.keys(chatModelProviders)[0]; - const chatModel = searchParams.get("chatModel") || Object.keys(chatModelProviders[chatModelProvider])[0]; + const chatModelProvider = searchParameters.get("chatModelProvider") || Object.keys(chatModelProviders)[0]; + const chatModel = searchParameters.get("chatModel") || Object.keys(chatModelProviders[chatModelProvider])[0]; const embeddingModelProvider = - searchParams.get("embeddingModelProvider") || Object.keys(embeddingModelProviders)[0]; + searchParameters.get("embeddingModelProvider") || Object.keys(embeddingModelProviders)[0]; const embeddingModel = - searchParams.get("embeddingModel") || Object.keys(embeddingModelProviders[embeddingModelProvider])[0]; + searchParameters.get("embeddingModel") || Object.keys(embeddingModelProviders[embeddingModelProvider])[0]; let llm: BaseChatModel | undefined; let embeddings: Embeddings | undefined; @@ -36,10 +36,10 @@ export const handleConnection = async (ws: WebSocket, request: IncomingMessage) } else if (chatModelProvider == "custom_openai") { llm = new ChatOpenAI({ modelName: chatModel, - openAIApiKey: searchParams.get("openAIApiKey"), + openAIApiKey: searchParameters.get("openAIApiKey"), temperature: 0.7, configuration: { - baseURL: searchParams.get("openAIBaseURL"), + baseURL: searchParameters.get("openAIBaseURL"), }, }); } @@ -65,7 +65,7 @@ export const handleConnection = async (ws: WebSocket, request: IncomingMessage) ws.on("message", async message => await handleMessage(message.toString(), ws, llm, embeddings)); ws.on("close", () => logger.debug("Connection closed")); - } catch (err) { + } catch (error) { ws.send( JSON.stringify({ type: "error", @@ -74,6 +74,6 @@ export const handleConnection = async (ws: WebSocket, request: IncomingMessage) }), ); ws.close(); - logger.error(err); + logger.error(error); } }; diff --git a/src/websocket/index.ts b/src/websocket/index.ts index 53f2dc4..be32a6e 100644 --- a/src/websocket/index.ts +++ b/src/websocket/index.ts @@ -1,5 +1,5 @@ import { initServer } from "./websocketServer"; -import http from "http"; +import http from "node:http"; export const startWebSocketServer = (server: http.Server) => { initServer(server); diff --git a/src/websocket/messageHandler.ts b/src/websocket/messageHandler.ts index 2426bec..6f30762 100644 --- a/src/websocket/messageHandler.ts +++ b/src/websocket/messageHandler.ts @@ -9,10 +9,10 @@ import handleRedditSearch from "../agents/redditSearchAgent"; import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; import type { Embeddings } from "@langchain/core/embeddings"; import logger from "../utils/logger"; -import db from "../db"; +import database from "../db"; import { chats, messages } from "../db/schema"; import { eq } from "drizzle-orm"; -import crypto from "crypto"; +import crypto from "node:crypto"; type Message = { messageId: string; @@ -66,7 +66,8 @@ const handleEmitterEvents = (emitter: EventEmitter, ws: WebSocket, messageId: st emitter.on("end", () => { ws.send(JSON.stringify({ type: "messageEnd", messageId: messageId })); - db.insert(messages) + database + .insert(messages) .values({ content: recievedMessage, chatId: chatId, @@ -107,16 +108,14 @@ export const handleMessage = async (message: string, ws: WebSocket, llm: BaseCha }), ); - const history: BaseMessage[] = parsedWSMessage.history.map(msg => { - if (msg[0] === "human") { - return new HumanMessage({ - content: msg[1], - }); - } else { - return new AIMessage({ - content: msg[1], - }); - } + const history: BaseMessage[] = parsedWSMessage.history.map(message_ => { + return message_[0] === "human" + ? new HumanMessage({ + content: message_[1], + }) + : new AIMessage({ + content: message_[1], + }); }); if (parsedWSMessage.type === "message") { @@ -127,12 +126,12 @@ export const handleMessage = async (message: string, ws: WebSocket, llm: BaseCha handleEmitterEvents(emitter, ws, id, parsedMessage.chatId); - const chat = await db.query.chats.findFirst({ + const chat = await database.query.chats.findFirst({ where: eq(chats.id, parsedMessage.chatId), }); if (!chat) { - await db + await database .insert(chats) .values({ id: parsedMessage.chatId, @@ -143,7 +142,7 @@ export const handleMessage = async (message: string, ws: WebSocket, llm: BaseCha .execute(); } - await db + await database .insert(messages) .values({ content: parsedMessage.content, @@ -165,7 +164,7 @@ export const handleMessage = async (message: string, ws: WebSocket, llm: BaseCha ); } } - } catch (err) { + } catch (error) { ws.send( JSON.stringify({ type: "error", @@ -173,6 +172,6 @@ export const handleMessage = async (message: string, ws: WebSocket, llm: BaseCha key: "INVALID_FORMAT", }), ); - logger.error(`Failed to handle message: ${err}`); + logger.error(`Failed to handle message: ${error}`); } }; diff --git a/src/websocket/websocketServer.ts b/src/websocket/websocketServer.ts index b3357af..e6a3d79 100644 --- a/src/websocket/websocketServer.ts +++ b/src/websocket/websocketServer.ts @@ -1,6 +1,6 @@ import { WebSocketServer } from "ws"; import { handleConnection } from "./connectionManager"; -import http from "http"; +import http from "node:http"; import { getPort } from "../config"; import logger from "../utils/logger"; diff --git a/tsconfig.json b/tsconfig.json index 48e6042..7821a4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,9 @@ "emitDecoratorMetadata": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true, - "skipDefaultLibCheck": true + "skipDefaultLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["src"], "exclude": ["node_modules", "**/*.spec.ts"] diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index 84c85a3..4819957 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -1,3 +1,23 @@ { - "overrides": [] + "extends": ["../.eslintrc.json"], + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "plugins": ["react", "react-hooks"], + "extends": ["plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:react/jsx-runtime"] + }, + { + "files": [ + "postcss.config.js", + "tailwind.config.js", + "tailwind.config.ts" + ], + "rules": { + "unicorn/prefer-module": "off" + }, + "env": { + "node": true + } + } + ] } diff --git a/ui/app/library/page.tsx b/ui/app/library/page.tsx index ac11201..b039859 100644 --- a/ui/app/library/page.tsx +++ b/ui/app/library/page.tsx @@ -71,10 +71,10 @@ const Page = () => { )} {chats.length > 0 && (
- {chats.map((chat, i) => ( + {chats.map((chat, index) => (
void; }) => { const [dividerWidth, setDividerWidth] = useState(0); - const dividerRef = useRef(null); + const dividerReference = useRef(null); const messageEnd = useRef(null); useEffect(() => { const updateDividerWidth = () => { - if (dividerRef.current) { - setDividerWidth(dividerRef.current.scrollWidth); + if (dividerReference.current) { + setDividerWidth(dividerReference.current.scrollWidth); } }; @@ -43,29 +43,29 @@ const Chat = ({ messageEnd.current?.scrollIntoView({ behavior: "smooth" }); if (messages.length === 1) { - document.title = `${messages[0].content.substring(0, 30)} - Perplexica`; + document.title = `${messages[0].content.slice(0, 30)} - Perplexica`; } }, [messages]); return (
- {messages.map((msg, i) => { - const isLast = i === messages.length - 1; + {messages.map((message, index) => { + const isLast = index === messages.length - 1; return ( - + - {!isLast && msg.role === "assistant" && ( + {!isLast && message.role === "assistant" && (
)} diff --git a/ui/components/ChatWindow.tsx b/ui/components/ChatWindow.tsx index 5ba6882..b95f530 100644 --- a/ui/components/ChatWindow.tsx +++ b/ui/components/ChatWindow.tsx @@ -6,7 +6,7 @@ import { Document } from "@langchain/core/documents"; import Navbar from "./Navbar"; import Chat from "./Chat"; import EmptyChat from "./EmptyChat"; -import crypto from "crypto"; +import crypto from "node:crypto"; import { toast } from "sonner"; import { useSearchParams } from "next/navigation"; import { getSuggestions } from "@/lib/actions"; @@ -62,20 +62,20 @@ const useSocket = (url: string, setIsWSReady: (ready: boolean) => void, setError } const wsURL = new URL(url); - const searchParams = new URLSearchParams({}); + const searchParameters = new URLSearchParams({}); - searchParams.append("chatModel", chatModel!); - searchParams.append("chatModelProvider", chatModelProvider); + searchParameters.append("chatModel", chatModel!); + searchParameters.append("chatModelProvider", chatModelProvider); if (chatModelProvider === "custom_openai") { - searchParams.append("openAIApiKey", localStorage.getItem("openAIApiKey")!); - searchParams.append("openAIBaseURL", localStorage.getItem("openAIBaseURL")!); + searchParameters.append("openAIApiKey", localStorage.getItem("openAIApiKey")!); + searchParameters.append("openAIBaseURL", localStorage.getItem("openAIBaseURL")!); } - searchParams.append("embeddingModel", embeddingModel!); - searchParams.append("embeddingModelProvider", embeddingModelProvider); + searchParameters.append("embeddingModel", embeddingModel!); + searchParameters.append("embeddingModelProvider", embeddingModelProvider); - wsURL.search = searchParams.toString(); + wsURL.search = searchParameters.toString(); const ws = new WebSocket(wsURL.toString()); @@ -85,26 +85,27 @@ const useSocket = (url: string, setIsWSReady: (ready: boolean) => void, setError setError(true); toast.error("Failed to connect to the server. Please try again later."); } - }, 10000); + }, 10_000); - ws.onopen = () => { + ws.addEventListener("open", () => { console.log("[DEBUG] open"); clearTimeout(timeoutId); setError(false); setIsWSReady(true); - }; + }); + // eslint-disable-next-line unicorn/prefer-add-event-listener ws.onerror = () => { clearTimeout(timeoutId); setError(true); toast.error("WebSocket connection error."); }; - ws.onclose = () => { + ws.addEventListener("close", () => { clearTimeout(timeoutId); setError(true); console.log("[DEBUG] closed"); - }; + }); setWs(ws); }; @@ -144,17 +145,17 @@ const loadMessages = async ( const data = await res.json(); - const messages = data.messages.map((msg: any) => { + const messages = data.messages.map((message: any) => { return { - ...msg, - ...JSON.parse(msg.metadata), + ...message, + ...JSON.parse(message.metadata), }; }) as Message[]; setMessages(messages); - const history = messages.map(msg => { - return [msg.role, msg.content]; + const history = messages.map(message => { + return [message.role, message.content]; }) as [string, string][]; console.log("[DEBUG] messages loaded"); @@ -167,8 +168,8 @@ const loadMessages = async ( }; const ChatWindow = ({ id }: { id?: string }) => { - const searchParams = useSearchParams(); - const initialMessage = searchParams.get("q"); + const searchParameters = useSearchParams(); + const initialMessage = searchParameters.get("q"); const [chatId, setChatId] = useState(id); const [newChatCreated, setNewChatCreated] = useState(false); @@ -202,10 +203,10 @@ const ChatWindow = ({ id }: { id?: string }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const messagesRef = useRef([]); + const messagesReference = useRef([]); useEffect(() => { - messagesRef.current = messages; + messagesReference.current = messages; }, [messages]); useEffect(() => { @@ -219,7 +220,7 @@ const ChatWindow = ({ id }: { id?: string }) => { setLoading(true); setMessageAppeared(false); - let sources: Document[] | undefined = undefined; + let sources: Document[] | undefined; let recievedMessage = ""; let added = false; @@ -237,8 +238,8 @@ const ChatWindow = ({ id }: { id?: string }) => { }), ); - setMessages(prevMessages => [ - ...prevMessages, + setMessages(previousMessages => [ + ...previousMessages, { content: message, messageId: messageId, @@ -260,8 +261,8 @@ const ChatWindow = ({ id }: { id?: string }) => { if (data.type === "sources") { sources = data.data; if (!added) { - setMessages(prevMessages => [ - ...prevMessages, + setMessages(previousMessages => [ + ...previousMessages, { content: "", messageId: data.messageId, @@ -278,8 +279,8 @@ const ChatWindow = ({ id }: { id?: string }) => { if (data.type === "message") { if (!added) { - setMessages(prevMessages => [ - ...prevMessages, + setMessages(previousMessages => [ + ...previousMessages, { content: data.data, messageId: data.messageId, @@ -292,8 +293,8 @@ const ChatWindow = ({ id }: { id?: string }) => { added = true; } - setMessages(prev => - prev.map(message => { + setMessages(previous => + previous.map(message => { if (message.messageId === data.messageId) { return { ...message, content: message.content + data.data }; } @@ -307,21 +308,27 @@ const ChatWindow = ({ id }: { id?: string }) => { } if (data.type === "messageEnd") { - setChatHistory(prevHistory => [...prevHistory, ["human", message], ["assistant", recievedMessage]]); + setChatHistory(previousHistory => [...previousHistory, ["human", message], ["assistant", recievedMessage]]); ws?.removeEventListener("message", messageHandler); setLoading(false); - const lastMsg = messagesRef.current[messagesRef.current.length - 1]; + const lastMessage = messagesReference.current.at(-1); - if (lastMsg.role === "assistant" && lastMsg.sources && lastMsg.sources.length > 0 && !lastMsg.suggestions) { - const suggestions = await getSuggestions(messagesRef.current); - setMessages(prev => - prev.map(msg => { - if (msg.messageId === lastMsg.messageId) { - return { ...msg, suggestions: suggestions }; + if ( + lastMessage && + lastMessage.role === "assistant" && + lastMessage.sources && + lastMessage.sources.length > 0 && + !lastMessage.suggestions + ) { + const suggestions = await getSuggestions(messagesReference.current); + setMessages(previous => + previous.map(message_ => { + if (message_.messageId === lastMessage.messageId) { + return { ...message_, suggestions: suggestions }; } - return msg; + return message_; }), ); } @@ -332,17 +339,19 @@ const ChatWindow = ({ id }: { id?: string }) => { }; const rewrite = (messageId: string) => { - const index = messages.findIndex(msg => msg.messageId === messageId); + const index = messages.findIndex(message_ => message_.messageId === messageId); if (index === -1) return; const message = messages[index - 1]; - setMessages(prev => { - return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; + setMessages(previous => { + // eslint-disable-next-line unicorn/no-useless-spread + return [...previous.slice(0, messages.length > 2 ? index - 1 : 0)]; }); - setChatHistory(prev => { - return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; + setChatHistory(previous => { + // eslint-disable-next-line unicorn/no-useless-spread + return [...previous.slice(0, messages.length > 2 ? index - 1 : 0)]; }); sendMessage(message.content); diff --git a/ui/components/DeleteChat.tsx b/ui/components/DeleteChat.tsx index 366bb6f..a379d02 100644 --- a/ui/components/DeleteChat.tsx +++ b/ui/components/DeleteChat.tsx @@ -33,8 +33,9 @@ const DeleteChat = ({ const newChats = chats.filter(chat => chat.id !== chatId); setChats(newChats); - } catch (err: any) { - toast.error(err.message); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + toast.error(error.message); } finally { setConfirmationDialogOpen(false); setLoading(false); diff --git a/ui/components/EmptyChatMessageInput.tsx b/ui/components/EmptyChatMessageInput.tsx index bd106be..052638c 100644 --- a/ui/components/EmptyChatMessageInput.tsx +++ b/ui/components/EmptyChatMessageInput.tsx @@ -16,12 +16,12 @@ const EmptyChatMessageInput = ({ const [copilotEnabled, setCopilotEnabled] = useState(false); const [message, setMessage] = useState(""); - const inputRef = useRef(null); + const inputReference = useRef(null); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "/") { e.preventDefault(); - inputRef.current?.focus(); + inputReference.current?.focus(); } }; @@ -51,7 +51,7 @@ const EmptyChatMessageInput = ({ >
setMessage(e.target.value)} minRows={2} diff --git a/ui/components/MessageActions/Copy.tsx b/ui/components/MessageActions/Copy.tsx index 79d3a77..63fa2a2 100644 --- a/ui/components/MessageActions/Copy.tsx +++ b/ui/components/MessageActions/Copy.tsx @@ -8,7 +8,8 @@ const Copy = ({ message, initialMessage }: { message: Message; initialMessage: s return (
- {message.suggestions.map((suggestion, i) => ( -
+ {message.suggestions.map((suggestion, index) => ( +
{ diff --git a/ui/components/MessageInput.tsx b/ui/components/MessageInput.tsx index 072c758..75bc293 100644 --- a/ui/components/MessageInput.tsx +++ b/ui/components/MessageInput.tsx @@ -19,12 +19,12 @@ const MessageInput = ({ sendMessage, loading }: { sendMessage: (message: string) } }, [textareaRows, mode, message]); - const inputRef = useRef(null); + const inputReference = useRef(null); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "/") { e.preventDefault(); - inputRef.current?.focus(); + inputReference.current?.focus(); } }; @@ -58,11 +58,11 @@ const MessageInput = ({ sendMessage, loading }: { sendMessage: (message: string) > {mode === "single" && } setMessage(e.target.value)} - onHeightChange={(height, props) => { - setTextareaRows(Math.ceil(height / props.rowHeight)); + onHeightChange={(height, properties) => { + setTextareaRows(Math.ceil(height / properties.rowHeight)); }} className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink" placeholder="Ask a follow-up" diff --git a/ui/components/MessageInputActions/Focus.tsx b/ui/components/MessageInputActions/Focus.tsx index 9072eca..b5c02f0 100644 --- a/ui/components/MessageInputActions/Focus.tsx +++ b/ui/components/MessageInputActions/Focus.tsx @@ -54,14 +54,14 @@ const Focus = ({ focusMode, setFocusMode }: { focusMode: string; setFocusMode: ( type="button" className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white" > - {focusMode !== "webSearch" ? ( + {focusMode === "webSearch" ? ( + + ) : (
{focusModes.find(mode => mode.key === focusMode)?.icon}

{focusModes.find(mode => mode.key === focusMode)?.title}

- ) : ( - )}
- {focusModes.map((mode, i) => ( + {focusModes.map((mode, index) => ( setFocusMode(mode.key)} - key={i} + key={index} className={cn( "p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition", focusMode === mode.key diff --git a/ui/components/MessageSources.tsx b/ui/components/MessageSources.tsx index 8724988..bceb3bf 100644 --- a/ui/components/MessageSources.tsx +++ b/ui/components/MessageSources.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @next/next/no-img-element */ import { Dialog, Transition } from "@headlessui/react"; import { Document } from "@langchain/core/documents"; import { Fragment, useState } from "react"; @@ -18,12 +17,13 @@ const MessageSources = ({ sources }: { sources: Document[] }) => { return (
- {i + 1} + {index + 1}
@@ -54,14 +54,14 @@ const MessageSources = ({ sources }: { sources: Document[] }) => { className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium" >
- {sources.slice(3, 6).map((source, i) => ( + {sources.slice(3, 6).map((source, index) => ( favicon ))}
@@ -84,12 +84,13 @@ const MessageSources = ({ sources }: { sources: Document[] }) => { Sources
- {i + 1} + {index + 1}
diff --git a/ui/components/Navbar.tsx b/ui/components/Navbar.tsx index 184926a..be89b7b 100644 --- a/ui/components/Navbar.tsx +++ b/ui/components/Navbar.tsx @@ -10,7 +10,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => { useEffect(() => { if (messages.length > 0) { const newTitle = - messages[0].content.length > 20 ? `${messages[0].content.substring(0, 20).trim()}...` : messages[0].content; + messages[0].content.length > 20 ? `${messages[0].content.slice(0, 20).trim()}...` : messages[0].content; setTitle(newTitle); const newTimeAgo = formatTimeDifference(new Date(), messages[0].createdAt); setTimeAgo(newTimeAgo); diff --git a/ui/components/SearchImages.tsx b/ui/components/SearchImages.tsx index 9fa1850..404c588 100644 --- a/ui/components/SearchImages.tsx +++ b/ui/components/SearchImages.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @next/next/no-img-element */ import { ImagesIcon, PlusIcon } from "lucide-react"; import { useState } from "react"; import Lightbox from "yet-another-react-lightbox"; @@ -15,6 +14,7 @@ const SearchImages = ({ query, chat_history }: { query: string; chat_history: Me const [images, setImages] = useState(null); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [slides, setSlides] = useState([]); return ( @@ -64,9 +64,9 @@ const SearchImages = ({ query, chat_history }: { query: string; chat_history: Me )} {loading && (
- {[...Array(4)].map((_, i) => ( + {Array.from({ length: 4 }).map((_, index) => (
))} @@ -76,25 +76,25 @@ const SearchImages = ({ query, chat_history }: { query: string; chat_history: Me <>
{images.length > 4 - ? images.slice(0, 3).map((image, i) => ( + ? images.slice(0, 3).map((image, index) => ( { setOpen(true); - setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]); + setSlides([slides[index], ...slides.slice(0, index), ...slides.slice(index + 1)]); }} - key={i} + key={index} src={image.img_src} alt={image.title} className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in" /> )) - : images.map((image, i) => ( + : images.map((image, index) => ( { setOpen(true); - setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]); + setSlides([slides[index], ...slides.slice(0, index), ...slides.slice(index + 1)]); }} - key={i} + key={index} src={image.img_src} alt={image.title} className="h-full w-full aspect-video object-cover rounded-lg transition duration-200 active:scale-95 hover:scale-[1.02] cursor-zoom-in" @@ -106,9 +106,9 @@ const SearchImages = ({ query, chat_history }: { query: string; chat_history: Me className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2" >
- {images.slice(3, 6).map((image, i) => ( + {images.slice(3, 6).map((image, index) => ( {image.title} - {[...Array(4)].map((_, i) => ( + {Array.from({ length: 4 }).map((_, index) => (
))} @@ -91,14 +90,14 @@ const Searchvideos = ({ query, chat_history }: { query: string; chat_history: Me <>
{videos.length > 4 - ? videos.slice(0, 3).map((video, i) => ( + ? videos.slice(0, 3).map((video, index) => (
{ setOpen(true); - setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]); + setSlides([slides[index], ...slides.slice(0, index), ...slides.slice(index + 1)]); }} className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer" - key={i} + key={index} >
)) - : videos.map((video, i) => ( + : videos.map((video, index) => (
{ setOpen(true); - setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]); + setSlides([slides[index], ...slides.slice(0, index), ...slides.slice(index + 1)]); }} className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer" - key={i} + key={index} >
- {videos.slice(3, 6).map((video, i) => ( + {videos.slice(3, 6).map((video, index) => ( {video.title} {} +interface InputProperties extends React.InputHTMLAttributes {} -const Input = ({ className, ...restProps }: InputProps) => { +const Input = ({ className, ...restProperties }: InputProperties) => { return ( { ); }; -interface SelectProps extends SelectHTMLAttributes { +interface SelectProperties extends SelectHTMLAttributes { options: { value: string; label: string; disabled?: boolean }[]; } -export const Select = ({ className, options, ...restProps }: SelectProps) => { +export const Select = ({ className, options, ...restProperties }: SelectProperties) => { return (