chore: Update dependencies and fix import paths

This commit is contained in:
Jin Yucong 2024-07-05 15:49:43 +08:00
parent 3b737a078a
commit 81c5e30fda
46 changed files with 1626 additions and 371 deletions

View file

@ -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": [
]
}

View file

@ -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",

View file

@ -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 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;

View file

@ -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);
}),

View file

@ -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 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;

View file

@ -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);
}),

View file

@ -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 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;

View file

@ -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 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;

View file

@ -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;

View file

@ -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 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;

View file

@ -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";

View file

@ -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;

View file

@ -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;

View file

@ -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];
}

View file

@ -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() {

View file

@ -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;

View file

@ -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());

View file

@ -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}`);
}
});

View file

@ -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: {

View file

@ -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}`);
}
});

View file

@ -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);
}
});

View file

@ -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}`);
}
});

View file

@ -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}`);
}
});

View file

@ -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);
}
};

View file

@ -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);

View file

@ -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}`);
}
};

View file

@ -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";

View file

@ -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"]

View file

@ -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
}
}
]
}

View file

@ -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}`}

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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}

View file

@ -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);

View file

@ -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={() => {

View file

@ -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"

View file

@ -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

View file

@ -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>

View file

@ -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);

View file

@ -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"

View file

@ -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"

View file

@ -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);

View file

@ -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",

View file

@ -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"}`;
};

1234
yarn.lock

File diff suppressed because it is too large Load diff