chore: Update dependencies and fix import paths
This commit is contained in:
parent
3b737a078a
commit
81c5e30fda
46 changed files with 1626 additions and 371 deletions
|
@ -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": [
|
||||
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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<StreamEvent, unknown, unknown>, 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 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 createBasicAcademicSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => {
|
||||
const basicAcademicSearchRetrieverChain = createBasicAcademicSearchRetrieverChain(llm);
|
||||
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
@ -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<StreamEvent, unknown, unknown>, 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 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 createBasicRedditSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => {
|
||||
const basicRedditSearchRetrieverChain = createBasicRedditSearchRetrieverChain(llm);
|
||||
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
@ -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<StreamEvent, unknown, unknown>, 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 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 createBasicWebSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => {
|
||||
const basicWebSearchRetrieverChain = createBasicWebSearchRetrieverChain(llm);
|
||||
|
||||
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;
|
||||
|
|
|
@ -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<StreamEvent, unknown, unknown>, 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 createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
|
||||
const basicWolframAlphaSearchRetrieverChain = createBasicWolframAlphaSearchRetrieverChain(llm);
|
||||
|
||||
const processDocs = (docs: Document[]) => {
|
||||
return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n");
|
||||
};
|
||||
|
||||
const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
|
||||
const basicWolframAlphaSearchRetrieverChain = createBasicWolframAlphaSearchRetrieverChain(llm);
|
||||
|
||||
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;
|
||||
|
|
|
@ -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<StreamEvent, unknown, unknown>, 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;
|
||||
|
|
|
@ -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<StreamEvent, unknown, unknown>, 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 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 createBasicYoutubeSearchAnsweringChain = (llm: BaseChatModel, embeddings: Embeddings) => {
|
||||
const basicYoutubeSearchRetrieverChain = createBasicYoutubeSearchRetrieverChain(llm);
|
||||
|
||||
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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<T> = {
|
|||
};
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<any>;
|
||||
|
||||
constructor(fields?: Partial<HuggingFaceTransformersEmbeddingsParams>) {
|
||||
constructor(fields?: Partial<HuggingFaceTransformersEmbeddingsParameters>) {
|
||||
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<number[][]> {
|
||||
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<number[]> {
|
||||
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];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { BaseOutputParser } from "@langchain/core/output_parsers";
|
||||
|
||||
interface LineListOutputParserArgs {
|
||||
interface LineListOutputParserArguments {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
class LineListOutputParser extends BaseOutputParser<string[]> {
|
||||
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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, options[key]);
|
||||
}
|
||||
url.searchParams.append(key, opts[key]);
|
||||
});
|
||||
}
|
||||
|
||||
const res = await axios.get(url.toString());
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { initServer } from "./websocketServer";
|
||||
import http from "http";
|
||||
import http from "node:http";
|
||||
|
||||
export const startWebSocketServer = (server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>) => {
|
||||
initServer(server);
|
||||
|
|
|
@ -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],
|
||||
const history: BaseMessage[] = parsedWSMessage.history.map(message_ => {
|
||||
return message_[0] === "human"
|
||||
? new HumanMessage({
|
||||
content: message_[1],
|
||||
})
|
||||
: new AIMessage({
|
||||
content: message_[1],
|
||||
});
|
||||
} else {
|
||||
return new AIMessage({
|
||||
content: msg[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}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -71,10 +71,10 @@ const Page = () => {
|
|||
)}
|
||||
{chats.length > 0 && (
|
||||
<div className="flex flex-col pt-16 lg:pt-24">
|
||||
{chats.map((chat, i) => (
|
||||
{chats.map((chat, index) => (
|
||||
<div
|
||||
className="flex flex-col space-y-4 border-b border-white-200 dark:border-dark-200 py-6 lg:mx-4"
|
||||
key={i}
|
||||
key={index}
|
||||
>
|
||||
<Link
|
||||
href={`/c/${chat.id}`}
|
||||
|
|
|
@ -20,13 +20,13 @@ const Chat = ({
|
|||
rewrite: (messageId: string) => void;
|
||||
}) => {
|
||||
const [dividerWidth, setDividerWidth] = useState(0);
|
||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||
const dividerReference = useRef<HTMLDivElement | null>(null);
|
||||
const messageEnd = useRef<HTMLDivElement | null>(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 (
|
||||
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
|
||||
{messages.map((msg, i) => {
|
||||
const isLast = i === messages.length - 1;
|
||||
{messages.map((message, index) => {
|
||||
const isLast = index === messages.length - 1;
|
||||
|
||||
return (
|
||||
<Fragment key={msg.messageId}>
|
||||
<Fragment key={message.messageId}>
|
||||
<MessageBox
|
||||
key={i}
|
||||
message={msg}
|
||||
messageIndex={i}
|
||||
key={index}
|
||||
message={message}
|
||||
messageIndex={index}
|
||||
history={messages}
|
||||
loading={loading}
|
||||
dividerRef={isLast ? dividerRef : undefined}
|
||||
dividerRef={isLast ? dividerReference : undefined}
|
||||
isLast={isLast}
|
||||
rewrite={rewrite}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
{!isLast && msg.role === "assistant" && (
|
||||
{!isLast && message.role === "assistant" && (
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
)}
|
||||
</Fragment>
|
||||
|
|
|
@ -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<string | undefined>(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<Message[]>([]);
|
||||
const messagesReference = useRef<Message[]>([]);
|
||||
|
||||
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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -16,12 +16,12 @@ const EmptyChatMessageInput = ({
|
|||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const inputReference = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
inputReference.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -51,7 +51,7 @@ const EmptyChatMessageInput = ({
|
|||
>
|
||||
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
|
||||
<TextareaAutosize
|
||||
ref={inputRef}
|
||||
ref={inputReference}
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
minRows={2}
|
||||
|
|
|
@ -8,7 +8,8 @@ const Copy = ({ message, initialMessage }: { message: Message; initialMessage: s
|
|||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, index: any) => `[${index + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||
navigator.clipboard.writeText(contentToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
|
|
|
@ -36,11 +36,11 @@ const MessageBox = ({
|
|||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||
|
||||
useEffect(() => {
|
||||
const regex = /\[(\d+)\]/g;
|
||||
const regex = /\[(\d+)]/g;
|
||||
|
||||
if (message.role === "assistant" && message?.sources && message.sources.length > 0) {
|
||||
return setParsedMessage(
|
||||
message.content.replace(
|
||||
message.content.replaceAll(
|
||||
regex,
|
||||
(_, number) =>
|
||||
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
|
||||
|
@ -48,7 +48,7 @@ const MessageBox = ({
|
|||
);
|
||||
}
|
||||
|
||||
setSpeechMessage(message.content.replace(regex, ""));
|
||||
setSpeechMessage(message.content.replaceAll(regex, ""));
|
||||
setParsedMessage(message.content);
|
||||
}, [message.content, message.sources, message.role]);
|
||||
|
||||
|
@ -128,8 +128,8 @@ const MessageBox = ({
|
|||
<h3 className="text-xl font-medium">Related</h3>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
{message.suggestions.map((suggestion, i) => (
|
||||
<div className="flex flex-col space-y-3 text-sm" key={i}>
|
||||
{message.suggestions.map((suggestion, index) => (
|
||||
<div className="flex flex-col space-y-3 text-sm" key={index}>
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
<div
|
||||
onClick={() => {
|
||||
|
|
|
@ -19,12 +19,12 @@ const MessageInput = ({ sendMessage, loading }: { sendMessage: (message: string)
|
|||
}
|
||||
}, [textareaRows, mode, message]);
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const inputReference = useRef<HTMLTextAreaElement | null>(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" && <Attach />}
|
||||
<TextareaAutosize
|
||||
ref={inputRef}
|
||||
ref={inputReference}
|
||||
value={message}
|
||||
onChange={e => 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"
|
||||
|
|
|
@ -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" ? (
|
||||
<ScanEye />
|
||||
) : (
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{focusModes.find(mode => mode.key === focusMode)?.icon}
|
||||
<p className="text-xs font-medium">{focusModes.find(mode => mode.key === focusMode)?.title}</p>
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
) : (
|
||||
<ScanEye />
|
||||
)}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
|
@ -75,10 +75,10 @@ const Focus = ({ focusMode, setFocusMode }: { focusMode: string; setFocusMode: (
|
|||
>
|
||||
<Popover.Panel className="absolute z-10 w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto">
|
||||
{focusModes.map((mode, i) => (
|
||||
{focusModes.map((mode, index) => (
|
||||
<Popover.Button
|
||||
onClick={() => 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
|
||||
|
|
|
@ -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 (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
{sources.slice(0, 3).map((source, i) => (
|
||||
{sources.slice(0, 3).map((source, index) => (
|
||||
<a
|
||||
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"
|
||||
key={i}
|
||||
key={index}
|
||||
href={source.metadata.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{source.metadata.title}
|
||||
|
@ -38,12 +38,12 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
className="rounded-lg h-4 w-4"
|
||||
/>
|
||||
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, "")}
|
||||
{source.metadata.url.replaceAll(/.+\/\/|www.|\..+/g, "")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
|
||||
<span>{i + 1}</span>
|
||||
<span>{index + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -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"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{sources.slice(3, 6).map((source, i) => (
|
||||
{sources.slice(3, 6).map((source, index) => (
|
||||
<img
|
||||
src={`https://s2.googleusercontent.com/s2/favicons?domain_url=${source.metadata.url}`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt="favicon"
|
||||
className="rounded-lg h-4 w-4"
|
||||
key={i}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -84,12 +84,13 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 dark:text-white">Sources</Dialog.Title>
|
||||
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
|
||||
{sources.map((source, i) => (
|
||||
{sources.map((source, index) => (
|
||||
<a
|
||||
className="bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 border border-light-200 dark:border-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
|
||||
key={i}
|
||||
key={index}
|
||||
href={source.metadata.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{source.metadata.title}
|
||||
|
@ -104,12 +105,12 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
className="rounded-lg h-4 w-4"
|
||||
/>
|
||||
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, "")}
|
||||
{source.metadata.url.replaceAll(/.+\/\/|www.|\..+/g, "")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
|
||||
<span>{i + 1}</span>
|
||||
<span>{index + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Image[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [slides, setSlides] = useState<any[]>([]);
|
||||
|
||||
return (
|
||||
|
@ -64,9 +64,9 @@ const SearchImages = ({ query, chat_history }: { query: string; chat_history: Me
|
|||
)}
|
||||
{loading && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
key={i}
|
||||
key={index}
|
||||
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
||||
/>
|
||||
))}
|
||||
|
@ -76,25 +76,25 @@ const SearchImages = ({ query, chat_history }: { query: string; chat_history: Me
|
|||
<>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{images.length > 4
|
||||
? images.slice(0, 3).map((image, i) => (
|
||||
? images.slice(0, 3).map((image, index) => (
|
||||
<img
|
||||
onClick={() => {
|
||||
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) => (
|
||||
<img
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{images.slice(3, 6).map((image, i) => (
|
||||
{images.slice(3, 6).map((image, index) => (
|
||||
<img
|
||||
key={i}
|
||||
key={index}
|
||||
src={image.img_src}
|
||||
alt={image.title}
|
||||
className="h-6 w-12 rounded-md lg:h-3 lg:w-6 lg:rounded-sm aspect-video object-cover"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import Lightbox, { GenericSlide, VideoSlide } from "yet-another-react-lightbox";
|
||||
|
@ -79,9 +78,9 @@ const Searchvideos = ({ query, chat_history }: { query: string; chat_history: Me
|
|||
)}
|
||||
{loading && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
key={i}
|
||||
key={index}
|
||||
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
||||
/>
|
||||
))}
|
||||
|
@ -91,14 +90,14 @@ const Searchvideos = ({ query, chat_history }: { query: string; chat_history: Me
|
|||
<>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{videos.length > 4
|
||||
? videos.slice(0, 3).map((video, i) => (
|
||||
? videos.slice(0, 3).map((video, index) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
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}
|
||||
>
|
||||
<img
|
||||
src={video.img_src}
|
||||
|
@ -111,14 +110,14 @@ const Searchvideos = ({ query, chat_history }: { query: string; chat_history: Me
|
|||
</div>
|
||||
</div>
|
||||
))
|
||||
: videos.map((video, i) => (
|
||||
: videos.map((video, index) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
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}
|
||||
>
|
||||
<img
|
||||
src={video.img_src}
|
||||
|
@ -137,9 +136,9 @@ const Searchvideos = ({ 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"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{videos.slice(3, 6).map((video, i) => (
|
||||
{videos.slice(3, 6).map((video, index) => (
|
||||
<img
|
||||
key={i}
|
||||
key={index}
|
||||
src={video.img_src}
|
||||
alt={video.title}
|
||||
className="h-6 w-12 rounded-md lg:h-3 lg:w-6 lg:rounded-sm aspect-video object-cover"
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
/* eslint-disable unicorn/no-nested-ternary */
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CloudUpload, RefreshCcw, RefreshCw } from "lucide-react";
|
||||
import React, { Fragment, useEffect, useState, type SelectHTMLAttributes } from "react";
|
||||
import ThemeSwitcher from "./theme/Switcher";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
interface InputProperties extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = ({ className, ...restProps }: InputProps) => {
|
||||
const Input = ({ className, ...restProperties }: InputProperties) => {
|
||||
return (
|
||||
<input
|
||||
{...restProps}
|
||||
{...restProperties}
|
||||
className={cn(
|
||||
"bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm",
|
||||
className,
|
||||
|
@ -18,14 +19,14 @@ const Input = ({ className, ...restProps }: InputProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
interface SelectProperties extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
options: { value: string; label: string; disabled?: boolean }[];
|
||||
}
|
||||
|
||||
export const Select = ({ className, options, ...restProps }: SelectProps) => {
|
||||
export const Select = ({ className, options, ...restProperties }: SelectProperties) => {
|
||||
return (
|
||||
<select
|
||||
{...restProps}
|
||||
{...restProperties}
|
||||
className={cn(
|
||||
"bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm",
|
||||
className,
|
||||
|
@ -129,8 +130,8 @@ const SettingsDialog = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (is
|
|||
localStorage.setItem("embeddingModel", selectedEmbeddingModel!);
|
||||
localStorage.setItem("openAIApiKey", customOpenAIApiKey!);
|
||||
localStorage.setItem("openAIBaseURL", customOpenAIBaseURL!);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
setIsOpen(false);
|
||||
|
|
|
@ -46,9 +46,9 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
<SquarePen className="cursor-pointer" />
|
||||
</a>
|
||||
<VerticalIconContainer>
|
||||
{navLinks.map((link, i) => (
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={i}
|
||||
key={index}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"relative flex flex-row items-center justify-center cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 duration-150 transition w-full py-2 rounded-lg",
|
||||
|
@ -70,10 +70,10 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
</div>
|
||||
|
||||
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-light-primary dark:bg-dark-primary px-4 py-4 shadow-sm lg:hidden">
|
||||
{navLinks.map((link, i) => (
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
href={link.href}
|
||||
key={i}
|
||||
key={index}
|
||||
className={cn(
|
||||
"relative flex flex-col items-center space-y-1 text-center w-full",
|
||||
link.active ? "text-black dark:text-white" : "text-black dark:text-white/70",
|
||||
|
|
|
@ -9,12 +9,13 @@ export const formatTimeDifference = (date1: Date | string, date2: Date | string)
|
|||
|
||||
const diffInSeconds = Math.floor(Math.abs(date2.getTime() - date1.getTime()) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return `${diffInSeconds} second${diffInSeconds !== 1 ? "s" : ""}`;
|
||||
if (diffInSeconds < 60) return `${diffInSeconds} second${diffInSeconds === 1 ? "" : "s"}`;
|
||||
else if (diffInSeconds < 3600)
|
||||
return `${Math.floor(diffInSeconds / 60)} minute${Math.floor(diffInSeconds / 60) !== 1 ? "s" : ""}`;
|
||||
else if (diffInSeconds < 86400)
|
||||
return `${Math.floor(diffInSeconds / 3600)} hour${Math.floor(diffInSeconds / 3600) !== 1 ? "s" : ""}`;
|
||||
else if (diffInSeconds < 31536000)
|
||||
return `${Math.floor(diffInSeconds / 86400)} day${Math.floor(diffInSeconds / 86400) !== 1 ? "s" : ""}`;
|
||||
else return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? "s" : ""}`;
|
||||
return `${Math.floor(diffInSeconds / 60)} minute${Math.floor(diffInSeconds / 60) === 1 ? "" : "s"}`;
|
||||
else if (diffInSeconds < 86_400)
|
||||
return `${Math.floor(diffInSeconds / 3600)} hour${Math.floor(diffInSeconds / 3600) === 1 ? "" : "s"}`;
|
||||
else if (diffInSeconds < 31_536_000)
|
||||
return `${Math.floor(diffInSeconds / 86_400)} day${Math.floor(diffInSeconds / 86_400) === 1 ? "" : "s"}`;
|
||||
else
|
||||
return `${Math.floor(diffInSeconds / 31_536_000)} year${Math.floor(diffInSeconds / 31_536_000) === 1 ? "" : "s"}`;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue