This commit is contained in:
Jin Yucong 2024-07-05 14:36:50 +08:00
parent 5b1aaee605
commit 3b737a078a
63 changed files with 1132 additions and 1853 deletions

View file

@ -2,10 +2,12 @@
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"prettier"
]
}

View file

@ -1,10 +1,10 @@
import { defineConfig } from 'drizzle-kit';
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: 'sqlite',
schema: './src/db/schema.ts',
out: './drizzle',
dialect: "sqlite",
schema: "./src/db/schema.ts",
out: "./drizzle",
dbCredentials: {
url: './data/db.sqlite',
url: "./data/db.sqlite",
},
});

View file

@ -19,6 +19,8 @@
"@typescript-eslint/parser": "^7.15.0",
"drizzle-kit": "^0.22.7",
"eslint": "^8",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",

View file

@ -1,24 +1,16 @@
import { BaseMessage } from '@langchain/core/messages';
import {
PromptTemplate,
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts';
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
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 computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
import { BaseMessage } from "@langchain/core/messages";
import { PromptTemplate, ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { Document } from "@langchain/core/documents";
import { searchSearxng } from "../lib/searxng";
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 computeSimilarity from "../utils/computeSimilarity";
import logger from "../utils/logger";
const basicAcademicSearchRetrieverPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
@ -65,34 +57,16 @@ const basicAcademicSearchResponsePrompt = `
const strParser = new StringOutputParser();
const handleStream = async (
stream: AsyncGenerator<StreamEvent, unknown, unknown>,
emitter: eventEmitter,
) => {
const handleStream = async (stream: AsyncGenerator<StreamEvent, unknown, unknown>, emitter: eventEmitter) => {
for await (const event of stream) {
if (
event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'sources', data: event.data.output }),
);
if (event.event === "on_chain_end" && event.name === "FinalSourceRetriever") {
emitter.emit("data", JSON.stringify({ type: "sources", data: event.data.output }));
}
if (
event.event === 'on_chain_stream' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk }),
);
if (event.event === "on_chain_stream" && event.name === "FinalResponseGenerator") {
emitter.emit("data", JSON.stringify({ type: "response", data: event.data.chunk }));
}
if (
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit('end');
if (event.event === "on_chain_end" && event.name === "FinalResponseGenerator") {
emitter.emit("end");
}
}
};
@ -108,22 +82,17 @@ const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => {
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
if (input === "not_needed") {
return { query: "", docs: [] };
}
const res = await searchSearxng(input, {
language: 'en',
engines: [
'arxiv',
'google scholar',
'internetarchivescholar',
'pubmed',
],
language: "en",
engines: ["arxiv", "google scholar", "internetarchivescholar", "pubmed"],
});
const documents = res.results.map(
(result) =>
result =>
new Document({
pageContent: result.content,
metadata: {
@ -139,36 +108,22 @@ const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => {
]);
};
const createBasicAcademicSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicAcademicSearchRetrieverChain =
createBasicAcademicSearchRetrieverChain(llm);
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');
return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n");
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
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(doc => doc.pageContent && doc.pageContent.length > 0);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)),
embeddings.embedQuery(query),
]);
@ -184,7 +139,7 @@ const createBasicAcademicSearchAnsweringChain = (
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
.map(sim => docsWithContent[sim.index]);
return sortedDocs;
};
@ -194,41 +149,35 @@ const createBasicAcademicSearchAnsweringChain = (
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
input => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicAcademicSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
runName: "FinalSourceRetriever",
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicAcademicSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
["system", basicAcademicSearchResponsePrompt],
new MessagesPlaceholder("chat_history"),
["user", "{query}"],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
runName: "FinalResponseGenerator",
});
};
const basicAcademicSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicAcademicSearch = (query: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = new eventEmitter();
try {
const basicAcademicSearchAnsweringChain =
createBasicAcademicSearchAnsweringChain(llm, embeddings);
const basicAcademicSearchAnsweringChain = createBasicAcademicSearchAnsweringChain(llm, embeddings);
const stream = basicAcademicSearchAnsweringChain.streamEvents(
{
@ -236,28 +185,20 @@ const basicAcademicSearch = (
query: query,
},
{
version: 'v1',
version: "v1",
},
);
handleStream(stream, emitter);
} catch (err) {
emitter.emit(
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" }));
logger.error(`Error in academic search: ${err}`);
}
return emitter;
};
const handleAcademicSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const handleAcademicSearch = (message: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = basicAcademicSearch(message, history, llm, embeddings);
return emitter;
};

View file

@ -1,14 +1,10 @@
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../lib/searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { PromptTemplate } from "@langchain/core/prompts";
import formatChatHistoryAsString from "../utils/formatHistory";
import { BaseMessage } from "@langchain/core/messages";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { searchSearxng } from "../lib/searxng";
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
const imageSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images.
@ -53,12 +49,12 @@ const createImageSearchChain = (llm: BaseChatModel) => {
strParser,
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
engines: ['bing images', 'google images'],
engines: ["bing images", "google images"],
});
const images = [];
res.results.forEach((result) => {
res.results.forEach(result => {
if (result.img_src && result.url && result.title) {
images.push({
img_src: result.img_src,
@ -73,10 +69,7 @@ const createImageSearchChain = (llm: BaseChatModel) => {
]);
};
const handleImageSearch = (
input: ImageSearchChainInput,
llm: BaseChatModel,
) => {
const handleImageSearch = (input: ImageSearchChainInput, llm: BaseChatModel) => {
const imageSearchChain = createImageSearchChain(llm);
return imageSearchChain.invoke(input);
};

View file

@ -1,24 +1,16 @@
import { BaseMessage } from '@langchain/core/messages';
import {
PromptTemplate,
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts';
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
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 computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
import { BaseMessage } from "@langchain/core/messages";
import { PromptTemplate, ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { Document } from "@langchain/core/documents";
import { searchSearxng } from "../lib/searxng";
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 computeSimilarity from "../utils/computeSimilarity";
import logger from "../utils/logger";
const basicRedditSearchRetrieverPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
@ -65,34 +57,16 @@ const basicRedditSearchResponsePrompt = `
const strParser = new StringOutputParser();
const handleStream = async (
stream: AsyncGenerator<StreamEvent, unknown, unknown>,
emitter: eventEmitter,
) => {
const handleStream = async (stream: AsyncGenerator<StreamEvent, unknown, unknown>, emitter: eventEmitter) => {
for await (const event of stream) {
if (
event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'sources', data: event.data.output }),
);
if (event.event === "on_chain_end" && event.name === "FinalSourceRetriever") {
emitter.emit("data", JSON.stringify({ type: "sources", data: event.data.output }));
}
if (
event.event === 'on_chain_stream' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk }),
);
if (event.event === "on_chain_stream" && event.name === "FinalResponseGenerator") {
emitter.emit("data", JSON.stringify({ type: "response", data: event.data.chunk }));
}
if (
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit('end');
if (event.event === "on_chain_end" && event.name === "FinalResponseGenerator") {
emitter.emit("end");
}
}
};
@ -108,17 +82,17 @@ const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => {
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
if (input === "not_needed") {
return { query: "", docs: [] };
}
const res = await searchSearxng(input, {
language: 'en',
engines: ['reddit'],
language: "en",
engines: ["reddit"],
});
const documents = res.results.map(
(result) =>
result =>
new Document({
pageContent: result.content ? result.content : result.title,
metadata: {
@ -134,36 +108,22 @@ const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => {
]);
};
const createBasicRedditSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicRedditSearchRetrieverChain =
createBasicRedditSearchRetrieverChain(llm);
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');
return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n");
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
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(doc => doc.pageContent && doc.pageContent.length > 0);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)),
embeddings.embedQuery(query),
]);
@ -179,8 +139,8 @@ const createBasicRedditSearchAnsweringChain = (
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.filter((sim) => sim.similarity > 0.3)
.map((sim) => docsWithContent[sim.index]);
.filter(sim => sim.similarity > 0.3)
.map(sim => docsWithContent[sim.index]);
return sortedDocs;
};
@ -190,69 +150,55 @@ const createBasicRedditSearchAnsweringChain = (
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
input => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicRedditSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
runName: "FinalSourceRetriever",
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicRedditSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
["system", basicRedditSearchResponsePrompt],
new MessagesPlaceholder("chat_history"),
["user", "{query}"],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
runName: "FinalResponseGenerator",
});
};
const basicRedditSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicRedditSearch = (query: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = new eventEmitter();
try {
const basicRedditSearchAnsweringChain =
createBasicRedditSearchAnsweringChain(llm, embeddings);
const basicRedditSearchAnsweringChain = createBasicRedditSearchAnsweringChain(llm, embeddings);
const stream = basicRedditSearchAnsweringChain.streamEvents(
{
chat_history: history,
query: query,
},
{
version: 'v1',
version: "v1",
},
);
handleStream(stream, emitter);
} catch (err) {
emitter.emit(
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" }));
logger.error(`Error in RedditSearch: ${err}`);
}
return emitter;
};
const handleRedditSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const handleRedditSearch = (message: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = basicRedditSearch(message, history, llm, embeddings);
return emitter;
};

View file

@ -1,10 +1,10 @@
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
import ListLineOutputParser from '../lib/outputParsers/listLineOutputParser';
import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ChatOpenAI } from '@langchain/openai';
import { RunnableSequence, RunnableMap } from "@langchain/core/runnables";
import ListLineOutputParser from "../lib/outputParsers/listLineOutputParser";
import { PromptTemplate } from "@langchain/core/prompts";
import formatChatHistoryAsString from "../utils/formatHistory";
import { BaseMessage } from "@langchain/core/messages";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { ChatOpenAI } from "@langchain/openai";
const suggestionGeneratorPrompt = `
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
@ -28,14 +28,13 @@ type SuggestionGeneratorInput = {
};
const outputParser = new ListLineOutputParser({
key: 'suggestions',
key: "suggestions",
});
const createSuggestionGeneratorChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: SuggestionGeneratorInput) =>
formatChatHistoryAsString(input.chat_history),
chat_history: (input: SuggestionGeneratorInput) => formatChatHistoryAsString(input.chat_history),
}),
PromptTemplate.fromTemplate(suggestionGeneratorPrompt),
llm,
@ -43,10 +42,7 @@ const createSuggestionGeneratorChain = (llm: BaseChatModel) => {
]);
};
const generateSuggestions = (
input: SuggestionGeneratorInput,
llm: BaseChatModel,
) => {
const generateSuggestions = (input: SuggestionGeneratorInput, llm: BaseChatModel) => {
(llm as ChatOpenAI).temperature = 0;
const suggestionGeneratorChain = createSuggestionGeneratorChain(llm);
return suggestionGeneratorChain.invoke(input);

View file

@ -1,14 +1,10 @@
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../lib/searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { PromptTemplate } from "@langchain/core/prompts";
import formatChatHistoryAsString from "../utils/formatHistory";
import { BaseMessage } from "@langchain/core/messages";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { searchSearxng } from "../lib/searxng";
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
const VideoSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
@ -53,18 +49,13 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
strParser,
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, {
engines: ['youtube'],
engines: ["youtube"],
});
const videos = [];
res.results.forEach((result) => {
if (
result.thumbnail &&
result.url &&
result.title &&
result.iframe_src
) {
res.results.forEach(result => {
if (result.thumbnail && result.url && result.title && result.iframe_src) {
videos.push({
img_src: result.thumbnail,
url: result.url,
@ -79,10 +70,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
]);
};
const handleVideoSearch = (
input: VideoSearchChainInput,
llm: BaseChatModel,
) => {
const handleVideoSearch = (input: VideoSearchChainInput, llm: BaseChatModel) => {
const VideoSearchChain = createVideoSearchChain(llm);
return VideoSearchChain.invoke(input);
};

View file

@ -1,24 +1,16 @@
import { BaseMessage } from '@langchain/core/messages';
import {
PromptTemplate,
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts';
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
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 computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
import { BaseMessage } from "@langchain/core/messages";
import { PromptTemplate, ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { Document } from "@langchain/core/documents";
import { searchSearxng } from "../lib/searxng";
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 computeSimilarity from "../utils/computeSimilarity";
import logger from "../utils/logger";
const basicSearchRetrieverPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
@ -65,34 +57,16 @@ const basicWebSearchResponsePrompt = `
const strParser = new StringOutputParser();
const handleStream = async (
stream: AsyncGenerator<StreamEvent, unknown, unknown>,
emitter: eventEmitter,
) => {
const handleStream = async (stream: AsyncGenerator<StreamEvent, unknown, unknown>, emitter: eventEmitter) => {
for await (const event of stream) {
if (
event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'sources', data: event.data.output }),
);
if (event.event === "on_chain_end" && event.name === "FinalSourceRetriever") {
emitter.emit("data", JSON.stringify({ type: "sources", data: event.data.output }));
}
if (
event.event === 'on_chain_stream' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk }),
);
if (event.event === "on_chain_stream" && event.name === "FinalResponseGenerator") {
emitter.emit("data", JSON.stringify({ type: "response", data: event.data.chunk }));
}
if (
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit('end');
if (event.event === "on_chain_end" && event.name === "FinalResponseGenerator") {
emitter.emit("end");
}
}
};
@ -108,16 +82,16 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
if (input === "not_needed") {
return { query: "", docs: [] };
}
const res = await searchSearxng(input, {
language: 'en',
language: "en",
});
const documents = res.results.map(
(result) =>
result =>
new Document({
pageContent: result.content,
metadata: {
@ -133,35 +107,22 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
]);
};
const createBasicWebSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
) => {
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');
return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n");
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
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(doc => doc.pageContent && doc.pageContent.length > 0);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)),
embeddings.embedQuery(query),
]);
@ -176,9 +137,9 @@ const createBasicWebSearchAnsweringChain = (
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.filter((sim) => sim.similarity > 0.5)
.filter(sim => sim.similarity > 0.5)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
.map(sim => docsWithContent[sim.index]);
return sortedDocs;
};
@ -188,43 +149,35 @@ const createBasicWebSearchAnsweringChain = (
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
input => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicWebSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
runName: "FinalSourceRetriever",
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicWebSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
["system", basicWebSearchResponsePrompt],
new MessagesPlaceholder("chat_history"),
["user", "{query}"],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
runName: "FinalResponseGenerator",
});
};
const basicWebSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicWebSearch = (query: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = new eventEmitter();
try {
const basicWebSearchAnsweringChain = createBasicWebSearchAnsweringChain(
llm,
embeddings,
);
const basicWebSearchAnsweringChain = createBasicWebSearchAnsweringChain(llm, embeddings);
const stream = basicWebSearchAnsweringChain.streamEvents(
{
@ -232,28 +185,20 @@ const basicWebSearch = (
query: query,
},
{
version: 'v1',
version: "v1",
},
);
handleStream(stream, emitter);
} catch (err) {
emitter.emit(
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" }));
logger.error(`Error in websearch: ${err}`);
}
return emitter;
};
const handleWebSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const handleWebSearch = (message: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = basicWebSearch(message, history, llm, embeddings);
return emitter;
};

View file

@ -1,23 +1,15 @@
import { BaseMessage } from '@langchain/core/messages';
import {
PromptTemplate,
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts';
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
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 logger from '../utils/logger';
import { BaseMessage } from "@langchain/core/messages";
import { PromptTemplate, ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { Document } from "@langchain/core/documents";
import { searchSearxng } from "../lib/searxng";
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 logger from "../utils/logger";
const basicWolframAlphaSearchRetrieverPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
@ -64,34 +56,16 @@ const basicWolframAlphaSearchResponsePrompt = `
const strParser = new StringOutputParser();
const handleStream = async (
stream: AsyncGenerator<StreamEvent, unknown, unknown>,
emitter: eventEmitter,
) => {
const handleStream = async (stream: AsyncGenerator<StreamEvent, unknown, unknown>, emitter: eventEmitter) => {
for await (const event of stream) {
if (
event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'sources', data: event.data.output }),
);
if (event.event === "on_chain_end" && event.name === "FinalSourceRetriever") {
emitter.emit("data", JSON.stringify({ type: "sources", data: event.data.output }));
}
if (
event.event === 'on_chain_stream' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk }),
);
if (event.event === "on_chain_stream" && event.name === "FinalResponseGenerator") {
emitter.emit("data", JSON.stringify({ type: "response", data: event.data.chunk }));
}
if (
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit('end');
if (event.event === "on_chain_end" && event.name === "FinalResponseGenerator") {
emitter.emit("end");
}
}
};
@ -107,17 +81,17 @@ const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => {
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
if (input === "not_needed") {
return { query: "", docs: [] };
}
const res = await searchSearxng(input, {
language: 'en',
engines: ['wolframalpha'],
language: "en",
engines: ["wolframalpha"],
});
const documents = res.results.map(
(result) =>
result =>
new Document({
pageContent: result.content,
metadata: {
@ -134,13 +108,10 @@ const createBasicWolframAlphaSearchRetrieverChain = (llm: BaseChatModel) => {
};
const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
const basicWolframAlphaSearchRetrieverChain =
createBasicWolframAlphaSearchRetrieverChain(llm);
const basicWolframAlphaSearchRetrieverChain = createBasicWolframAlphaSearchRetrieverChain(llm);
const processDocs = (docs: Document[]) => {
return docs
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
.join('\n');
return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n");
};
return RunnableSequence.from([
@ -148,7 +119,7 @@ const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
input => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
@ -157,49 +128,41 @@ const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
return docs;
})
.withConfig({
runName: 'FinalSourceRetriever',
runName: "FinalSourceRetriever",
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicWolframAlphaSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
["system", basicWolframAlphaSearchResponsePrompt],
new MessagesPlaceholder("chat_history"),
["user", "{query}"],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
runName: "FinalResponseGenerator",
});
};
const basicWolframAlphaSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
) => {
const basicWolframAlphaSearch = (query: string, history: BaseMessage[], llm: BaseChatModel) => {
const emitter = new eventEmitter();
try {
const basicWolframAlphaSearchAnsweringChain =
createBasicWolframAlphaSearchAnsweringChain(llm);
const basicWolframAlphaSearchAnsweringChain = createBasicWolframAlphaSearchAnsweringChain(llm);
const stream = basicWolframAlphaSearchAnsweringChain.streamEvents(
{
chat_history: history,
query: query,
},
{
version: 'v1',
version: "v1",
},
);
handleStream(stream, emitter);
} catch (err) {
emitter.emit(
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" }));
logger.error(`Error in WolframAlphaSearch: ${err}`);
}

View file

@ -1,15 +1,12 @@
import { BaseMessage } from '@langchain/core/messages';
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 type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type { Embeddings } from '@langchain/core/embeddings';
import logger from '../utils/logger';
import { BaseMessage } from "@langchain/core/messages";
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 type { BaseChatModel } from "@langchain/core/language_models/chat_models";
import type { Embeddings } from "@langchain/core/embeddings";
import logger from "../utils/logger";
const writingAssistantPrompt = `
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
@ -18,25 +15,13 @@ Since you are a writing assistant, you would not perform web searches. If you th
const strParser = new StringOutputParser();
const handleStream = async (
stream: AsyncGenerator<StreamEvent, unknown, unknown>,
emitter: eventEmitter,
) => {
const handleStream = async (stream: AsyncGenerator<StreamEvent, unknown, unknown>, emitter: eventEmitter) => {
for await (const event of stream) {
if (
event.event === 'on_chain_stream' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk }),
);
if (event.event === "on_chain_stream" && event.name === "FinalResponseGenerator") {
emitter.emit("data", JSON.stringify({ type: "response", data: event.data.chunk }));
}
if (
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit('end');
if (event.event === "on_chain_end" && event.name === "FinalResponseGenerator") {
emitter.emit("end");
}
}
};
@ -44,14 +29,14 @@ const handleStream = async (
const createWritingAssistantChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
ChatPromptTemplate.fromMessages([
['system', writingAssistantPrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
["system", writingAssistantPrompt],
new MessagesPlaceholder("chat_history"),
["user", "{query}"],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
runName: "FinalResponseGenerator",
});
};
@ -72,16 +57,13 @@ const handleWritingAssistant = (
query: query,
},
{
version: 'v1',
version: "v1",
},
);
handleStream(stream, emitter);
} catch (err) {
emitter.emit(
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" }));
logger.error(`Error in writing assistant: ${err}`);
}

View file

@ -1,24 +1,16 @@
import { BaseMessage } from '@langchain/core/messages';
import {
PromptTemplate,
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts';
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { Document } from '@langchain/core/documents';
import { searchSearxng } from '../lib/searxng';
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 computeSimilarity from '../utils/computeSimilarity';
import logger from '../utils/logger';
import { BaseMessage } from "@langchain/core/messages";
import { PromptTemplate, ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableSequence, RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { Document } from "@langchain/core/documents";
import { searchSearxng } from "../lib/searxng";
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 computeSimilarity from "../utils/computeSimilarity";
import logger from "../utils/logger";
const basicYoutubeSearchRetrieverPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the LLM to search the web for information.
@ -65,34 +57,16 @@ const basicYoutubeSearchResponsePrompt = `
const strParser = new StringOutputParser();
const handleStream = async (
stream: AsyncGenerator<StreamEvent, unknown, unknown>,
emitter: eventEmitter,
) => {
const handleStream = async (stream: AsyncGenerator<StreamEvent, unknown, unknown>, emitter: eventEmitter) => {
for await (const event of stream) {
if (
event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'sources', data: event.data.output }),
);
if (event.event === "on_chain_end" && event.name === "FinalSourceRetriever") {
emitter.emit("data", JSON.stringify({ type: "sources", data: event.data.output }));
}
if (
event.event === 'on_chain_stream' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk }),
);
if (event.event === "on_chain_stream" && event.name === "FinalResponseGenerator") {
emitter.emit("data", JSON.stringify({ type: "response", data: event.data.chunk }));
}
if (
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
emitter.emit('end');
if (event.event === "on_chain_end" && event.name === "FinalResponseGenerator") {
emitter.emit("end");
}
}
};
@ -108,17 +82,17 @@ const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => {
llm,
strParser,
RunnableLambda.from(async (input: string) => {
if (input === 'not_needed') {
return { query: '', docs: [] };
if (input === "not_needed") {
return { query: "", docs: [] };
}
const res = await searchSearxng(input, {
language: 'en',
engines: ['youtube'],
language: "en",
engines: ["youtube"],
});
const documents = res.results.map(
(result) =>
result =>
new Document({
pageContent: result.content ? result.content : result.title,
metadata: {
@ -134,36 +108,22 @@ const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => {
]);
};
const createBasicYoutubeSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicYoutubeSearchRetrieverChain =
createBasicYoutubeSearchRetrieverChain(llm);
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');
return docs.map((_, index) => `${index + 1}. ${docs[index].pageContent}`).join("\n");
};
const rerankDocs = async ({
query,
docs,
}: {
query: string;
docs: Document[];
}) => {
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(doc => doc.pageContent && doc.pageContent.length > 0);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedDocuments(docsWithContent.map(doc => doc.pageContent)),
embeddings.embedQuery(query),
]);
@ -179,8 +139,8 @@ const createBasicYoutubeSearchAnsweringChain = (
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.filter((sim) => sim.similarity > 0.3)
.map((sim) => docsWithContent[sim.index]);
.filter(sim => sim.similarity > 0.3)
.map(sim => docsWithContent[sim.index]);
return sortedDocs;
};
@ -190,41 +150,35 @@ const createBasicYoutubeSearchAnsweringChain = (
query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history,
context: RunnableSequence.from([
(input) => ({
input => ({
query: input.query,
chat_history: formatChatHistoryAsString(input.chat_history),
}),
basicYoutubeSearchRetrieverChain
.pipe(rerankDocs)
.withConfig({
runName: 'FinalSourceRetriever',
runName: "FinalSourceRetriever",
})
.pipe(processDocs),
]),
}),
ChatPromptTemplate.fromMessages([
['system', basicYoutubeSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
["system", basicYoutubeSearchResponsePrompt],
new MessagesPlaceholder("chat_history"),
["user", "{query}"],
]),
llm,
strParser,
]).withConfig({
runName: 'FinalResponseGenerator',
runName: "FinalResponseGenerator",
});
};
const basicYoutubeSearch = (
query: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const basicYoutubeSearch = (query: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = new eventEmitter();
try {
const basicYoutubeSearchAnsweringChain =
createBasicYoutubeSearchAnsweringChain(llm, embeddings);
const basicYoutubeSearchAnsweringChain = createBasicYoutubeSearchAnsweringChain(llm, embeddings);
const stream = basicYoutubeSearchAnsweringChain.streamEvents(
{
@ -232,28 +186,20 @@ const basicYoutubeSearch = (
query: query,
},
{
version: 'v1',
version: "v1",
},
);
handleStream(stream, emitter);
} catch (err) {
emitter.emit(
'error',
JSON.stringify({ data: 'An error has occurred please try again later' }),
);
emitter.emit("error", JSON.stringify({ data: "An error has occurred please try again later" }));
logger.error(`Error in youtube search: ${err}`);
}
return emitter;
};
const handleYoutubeSearch = (
message: string,
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
) => {
const handleYoutubeSearch = (message: string, history: BaseMessage[], llm: BaseChatModel, embeddings: Embeddings) => {
const emitter = basicYoutubeSearch(message, history, llm, embeddings);
return emitter;
};

View file

@ -1,10 +1,10 @@
import { startWebSocketServer } from './websocket';
import express from 'express';
import cors from 'cors';
import http from 'http';
import routes from './routes';
import { getPort } from './config';
import logger from './utils/logger';
import { startWebSocketServer } from "./websocket";
import express from "express";
import cors from "cors";
import http from "http";
import routes from "./routes";
import { getPort } from "./config";
import logger from "./utils/logger";
const port = getPort();
@ -12,15 +12,15 @@ const app = express();
const server = http.createServer(app);
const corsOptions = {
origin: '*',
origin: "*",
};
app.use(cors(corsOptions));
app.use(express.json());
app.use('/api', routes);
app.get('/api', (_, res) => {
res.status(200).json({ status: 'ok' });
app.use("/api", routes);
app.get("/api", (_, res) => {
res.status(200).json({ status: "ok" });
});
server.listen(port, () => {

View file

@ -1,8 +1,8 @@
import fs from 'fs';
import path from 'path';
import toml from '@iarna/toml';
import fs from "fs";
import path from "path";
import toml from "@iarna/toml";
const configFileName = 'config.toml';
const configFileName = "config.toml";
interface Config {
GENERAL: {
@ -24,14 +24,11 @@ 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}`), "utf-8")) as unknown as Config;
export const getPort = () => loadConfig().GENERAL.PORT;
export const getSimilarityMeasure = () =>
loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getSimilarityMeasure = () => loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
@ -47,23 +44,16 @@ export const updateConfig = (config: RecursivePartial<Config>) => {
for (const key in currentConfig) {
if (!config[key]) config[key] = {};
if (typeof currentConfig[key] === 'object' && currentConfig[key] !== null) {
if (typeof currentConfig[key] === "object" && currentConfig[key] !== null) {
for (const nestedKey in currentConfig[key]) {
if (
!config[key][nestedKey] &&
currentConfig[key][nestedKey] &&
config[key][nestedKey] !== ''
) {
if (!config[key][nestedKey] && currentConfig[key][nestedKey] && config[key][nestedKey] !== "") {
config[key][nestedKey] = currentConfig[key][nestedKey];
}
}
} else if (currentConfig[key] && config[key] !== '') {
} else if (currentConfig[key] && config[key] !== "") {
config[key] = currentConfig[key];
}
}
fs.writeFileSync(
path.join(__dirname, `../${configFileName}`),
toml.stringify(config),
);
fs.writeFileSync(path.join(__dirname, `../${configFileName}`), toml.stringify(config));
};

View file

@ -1,8 +1,8 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import * as schema from "./schema";
const sqlite = new Database('data/db.sqlite');
const sqlite = new Database("data/db.sqlite");
const db = drizzle(sqlite, {
schema: schema,
});

View file

@ -1,19 +1,19 @@
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
import { text, integer, sqliteTable } from "drizzle-orm/sqlite-core";
export const messages = sqliteTable('messages', {
id: integer('id').primaryKey(),
content: text('content').notNull(),
chatId: text('chatId').notNull(),
messageId: text('messageId').notNull(),
role: text('type', { enum: ['assistant', 'user'] }),
metadata: text('metadata', {
mode: 'json',
export const messages = sqliteTable("messages", {
id: integer("id").primaryKey(),
content: text("content").notNull(),
chatId: text("chatId").notNull(),
messageId: text("messageId").notNull(),
role: text("type", { enum: ["assistant", "user"] }),
metadata: text("metadata", {
mode: "json",
}),
});
export const chats = sqliteTable('chats', {
id: text('id').primaryKey(),
title: text('title').notNull(),
createdAt: text('createdAt').notNull(),
focusMode: text('focusMode').notNull(),
export const chats = sqliteTable("chats", {
id: text("id").primaryKey(),
title: text("title").notNull(),
createdAt: text("createdAt").notNull(),
focusMode: text("focusMode").notNull(),
});

View file

@ -1,8 +1,7 @@
import { Embeddings, type EmbeddingsParams } from '@langchain/core/embeddings';
import { chunkArray } from '@langchain/core/utils/chunk_array';
import { Embeddings, type EmbeddingsParams } from "@langchain/core/embeddings";
import { chunkArray } from "@langchain/core/utils/chunk_array";
export interface HuggingFaceTransformersEmbeddingsParams
extends EmbeddingsParams {
export interface HuggingFaceTransformersEmbeddingsParams extends EmbeddingsParams {
modelName: string;
model: string;
@ -14,13 +13,10 @@ export interface HuggingFaceTransformersEmbeddingsParams
stripNewLines?: boolean;
}
export class HuggingFaceTransformersEmbeddings
extends Embeddings
implements HuggingFaceTransformersEmbeddingsParams
{
modelName = 'Xenova/all-MiniLM-L6-v2';
export class HuggingFaceTransformersEmbeddings extends Embeddings implements HuggingFaceTransformersEmbeddingsParams {
modelName = "Xenova/all-MiniLM-L6-v2";
model = 'Xenova/all-MiniLM-L6-v2';
model = "Xenova/all-MiniLM-L6-v2";
batchSize = 512;
@ -41,12 +37,9 @@ export class HuggingFaceTransformersEmbeddings
}
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.replace(/\n/g, " ")) : texts, this.batchSize);
const batchRequests = batches.map((batch) => this.runEmbedding(batch));
const batchRequests = batches.map(batch => this.runEmbedding(batch));
const batchResponses = await Promise.all(batchRequests);
const embeddings: number[][] = [];
@ -61,22 +54,17 @@ export class HuggingFaceTransformersEmbeddings
}
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.replace(/\n/g, " ") : text]);
return data[0];
}
private async runEmbedding(texts: string[]) {
const { pipeline } = await import('@xenova/transformers');
const { pipeline } = await import("@xenova/transformers");
const pipe = await (this.pipelinePromise ??= pipeline(
'feature-extraction',
this.model,
));
const pipe = await (this.pipelinePromise ??= pipeline("feature-extraction", this.model));
return this.caller.call(async () => {
const output = await pipe(texts, { pooling: 'mean', normalize: true });
const output = await pipe(texts, { pooling: "mean", normalize: true });
return output.tolist();
});
}

View file

@ -1,11 +1,11 @@
import { BaseOutputParser } from '@langchain/core/output_parsers';
import { BaseOutputParser } from "@langchain/core/output_parsers";
interface LineListOutputParserArgs {
key?: string;
}
class LineListOutputParser extends BaseOutputParser<string[]> {
private key = 'questions';
private key = "questions";
constructor(args?: LineListOutputParserArgs) {
super();
@ -13,30 +13,29 @@ class LineListOutputParser extends BaseOutputParser<string[]> {
}
static lc_name() {
return 'LineListOutputParser';
return "LineListOutputParser";
}
lc_namespace = ['langchain', 'output_parsers', 'line_list_output_parser'];
lc_namespace = ["langchain", "output_parsers", "line_list_output_parser"];
async parse(text: string): Promise<string[]> {
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
const startKeyIndex = text.indexOf(`<${this.key}>`);
const endKeyIndex = text.indexOf(`</${this.key}>`);
const questionsStartIndex =
startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length;
const questionsStartIndex = startKeyIndex === -1 ? 0 : startKeyIndex + `<${this.key}>`.length;
const questionsEndIndex = endKeyIndex === -1 ? text.length : endKeyIndex;
const lines = text
.slice(questionsStartIndex, questionsEndIndex)
.trim()
.split('\n')
.filter((line) => line.trim() !== '')
.map((line) => line.replace(regex, ''));
.split("\n")
.filter(line => line.trim() !== "")
.map(line => line.replace(regex, ""));
return lines;
}
getFormatInstructions(): string {
throw new Error('Not implemented.');
throw new Error("Not implemented.");
}
}

View file

@ -1,13 +1,9 @@
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { HuggingFaceTransformersEmbeddings } from './huggingfaceTransformer';
import {
getGroqApiKey,
getOllamaApiEndpoint,
getOpenaiApiKey,
} from '../config';
import logger from '../utils/logger';
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { ChatOllama } from "@langchain/community/chat_models/ollama";
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama";
import { HuggingFaceTransformersEmbeddings } from "./huggingfaceTransformer";
import { getGroqApiKey, getOllamaApiEndpoint, getOpenaiApiKey } from "../config";
import logger from "../utils/logger";
export const getAvailableChatModelProviders = async () => {
const openAIApiKey = getOpenaiApiKey();
@ -18,25 +14,25 @@ export const getAvailableChatModelProviders = async () => {
if (openAIApiKey) {
try {
models['openai'] = {
'GPT-3.5 turbo': new ChatOpenAI({
models["openai"] = {
"GPT-3.5 turbo": new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-3.5-turbo',
modelName: "gpt-3.5-turbo",
temperature: 0.7,
}),
'GPT-4': new ChatOpenAI({
"GPT-4": new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-4',
modelName: "gpt-4",
temperature: 0.7,
}),
'GPT-4 turbo': new ChatOpenAI({
"GPT-4 turbo": new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-4-turbo',
modelName: "gpt-4-turbo",
temperature: 0.7,
}),
'GPT-4 omni': new ChatOpenAI({
"GPT-4 omni": new ChatOpenAI({
openAIApiKey,
modelName: 'gpt-4o',
modelName: "gpt-4o",
temperature: 0.7,
}),
};
@ -47,45 +43,45 @@ export const getAvailableChatModelProviders = async () => {
if (groqApiKey) {
try {
models['groq'] = {
'LLaMA3 8b': new ChatOpenAI(
models["groq"] = {
"LLaMA3 8b": new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama3-8b-8192',
modelName: "llama3-8b-8192",
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
baseURL: "https://api.groq.com/openai/v1",
},
),
'LLaMA3 70b': new ChatOpenAI(
"LLaMA3 70b": new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama3-70b-8192',
modelName: "llama3-70b-8192",
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
baseURL: "https://api.groq.com/openai/v1",
},
),
'Mixtral 8x7b': new ChatOpenAI(
"Mixtral 8x7b": new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'mixtral-8x7b-32768',
modelName: "mixtral-8x7b-32768",
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
baseURL: "https://api.groq.com/openai/v1",
},
),
'Gemma 7b': new ChatOpenAI(
"Gemma 7b": new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'gemma-7b-it',
modelName: "gemma-7b-it",
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
baseURL: "https://api.groq.com/openai/v1",
},
),
};
@ -98,14 +94,14 @@ export const getAvailableChatModelProviders = async () => {
try {
const response = await fetch(`${ollamaEndpoint}/api/tags`, {
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { models: ollamaModels } = (await response.json()) as any;
models['ollama'] = ollamaModels.reduce((acc, model) => {
models["ollama"] = ollamaModels.reduce((acc, model) => {
acc[model.model] = new ChatOllama({
baseUrl: ollamaEndpoint,
model: model.model,
@ -118,7 +114,7 @@ export const getAvailableChatModelProviders = async () => {
}
}
models['custom_openai'] = {};
models["custom_openai"] = {};
return models;
};
@ -131,14 +127,14 @@ export const getAvailableEmbeddingModelProviders = async () => {
if (openAIApiKey) {
try {
models['openai'] = {
'Text embedding 3 small': new OpenAIEmbeddings({
models["openai"] = {
"Text embedding 3 small": new OpenAIEmbeddings({
openAIApiKey,
modelName: 'text-embedding-3-small',
modelName: "text-embedding-3-small",
}),
'Text embedding 3 large': new OpenAIEmbeddings({
"Text embedding 3 large": new OpenAIEmbeddings({
openAIApiKey,
modelName: 'text-embedding-3-large',
modelName: "text-embedding-3-large",
}),
};
} catch (err) {
@ -150,14 +146,14 @@ export const getAvailableEmbeddingModelProviders = async () => {
try {
const response = await fetch(`${ollamaEndpoint}/api/tags`, {
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { models: ollamaModels } = (await response.json()) as any;
models['ollama'] = ollamaModels.reduce((acc, model) => {
models["ollama"] = ollamaModels.reduce((acc, model) => {
acc[model.model] = new OllamaEmbeddings({
baseUrl: ollamaEndpoint,
model: model.model,
@ -170,15 +166,15 @@ export const getAvailableEmbeddingModelProviders = async () => {
}
try {
models['local'] = {
'BGE Small': new HuggingFaceTransformersEmbeddings({
modelName: 'Xenova/bge-small-en-v1.5',
models["local"] = {
"BGE Small": new HuggingFaceTransformersEmbeddings({
modelName: "Xenova/bge-small-en-v1.5",
}),
'GTE Small': new HuggingFaceTransformersEmbeddings({
modelName: 'Xenova/gte-small',
"GTE Small": new HuggingFaceTransformersEmbeddings({
modelName: "Xenova/gte-small",
}),
'Bert Multilingual': new HuggingFaceTransformersEmbeddings({
modelName: 'Xenova/bert-base-multilingual-uncased',
"Bert Multilingual": new HuggingFaceTransformersEmbeddings({
modelName: "Xenova/bert-base-multilingual-uncased",
}),
};
} catch (err) {

View file

@ -1,5 +1,5 @@
import axios from 'axios';
import { getSearxngApiEndpoint } from '../config';
import axios from "axios";
import { getSearxngApiEndpoint } from "../config";
interface SearxngSearchOptions {
categories?: string[];
@ -19,19 +19,16 @@ interface SearxngSearchResult {
iframe_src?: string;
}
export const searchSearxng = async (
query: string,
opts?: SearxngSearchOptions,
) => {
export const searchSearxng = async (query: string, opts?: SearxngSearchOptions) => {
const searxngURL = getSearxngApiEndpoint();
const url = new URL(`${searxngURL}/search?format=json`);
url.searchParams.append('q', query);
url.searchParams.append("q", query);
if (opts) {
Object.keys(opts).forEach((key) => {
Object.keys(opts).forEach(key => {
if (Array.isArray(opts[key])) {
url.searchParams.append(key, opts[key].join(','));
url.searchParams.append(key, opts[key].join(","));
return;
}
url.searchParams.append(key, opts[key]);

View file

@ -1,12 +1,12 @@
import express from 'express';
import logger from '../utils/logger';
import db from '../db/index';
import { eq } from 'drizzle-orm';
import { chats, messages } from '../db/schema';
import express from "express";
import logger from "../utils/logger";
import db from "../db/index";
import { eq } from "drizzle-orm";
import { chats, messages } from "../db/schema";
const router = express.Router();
router.get('/', async (_, res) => {
router.get("/", async (_, res) => {
try {
let chats = await db.query.chats.findMany();
@ -14,19 +14,19 @@ router.get('/', async (_, res) => {
return res.status(200).json({ chats: chats });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(`Error in getting chats: ${err.message}`);
}
});
router.get('/:id', async (req, res) => {
router.get("/:id", async (req, res) => {
try {
const chatExists = await db.query.chats.findFirst({
where: eq(chats.id, req.params.id),
});
if (!chatExists) {
return res.status(404).json({ message: 'Chat not found' });
return res.status(404).json({ message: "Chat not found" });
}
const chatMessages = await db.query.messages.findMany({
@ -35,7 +35,7 @@ router.get('/:id', async (req, res) => {
return res.status(200).json({ chat: chatExists, messages: chatMessages });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(`Error in getting chat: ${err.message}`);
}
});
@ -47,18 +47,15 @@ router.delete(`/:id`, async (req, res) => {
});
if (!chatExists) {
return res.status(404).json({ message: 'Chat not found' });
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 db.delete(messages).where(eq(messages.chatId, req.params.id)).execute();
return res.status(200).json({ message: 'Chat deleted successfully' });
return res.status(200).json({ message: "Chat deleted successfully" });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(`Error in deleting chat: ${err.message}`);
}
});

View file

@ -1,18 +1,10 @@
import express from 'express';
import {
getAvailableChatModelProviders,
getAvailableEmbeddingModelProviders,
} from '../lib/providers';
import {
getGroqApiKey,
getOllamaApiEndpoint,
getOpenaiApiKey,
updateConfig,
} from '../config';
import express from "express";
import { getAvailableChatModelProviders, getAvailableEmbeddingModelProviders } from "../lib/providers";
import { getGroqApiKey, getOllamaApiEndpoint, getOpenaiApiKey, updateConfig } from "../config";
const router = express.Router();
router.get('/', async (_, res) => {
router.get("/", async (_, res) => {
const config = {};
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
@ -20,29 +12,25 @@ router.get('/', async (_, res) => {
getAvailableEmbeddingModelProviders(),
]);
config['chatModelProviders'] = {};
config['embeddingModelProviders'] = {};
config["chatModelProviders"] = {};
config["embeddingModelProviders"] = {};
for (const provider in chatModelProviders) {
config['chatModelProviders'][provider] = Object.keys(
chatModelProviders[provider],
);
config["chatModelProviders"][provider] = Object.keys(chatModelProviders[provider]);
}
for (const provider in embeddingModelProviders) {
config['embeddingModelProviders'][provider] = Object.keys(
embeddingModelProviders[provider],
);
config["embeddingModelProviders"][provider] = Object.keys(embeddingModelProviders[provider]);
}
config['openaiApiKey'] = getOpenaiApiKey();
config['ollamaApiUrl'] = getOllamaApiEndpoint();
config['groqApiKey'] = getGroqApiKey();
config["openaiApiKey"] = getOpenaiApiKey();
config["ollamaApiUrl"] = getOllamaApiEndpoint();
config["groqApiKey"] = getGroqApiKey();
res.status(200).json(config);
});
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
const config = req.body;
const updatedConfig = {
@ -57,7 +45,7 @@ router.post('/', async (req, res) => {
updateConfig(updatedConfig);
res.status(200).json({ message: 'Config updated' });
res.status(200).json({ message: "Config updated" });
});
export default router;

View file

@ -1,21 +1,21 @@
import express from 'express';
import handleImageSearch from '../agents/imageSearchAgent';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import express from "express";
import handleImageSearch from "../agents/imageSearchAgent";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { getAvailableChatModelProviders } from "../lib/providers";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import logger from "../utils/logger";
const router = express.Router();
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
try {
const { query, chat_history: raw_chat_history, chat_model_provider, chat_model } = req.body;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chat_history = raw_chat_history.map((msg: any) => {
if (msg.role === 'user') {
if (msg.role === "user") {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
} else if (msg.role === "assistant") {
return new AIMessage(msg.content);
}
});
@ -31,7 +31,7 @@ router.post('/', async (req, res) => {
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
res.status(500).json({ message: "Invalid LLM model selected" });
return;
}
@ -39,7 +39,7 @@ router.post('/', async (req, res) => {
res.status(200).json({ images });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(`Error in image search: ${err.message}`);
}
});

View file

@ -1,18 +1,18 @@
import express from 'express';
import imagesRouter from './images';
import videosRouter from './videos';
import configRouter from './config';
import modelsRouter from './models';
import suggestionsRouter from './suggestions';
import chatsRouter from './chats';
import express from "express";
import imagesRouter from "./images";
import videosRouter from "./videos";
import configRouter from "./config";
import modelsRouter from "./models";
import suggestionsRouter from "./suggestions";
import chatsRouter from "./chats";
const router = express.Router();
router.use('/images', imagesRouter);
router.use('/videos', videosRouter);
router.use('/config', configRouter);
router.use('/models', modelsRouter);
router.use('/suggestions', suggestionsRouter);
router.use('/chats', chatsRouter);
router.use("/images", imagesRouter);
router.use("/videos", videosRouter);
router.use("/config", configRouter);
router.use("/models", modelsRouter);
router.use("/suggestions", suggestionsRouter);
router.use("/chats", chatsRouter);
export default router;

View file

@ -1,13 +1,10 @@
import express from 'express';
import logger from '../utils/logger';
import {
getAvailableChatModelProviders,
getAvailableEmbeddingModelProviders,
} from '../lib/providers';
import express from "express";
import logger from "../utils/logger";
import { getAvailableChatModelProviders, getAvailableEmbeddingModelProviders } from "../lib/providers";
const router = express.Router();
router.get('/', async (req, res) => {
router.get("/", async (req, res) => {
try {
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
getAvailableChatModelProviders(),
@ -16,7 +13,7 @@ router.get('/', async (req, res) => {
res.status(200).json({ chatModelProviders, embeddingModelProviders });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(err.message);
}
});

View file

@ -1,21 +1,21 @@
import express from 'express';
import generateSuggestions from '../agents/suggestionGeneratorAgent';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import express from "express";
import generateSuggestions from "../agents/suggestionGeneratorAgent";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { getAvailableChatModelProviders } from "../lib/providers";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import logger from "../utils/logger";
const router = express.Router();
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
try {
const { chat_history: raw_chat_history, chat_model, chat_model_provider } = req.body;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chat_history = raw_chat_history.map((msg: any) => {
if (msg.role === 'user') {
if (msg.role === "user") {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
} else if (msg.role === "assistant") {
return new AIMessage(msg.content);
}
});
@ -31,7 +31,7 @@ router.post('/', async (req, res) => {
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
res.status(500).json({ message: "Invalid LLM model selected" });
return;
}
@ -39,7 +39,7 @@ router.post('/', async (req, res) => {
res.status(200).json({ suggestions: suggestions });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(`Error in generating suggestions: ${err.message}`);
}
});

View file

@ -1,21 +1,21 @@
import express from 'express';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import handleVideoSearch from '../agents/videoSearchAgent';
import express from "express";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { getAvailableChatModelProviders } from "../lib/providers";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import logger from "../utils/logger";
import handleVideoSearch from "../agents/videoSearchAgent";
const router = express.Router();
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
try {
const { query, chat_history: raw_chat_history, chat_model_provider, chat_model } = req.body;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chat_history = raw_chat_history.map((msg: any) => {
if (msg.role === 'user') {
if (msg.role === "user") {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
} else if (msg.role === "assistant") {
return new AIMessage(msg.content);
}
});
@ -31,7 +31,7 @@ router.post('/', async (req, res) => {
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
res.status(500).json({ message: "Invalid LLM model selected" });
return;
}
@ -39,7 +39,7 @@ router.post('/', async (req, res) => {
res.status(200).json({ videos });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
res.status(500).json({ message: "An error has occurred." });
logger.error(`Error in video search: ${err.message}`);
}
});

View file

@ -1,17 +1,17 @@
import dot from 'compute-dot';
import cosineSimilarity from 'compute-cosine-similarity';
import { getSimilarityMeasure } from '../config';
import dot from "compute-dot";
import cosineSimilarity from "compute-cosine-similarity";
import { getSimilarityMeasure } from "../config";
const computeSimilarity = (x: number[], y: number[]): number => {
const similarityMeasure = getSimilarityMeasure();
if (similarityMeasure === 'cosine') {
if (similarityMeasure === "cosine") {
return cosineSimilarity(x, y);
} else if (similarityMeasure === 'dot') {
} else if (similarityMeasure === "dot") {
return dot(x, y);
}
throw new Error('Invalid similarity measure');
throw new Error("Invalid similarity measure");
};
export default computeSimilarity;

View file

@ -1,9 +1,7 @@
import { BaseMessage } from '@langchain/core/messages';
import { BaseMessage } from "@langchain/core/messages";
const formatChatHistoryAsString = (history: BaseMessage[]) => {
return history
.map((message) => `${message._getType()}: ${message.content}`)
.join('\n');
return history.map(message => `${message._getType()}: ${message.content}`).join("\n");
};
export default formatChatHistoryAsString;

View file

@ -1,20 +1,14 @@
import winston from 'winston';
import winston from "winston";
const logger = winston.createLogger({
level: 'info',
level: "info",
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
}),
new winston.transports.File({
filename: 'app.log',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
filename: "app.log",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
}),
],
});

View file

@ -1,41 +1,28 @@
import { WebSocket } from 'ws';
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 logger from '../utils/logger';
import { ChatOpenAI } from '@langchain/openai';
import { WebSocket } from "ws";
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 logger from "../utils/logger";
import { ChatOpenAI } from "@langchain/openai";
export const handleConnection = async (
ws: WebSocket,
request: IncomingMessage,
) => {
export const handleConnection = async (ws: WebSocket, request: IncomingMessage) => {
try {
const searchParams = new URL(request.url, `http://${request.headers.host}`)
.searchParams;
const searchParams = 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 = searchParams.get("chatModelProvider") || Object.keys(chatModelProviders)[0];
const chatModel = searchParams.get("chatModel") || Object.keys(chatModelProviders[chatModelProvider])[0];
const embeddingModelProvider =
searchParams.get('embeddingModelProvider') ||
Object.keys(embeddingModelProviders)[0];
searchParams.get("embeddingModelProvider") || Object.keys(embeddingModelProviders)[0];
const embeddingModel =
searchParams.get('embeddingModel') ||
Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
searchParams.get("embeddingModel") || Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
let llm: BaseChatModel | undefined;
let embeddings: Embeddings | undefined;
@ -43,18 +30,16 @@ export const handleConnection = async (
if (
chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel] &&
chatModelProvider != 'custom_openai'
chatModelProvider != "custom_openai"
) {
llm = chatModelProviders[chatModelProvider][chatModel] as
| BaseChatModel
| undefined;
} else if (chatModelProvider == 'custom_openai') {
llm = chatModelProviders[chatModelProvider][chatModel] as BaseChatModel | undefined;
} else if (chatModelProvider == "custom_openai") {
llm = new ChatOpenAI({
modelName: chatModel,
openAIApiKey: searchParams.get('openAIApiKey'),
openAIApiKey: searchParams.get("openAIApiKey"),
temperature: 0.7,
configuration: {
baseURL: searchParams.get('openAIBaseURL'),
baseURL: searchParams.get("openAIBaseURL"),
},
});
}
@ -63,35 +48,29 @@ export const handleConnection = async (
embeddingModelProviders[embeddingModelProvider] &&
embeddingModelProviders[embeddingModelProvider][embeddingModel]
) {
embeddings = embeddingModelProviders[embeddingModelProvider][
embeddingModel
] as Embeddings | undefined;
embeddings = embeddingModelProviders[embeddingModelProvider][embeddingModel] as Embeddings | undefined;
}
if (!llm || !embeddings) {
ws.send(
JSON.stringify({
type: 'error',
data: 'Invalid LLM or embeddings model selected, please refresh the page and try again.',
key: 'INVALID_MODEL_SELECTED',
type: "error",
data: "Invalid LLM or embeddings model selected, please refresh the page and try again.",
key: "INVALID_MODEL_SELECTED",
}),
);
ws.close();
}
ws.on(
'message',
async (message) =>
await handleMessage(message.toString(), ws, llm, embeddings),
);
ws.on("message", async message => await handleMessage(message.toString(), ws, llm, embeddings));
ws.on('close', () => logger.debug('Connection closed'));
ws.on("close", () => logger.debug("Connection closed"));
} catch (err) {
ws.send(
JSON.stringify({
type: 'error',
data: 'Internal server error.',
key: 'INTERNAL_SERVER_ERROR',
type: "error",
data: "Internal server error.",
key: "INTERNAL_SERVER_ERROR",
}),
);
ws.close();

View file

@ -1,8 +1,6 @@
import { initServer } from './websocketServer';
import http from 'http';
import { initServer } from "./websocketServer";
import http from "http";
export const startWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
export const startWebSocketServer = (server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>) => {
initServer(server);
};

View file

@ -1,18 +1,18 @@
import { EventEmitter, WebSocket } from 'ws';
import { BaseMessage, AIMessage, HumanMessage } from '@langchain/core/messages';
import handleWebSearch from '../agents/webSearchAgent';
import handleAcademicSearch from '../agents/academicSearchAgent';
import handleWritingAssistant from '../agents/writingAssistant';
import handleWolframAlphaSearch from '../agents/wolframAlphaSearchAgent';
import handleYoutubeSearch from '../agents/youtubeSearchAgent';
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 { chats, messages } from '../db/schema';
import { eq } from 'drizzle-orm';
import crypto from 'crypto';
import { EventEmitter, WebSocket } from "ws";
import { BaseMessage, AIMessage, HumanMessage } from "@langchain/core/messages";
import handleWebSearch from "../agents/webSearchAgent";
import handleAcademicSearch from "../agents/academicSearchAgent";
import handleWritingAssistant from "../agents/writingAssistant";
import handleWolframAlphaSearch from "../agents/wolframAlphaSearchAgent";
import handleYoutubeSearch from "../agents/youtubeSearchAgent";
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 { chats, messages } from "../db/schema";
import { eq } from "drizzle-orm";
import crypto from "crypto";
type Message = {
messageId: string;
@ -37,30 +37,25 @@ const searchHandlers = {
redditSearch: handleRedditSearch,
};
const handleEmitterEvents = (
emitter: EventEmitter,
ws: WebSocket,
messageId: string,
chatId: string,
) => {
let recievedMessage = '';
const handleEmitterEvents = (emitter: EventEmitter, ws: WebSocket, messageId: string, chatId: string) => {
let recievedMessage = "";
let sources = [];
emitter.on('data', (data) => {
emitter.on("data", data => {
const parsedData = JSON.parse(data);
if (parsedData.type === 'response') {
if (parsedData.type === "response") {
ws.send(
JSON.stringify({
type: 'message',
type: "message",
data: parsedData.data,
messageId: messageId,
}),
);
recievedMessage += parsedData.data;
} else if (parsedData.type === 'sources') {
} else if (parsedData.type === "sources") {
ws.send(
JSON.stringify({
type: 'sources',
type: "sources",
data: parsedData.data,
messageId: messageId,
}),
@ -68,15 +63,15 @@ const handleEmitterEvents = (
sources = parsedData.data;
}
});
emitter.on('end', () => {
ws.send(JSON.stringify({ type: 'messageEnd', messageId: messageId }));
emitter.on("end", () => {
ws.send(JSON.stringify({ type: "messageEnd", messageId: messageId }));
db.insert(messages)
.values({
content: recievedMessage,
chatId: chatId,
messageId: messageId,
role: 'assistant',
role: "assistant",
metadata: JSON.stringify({
createdAt: new Date(),
...(sources && sources.length > 0 && { sources }),
@ -84,41 +79,36 @@ const handleEmitterEvents = (
})
.execute();
});
emitter.on('error', (data) => {
emitter.on("error", data => {
const parsedData = JSON.parse(data);
ws.send(
JSON.stringify({
type: 'error',
type: "error",
data: parsedData.data,
key: 'CHAIN_ERROR',
key: "CHAIN_ERROR",
}),
);
});
};
export const handleMessage = async (
message: string,
ws: WebSocket,
llm: BaseChatModel,
embeddings: Embeddings,
) => {
export const handleMessage = async (message: string, ws: WebSocket, llm: BaseChatModel, embeddings: Embeddings) => {
try {
const parsedWSMessage = JSON.parse(message) as WSMessage;
const parsedMessage = parsedWSMessage.message;
const id = crypto.randomBytes(7).toString('hex');
const id = crypto.randomBytes(7).toString("hex");
if (!parsedMessage.content)
return ws.send(
JSON.stringify({
type: 'error',
data: 'Invalid message format',
key: 'INVALID_FORMAT',
type: "error",
data: "Invalid message format",
key: "INVALID_FORMAT",
}),
);
const history: BaseMessage[] = parsedWSMessage.history.map((msg) => {
if (msg[0] === 'human') {
const history: BaseMessage[] = parsedWSMessage.history.map(msg => {
if (msg[0] === "human") {
return new HumanMessage({
content: msg[1],
});
@ -129,16 +119,11 @@ export const handleMessage = async (
}
});
if (parsedWSMessage.type === 'message') {
if (parsedWSMessage.type === "message") {
const handler = searchHandlers[parsedWSMessage.focusMode];
if (handler) {
const emitter = handler(
parsedMessage.content,
history,
llm,
embeddings,
);
const emitter = handler(parsedMessage.content, history, llm, embeddings);
handleEmitterEvents(emitter, ws, id, parsedMessage.chatId);
@ -164,7 +149,7 @@ export const handleMessage = async (
content: parsedMessage.content,
chatId: parsedMessage.chatId,
messageId: id,
role: 'user',
role: "user",
metadata: JSON.stringify({
createdAt: new Date(),
}),
@ -173,9 +158,9 @@ export const handleMessage = async (
} else {
ws.send(
JSON.stringify({
type: 'error',
data: 'Invalid focus mode',
key: 'INVALID_FOCUS_MODE',
type: "error",
data: "Invalid focus mode",
key: "INVALID_FOCUS_MODE",
}),
);
}
@ -183,9 +168,9 @@ export const handleMessage = async (
} catch (err) {
ws.send(
JSON.stringify({
type: 'error',
data: 'Invalid message format',
key: 'INVALID_FORMAT',
type: "error",
data: "Invalid message format",
key: "INVALID_FORMAT",
}),
);
logger.error(`Failed to handle message: ${err}`);

View file

@ -1,16 +1,14 @@
import { WebSocketServer } from 'ws';
import { handleConnection } from './connectionManager';
import http from 'http';
import { getPort } from '../config';
import logger from '../utils/logger';
import { WebSocketServer } from "ws";
import { handleConnection } from "./connectionManager";
import http from "http";
import { getPort } from "../config";
import logger from "../utils/logger";
export const initServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
export const initServer = (server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>) => {
const port = getPort();
const wss = new WebSocketServer({ server });
wss.on('connection', handleConnection);
wss.on("connection", handleConnection);
logger.info(`WebSocket server started on port ${port}`);
};

View file

@ -1,4 +1,4 @@
import ChatWindow from '@/components/ChatWindow';
import ChatWindow from "@/components/ChatWindow";
const Page = ({ params }: { params: { chatId: string } }) => {
return <ChatWindow id={params.chatId} />;

View file

@ -1,22 +1,21 @@
import type { Metadata } from 'next';
import { Montserrat } from 'next/font/google';
import './globals.css';
import { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider';
import type { Metadata } from "next";
import { Montserrat } from "next/font/google";
import "./globals.css";
import { cn } from "@/lib/utils";
import Sidebar from "@/components/Sidebar";
import { Toaster } from "sonner";
import ThemeProvider from "@/components/theme/Provider";
const montserrat = Montserrat({
weight: ['300', '400', '500', '700'],
subsets: ['latin'],
display: 'swap',
fallback: ['Arial', 'sans-serif'],
weight: ["300", "400", "500", "700"],
subsets: ["latin"],
display: "swap",
fallback: ["Arial", "sans-serif"],
});
export const metadata: Metadata = {
title: 'Perplexica - Chat with the internet',
description:
'Perplexica is an AI powered chatbot that is connected to the internet.',
title: "Perplexica - Chat with the internet",
description: "Perplexica is an AI powered chatbot that is connected to the internet.",
};
export default function RootLayout({
@ -26,7 +25,7 @@ export default function RootLayout({
}>) {
return (
<html className="h-full" lang="en" suppressHydrationWarning>
<body className={cn('h-full', montserrat.className)}>
<body className={cn("h-full", montserrat.className)}>
<ThemeProvider>
<Sidebar>{children}</Sidebar>
<Toaster
@ -34,7 +33,7 @@ export default function RootLayout({
unstyled: true,
classNames: {
toast:
'bg-light-primary dark:bg-dark-primary text-white rounded-lg p-4 flex flex-row items-center space-x-2',
"bg-light-primary dark:bg-dark-primary text-white rounded-lg p-4 flex flex-row items-center space-x-2",
},
}}
/>

View file

@ -1,8 +1,8 @@
import { Metadata } from 'next';
import React from 'react';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: 'Library - Perplexica',
title: "Library - Perplexica",
};
const Layout = ({ children }: { children: React.ReactNode }) => {

View file

@ -1,10 +1,10 @@
'use client';
"use client";
import DeleteChat from '@/components/DeleteChat';
import { formatTimeDifference } from '@/lib/utils';
import { BookOpenText, ClockIcon } from 'lucide-react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import DeleteChat from "@/components/DeleteChat";
import { formatTimeDifference } from "@/lib/utils";
import { BookOpenText, ClockIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
export interface Chat {
id: string;
@ -22,9 +22,9 @@ const Page = () => {
setLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats`, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
@ -61,16 +61,12 @@ const Page = () => {
<div className="fixed z-40 top-0 left-0 right-0 lg:pl-[104px] lg:pr-6 lg:px-8 px-4 py-4 lg:py-6 border-b border-light-200 dark:border-dark-200">
<div className="flex flex-row items-center space-x-2 max-w-screen-lg lg:mx-auto">
<BookOpenText />
<h2 className="text-black dark:text-white lg:text-3xl lg:font-medium">
Library
</h2>
<h2 className="text-black dark:text-white lg:text-3xl lg:font-medium">Library</h2>
</div>
</div>
{chats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-screen">
<p className="text-black/70 dark:text-white/70 text-sm">
No chats found.
</p>
<p className="text-black/70 dark:text-white/70 text-sm">No chats found.</p>
</div>
)}
{chats.length > 0 && (
@ -89,15 +85,9 @@ const Page = () => {
<div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
<ClockIcon size={15} />
<p className="text-xs">
{formatTimeDifference(new Date(), chat.createdAt)} Ago
</p>
<p className="text-xs">{formatTimeDifference(new Date(), chat.createdAt)} Ago</p>
</div>
<DeleteChat
chatId={chat.id}
chats={chats}
setChats={setChats}
/>
<DeleteChat chatId={chat.id} chats={chats} setChats={setChats} />
</div>
</div>
))}

View file

@ -1,10 +1,10 @@
import ChatWindow from '@/components/ChatWindow';
import { Metadata } from 'next';
import { Suspense } from 'react';
import ChatWindow from "@/components/ChatWindow";
import { Metadata } from "next";
import { Suspense } from "react";
export const metadata: Metadata = {
title: 'Chat - Perplexica',
description: 'Chat with the internet, chat with Perplexica.',
title: "Chat - Perplexica",
description: "Chat with the internet, chat with Perplexica.",
};
const Home = () => {

View file

@ -1,10 +1,10 @@
'use client';
"use client";
import { Fragment, useEffect, useRef, useState } from 'react';
import MessageInput from './MessageInput';
import { Message } from './ChatWindow';
import MessageBox from './MessageBox';
import MessageBoxLoading from './MessageBoxLoading';
import { Fragment, useEffect, useRef, useState } from "react";
import MessageInput from "./MessageInput";
import { Message } from "./ChatWindow";
import MessageBox from "./MessageBox";
import MessageBoxLoading from "./MessageBoxLoading";
const Chat = ({
loading,
@ -32,15 +32,15 @@ const Chat = ({
updateDividerWidth();
window.addEventListener('resize', updateDividerWidth);
window.addEventListener("resize", updateDividerWidth);
return () => {
window.removeEventListener('resize', updateDividerWidth);
window.removeEventListener("resize", updateDividerWidth);
};
});
useEffect(() => {
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
messageEnd.current?.scrollIntoView({ behavior: "smooth" });
if (messages.length === 1) {
document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
@ -65,7 +65,7 @@ const Chat = ({
rewrite={rewrite}
sendMessage={sendMessage}
/>
{!isLast && msg.role === 'assistant' && (
{!isLast && msg.role === "assistant" && (
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
)}
</Fragment>
@ -74,10 +74,7 @@ const Chat = ({
{loading && !messageAppeared && <MessageBoxLoading />}
<div ref={messageEnd} className="h-0" />
{dividerWidth > 0 && (
<div
className="bottom-24 lg:bottom-10 fixed z-40"
style={{ width: dividerWidth }}
>
<div className="bottom-24 lg:bottom-10 fixed z-40" style={{ width: dividerWidth }}>
<MessageInput loading={loading} sendMessage={sendMessage} />
</div>
)}

View file

@ -1,110 +1,79 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
"use client";
import { useEffect, useRef, useState } from 'react';
import { Document } from '@langchain/core/documents';
import Navbar from './Navbar';
import Chat from './Chat';
import EmptyChat from './EmptyChat';
import crypto from 'crypto';
import { toast } from 'sonner';
import { useSearchParams } from 'next/navigation';
import { getSuggestions } from '@/lib/actions';
import Error from 'next/error';
import { useEffect, useRef, useState } from "react";
import { Document } from "@langchain/core/documents";
import Navbar from "./Navbar";
import Chat from "./Chat";
import EmptyChat from "./EmptyChat";
import crypto from "crypto";
import { toast } from "sonner";
import { useSearchParams } from "next/navigation";
import { getSuggestions } from "@/lib/actions";
import Error from "next/error";
export type Message = {
messageId: string;
chatId: string;
createdAt: Date;
content: string;
role: 'user' | 'assistant';
role: "user" | "assistant";
suggestions?: string[];
sources?: Document[];
};
const useSocket = (
url: string,
setIsWSReady: (ready: boolean) => void,
setError: (error: boolean) => void,
) => {
const useSocket = (url: string, setIsWSReady: (ready: boolean) => void, setError: (error: boolean) => void) => {
const [ws, setWs] = useState<WebSocket | null>(null);
useEffect(() => {
if (!ws) {
const connectWs = async () => {
let chatModel = localStorage.getItem('chatModel');
let chatModelProvider = localStorage.getItem('chatModelProvider');
let embeddingModel = localStorage.getItem('embeddingModel');
let embeddingModelProvider = localStorage.getItem(
'embeddingModelProvider',
);
let chatModel = localStorage.getItem("chatModel");
let chatModelProvider = localStorage.getItem("chatModelProvider");
let embeddingModel = localStorage.getItem("embeddingModel");
let embeddingModelProvider = localStorage.getItem("embeddingModelProvider");
if (
!chatModel ||
!chatModelProvider ||
!embeddingModel ||
!embeddingModelProvider
) {
const providers = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/models`,
{
headers: {
'Content-Type': 'application/json',
},
if (!chatModel || !chatModelProvider || !embeddingModel || !embeddingModelProvider) {
const providers = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/models`, {
headers: {
"Content-Type": "application/json",
},
).then(async (res) => await res.json());
}).then(async res => await res.json());
const chatModelProviders = providers.chatModelProviders;
const embeddingModelProviders = providers.embeddingModelProviders;
if (
!chatModelProviders ||
Object.keys(chatModelProviders).length === 0
)
return toast.error('No chat models available');
if (!chatModelProviders || Object.keys(chatModelProviders).length === 0)
return toast.error("No chat models available");
if (
!embeddingModelProviders ||
Object.keys(embeddingModelProviders).length === 0
)
return toast.error('No embedding models available');
if (!embeddingModelProviders || Object.keys(embeddingModelProviders).length === 0)
return toast.error("No embedding models available");
chatModelProvider = Object.keys(chatModelProviders)[0];
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
embeddingModel = Object.keys(
embeddingModelProviders[embeddingModelProvider],
)[0];
embeddingModel = Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
localStorage.setItem('chatModel', chatModel!);
localStorage.setItem('chatModelProvider', chatModelProvider);
localStorage.setItem('embeddingModel', embeddingModel!);
localStorage.setItem(
'embeddingModelProvider',
embeddingModelProvider,
);
localStorage.setItem("chatModel", chatModel!);
localStorage.setItem("chatModelProvider", chatModelProvider);
localStorage.setItem("embeddingModel", embeddingModel!);
localStorage.setItem("embeddingModelProvider", embeddingModelProvider);
}
const wsURL = new URL(url);
const searchParams = new URLSearchParams({});
searchParams.append('chatModel', chatModel!);
searchParams.append('chatModelProvider', chatModelProvider);
searchParams.append("chatModel", chatModel!);
searchParams.append("chatModelProvider", chatModelProvider);
if (chatModelProvider === 'custom_openai') {
searchParams.append(
'openAIApiKey',
localStorage.getItem('openAIApiKey')!,
);
searchParams.append(
'openAIBaseURL',
localStorage.getItem('openAIBaseURL')!,
);
if (chatModelProvider === "custom_openai") {
searchParams.append("openAIApiKey", localStorage.getItem("openAIApiKey")!);
searchParams.append("openAIBaseURL", localStorage.getItem("openAIBaseURL")!);
}
searchParams.append('embeddingModel', embeddingModel!);
searchParams.append('embeddingModelProvider', embeddingModelProvider);
searchParams.append("embeddingModel", embeddingModel!);
searchParams.append("embeddingModelProvider", embeddingModelProvider);
wsURL.search = searchParams.toString();
@ -114,14 +83,12 @@ const useSocket = (
if (ws.readyState !== 1) {
ws.close();
setError(true);
toast.error(
'Failed to connect to the server. Please try again later.',
);
toast.error("Failed to connect to the server. Please try again later.");
}
}, 10000);
ws.onopen = () => {
console.log('[DEBUG] open');
console.log("[DEBUG] open");
clearTimeout(timeoutId);
setError(false);
setIsWSReady(true);
@ -130,13 +97,13 @@ const useSocket = (
ws.onerror = () => {
clearTimeout(timeoutId);
setError(true);
toast.error('WebSocket connection error.');
toast.error("WebSocket connection error.");
};
ws.onclose = () => {
clearTimeout(timeoutId);
setError(true);
console.log('[DEBUG] closed');
console.log("[DEBUG] closed");
};
setWs(ws);
@ -147,7 +114,7 @@ const useSocket = (
return () => {
ws?.close();
console.log('[DEBUG] closed');
console.log("[DEBUG] closed");
};
}, [ws, url, setIsWSReady, setError]);
@ -162,15 +129,12 @@ const loadMessages = async (
setFocusMode: (mode: string) => void,
setNotFound: (notFound: boolean) => void,
) => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
);
});
if (res.status === 404) {
setNotFound(true);
@ -189,11 +153,11 @@ const loadMessages = async (
setMessages(messages);
const history = messages.map((msg) => {
const history = messages.map(msg => {
return [msg.role, msg.content];
}) as [string, string][];
console.log('[DEBUG] messages loaded');
console.log("[DEBUG] messages loaded");
document.title = messages[0].content;
@ -204,7 +168,7 @@ const loadMessages = async (
const ChatWindow = ({ id }: { id?: string }) => {
const searchParams = useSearchParams();
const initialMessage = searchParams.get('q');
const initialMessage = searchParams.get("q");
const [chatId, setChatId] = useState<string | undefined>(id);
const [newChatCreated, setNewChatCreated] = useState(false);
@ -213,11 +177,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [isReady, setIsReady] = useState(false);
const [isWSReady, setIsWSReady] = useState(false);
const ws = useSocket(
process.env.NEXT_PUBLIC_WS_URL!,
setIsWSReady,
setHasError,
);
const ws = useSocket(process.env.NEXT_PUBLIC_WS_URL!, setIsWSReady, setHasError);
const [loading, setLoading] = useState(false);
const [messageAppeared, setMessageAppeared] = useState(false);
@ -225,31 +185,19 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [focusMode, setFocusMode] = useState('webSearch');
const [focusMode, setFocusMode] = useState("webSearch");
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
if (
chatId &&
!newChatCreated &&
!isMessagesLoaded &&
messages.length === 0
) {
loadMessages(
chatId,
setMessages,
setIsMessagesLoaded,
setChatHistory,
setFocusMode,
setNotFound,
);
if (chatId && !newChatCreated && !isMessagesLoaded && messages.length === 0) {
loadMessages(chatId, setMessages, setIsMessagesLoaded, setChatHistory, setFocusMode, setNotFound);
} else if (!chatId) {
setNewChatCreated(true);
setIsMessagesLoaded(true);
setChatId(crypto.randomBytes(20).toString('hex'));
setChatId(crypto.randomBytes(20).toString("hex"));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -272,30 +220,30 @@ const ChatWindow = ({ id }: { id?: string }) => {
setMessageAppeared(false);
let sources: Document[] | undefined = undefined;
let recievedMessage = '';
let recievedMessage = "";
let added = false;
const messageId = crypto.randomBytes(7).toString('hex');
const messageId = crypto.randomBytes(7).toString("hex");
ws?.send(
JSON.stringify({
type: 'message',
type: "message",
message: {
chatId: chatId!,
content: message,
},
focusMode: focusMode,
history: [...chatHistory, ['human', message]],
history: [...chatHistory, ["human", message]],
}),
);
setMessages((prevMessages) => [
setMessages(prevMessages => [
...prevMessages,
{
content: message,
messageId: messageId,
chatId: chatId!,
role: 'user',
role: "user",
createdAt: new Date(),
},
]);
@ -303,22 +251,22 @@ const ChatWindow = ({ id }: { id?: string }) => {
const messageHandler = async (e: MessageEvent) => {
const data = JSON.parse(e.data);
if (data.type === 'error') {
if (data.type === "error") {
toast.error(data.data);
setLoading(false);
return;
}
if (data.type === 'sources') {
if (data.type === "sources") {
sources = data.data;
if (!added) {
setMessages((prevMessages) => [
setMessages(prevMessages => [
...prevMessages,
{
content: '',
content: "",
messageId: data.messageId,
chatId: chatId!,
role: 'assistant',
role: "assistant",
sources: sources,
createdAt: new Date(),
},
@ -328,15 +276,15 @@ const ChatWindow = ({ id }: { id?: string }) => {
setMessageAppeared(true);
}
if (data.type === 'message') {
if (data.type === "message") {
if (!added) {
setMessages((prevMessages) => [
setMessages(prevMessages => [
...prevMessages,
{
content: data.data,
messageId: data.messageId,
chatId: chatId!,
role: 'assistant',
role: "assistant",
sources: sources,
createdAt: new Date(),
},
@ -344,8 +292,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
added = true;
}
setMessages((prev) =>
prev.map((message) => {
setMessages(prev =>
prev.map(message => {
if (message.messageId === data.messageId) {
return { ...message, content: message.content + data.data };
}
@ -358,27 +306,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
setMessageAppeared(true);
}
if (data.type === 'messageEnd') {
setChatHistory((prevHistory) => [
...prevHistory,
['human', message],
['assistant', recievedMessage],
]);
if (data.type === "messageEnd") {
setChatHistory(prevHistory => [...prevHistory, ["human", message], ["assistant", recievedMessage]]);
ws?.removeEventListener('message', messageHandler);
ws?.removeEventListener("message", messageHandler);
setLoading(false);
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
if (
lastMsg.role === 'assistant' &&
lastMsg.sources &&
lastMsg.sources.length > 0 &&
!lastMsg.suggestions
) {
if (lastMsg.role === "assistant" && lastMsg.sources && lastMsg.sources.length > 0 && !lastMsg.suggestions) {
const suggestions = await getSuggestions(messagesRef.current);
setMessages((prev) =>
prev.map((msg) => {
setMessages(prev =>
prev.map(msg => {
if (msg.messageId === lastMsg.messageId) {
return { ...msg, suggestions: suggestions };
}
@ -389,20 +328,20 @@ const ChatWindow = ({ id }: { id?: string }) => {
}
};
ws?.addEventListener('message', messageHandler);
ws?.addEventListener("message", messageHandler);
};
const rewrite = (messageId: string) => {
const index = messages.findIndex((msg) => msg.messageId === messageId);
const index = messages.findIndex(msg => msg.messageId === messageId);
if (index === -1) return;
const message = messages[index - 1];
setMessages((prev) => {
setMessages(prev => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
setChatHistory((prev) => {
setChatHistory(prev => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
@ -443,11 +382,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
/>
</>
) : (
<EmptyChat
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}
/>
<EmptyChat sendMessage={sendMessage} focusMode={focusMode} setFocusMode={setFocusMode} />
)}
</div>
)

View file

@ -1,8 +1,8 @@
import { Delete, Trash } from 'lucide-react';
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, useState } from 'react';
import { toast } from 'sonner';
import { Chat } from '@/app/library/page';
import { Delete, Trash } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
import { toast } from "sonner";
import { Chat } from "@/app/library/page";
const DeleteChat = ({
chatId,
@ -19,21 +19,18 @@ const DeleteChat = ({
const handleDelete = async () => {
setLoading(true);
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
);
});
if (res.status != 200) {
throw new Error('Failed to delete chat');
throw new Error("Failed to delete chat");
}
const newChats = chats.filter((chat) => chat.id !== chatId);
const newChats = chats.filter(chat => chat.id !== chatId);
setChats(newChats);
} catch (err: any) {

View file

@ -1,4 +1,4 @@
import EmptyChatMessageInput from './EmptyChatMessageInput';
import EmptyChatMessageInput from "./EmptyChatMessageInput";
const EmptyChat = ({
sendMessage,
@ -12,14 +12,8 @@ const EmptyChat = ({
return (
<div className="relative">
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
Research begins here.
</h2>
<EmptyChatMessageInput
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}
/>
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">Research begins here.</h2>
<EmptyChatMessageInput sendMessage={sendMessage} focusMode={focusMode} setFocusMode={setFocusMode} />
</div>
</div>
);

View file

@ -1,8 +1,8 @@
import { ArrowRight } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import CopilotToggle from './MessageInputActions/Copilot';
import Focus from './MessageInputActions/Focus';
import { ArrowRight } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import CopilotToggle from "./MessageInputActions/Copilot";
import Focus from "./MessageInputActions/Focus";
const EmptyChatMessageInput = ({
sendMessage,
@ -14,37 +14,37 @@ const EmptyChatMessageInput = ({
setFocusMode: (mode: string) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
const [message, setMessage] = useState("");
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === '/') {
if (e.key === "/") {
e.preventDefault();
inputRef.current?.focus();
}
};
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<form
onSubmit={(e) => {
onSubmit={e => {
e.preventDefault();
sendMessage(message);
setMessage('');
setMessage("");
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onKeyDown={e => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage(message);
setMessage('');
setMessage("");
}
}}
className="w-full"
@ -53,7 +53,7 @@ const EmptyChatMessageInput = ({
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onChange={e => setMessage(e.target.value)}
minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..."
@ -64,10 +64,7 @@ const EmptyChatMessageInput = ({
{/* <Attach /> */}
</div>
<div className="flex flex-row items-center space-x-4 -mx-2">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
<CopilotToggle copilotEnabled={copilotEnabled} setCopilotEnabled={setCopilotEnabled} />
<button
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"

View file

@ -1,14 +1,8 @@
import { Check, ClipboardList } from 'lucide-react';
import { Message } from '../ChatWindow';
import { useState } from 'react';
import { Check, ClipboardList } from "lucide-react";
import { Message } from "../ChatWindow";
import { useState } from "react";
const Copy = ({
message,
initialMessage,
}: {
message: Message;
initialMessage: string;
}) => {
const Copy = ({ message, initialMessage }: { message: Message; initialMessage: string }) => {
const [copied, setCopied] = useState(false);
return (

View file

@ -1,12 +1,6 @@
import { ArrowLeftRight } from 'lucide-react';
import { ArrowLeftRight } from "lucide-react";
const Rewrite = ({
rewrite,
messageId,
}: {
rewrite: (messageId: string) => void;
messageId: string;
}) => {
const Rewrite = ({ rewrite, messageId }: { rewrite: (messageId: string) => void; messageId: string }) => {
return (
<button
onClick={() => rewrite(messageId)}

View file

@ -1,24 +1,17 @@
'use client';
"use client";
/* eslint-disable @next/next/no-img-element */
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils';
import {
BookCopy,
Disc3,
Volume2,
StopCircle,
Layers3,
Plus,
} from 'lucide-react';
import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech';
import React, { MutableRefObject, useEffect, useState } from "react";
import { Message } from "./ChatWindow";
import { cn } from "@/lib/utils";
import { BookCopy, Disc3, Volume2, StopCircle, Layers3, Plus } from "lucide-react";
import Markdown from "markdown-to-jsx";
import Copy from "./MessageActions/Copy";
import Rewrite from "./MessageActions/Rewrite";
import MessageSources from "./MessageSources";
import SearchImages from "./SearchImages";
import SearchVideos from "./SearchVideos";
import { useSpeech } from "react-text-to-speech";
const MessageBox = ({
message,
@ -45,11 +38,7 @@ const MessageBox = ({
useEffect(() => {
const regex = /\[(\d+)\]/g;
if (
message.role === 'assistant' &&
message?.sources &&
message.sources.length > 0
) {
if (message.role === "assistant" && message?.sources && message.sources.length > 0) {
return setParsedMessage(
message.content.replace(
regex,
@ -59,7 +48,7 @@ const MessageBox = ({
);
}
setSpeechMessage(message.content.replace(regex, ''));
setSpeechMessage(message.content.replace(regex, ""));
setParsedMessage(message.content);
}, [message.content, message.sources, message.role]);
@ -67,27 +56,20 @@ const MessageBox = ({
return (
<div>
{message.role === 'user' && (
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{message.content}
</h2>
{message.role === "user" && (
<div className={cn("w-full", messageIndex === 0 ? "pt-16" : "pt-8")}>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">{message.content}</h2>
</div>
)}
{message.role === 'assistant' && (
{message.role === "assistant" && (
<div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9">
<div
ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12"
>
<div ref={dividerRef} className="flex flex-col space-y-6 w-full lg:w-9/12">
{message.sources && message.sources.length > 0 && (
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">
<BookCopy className="text-black dark:text-white" size={20} />
<h3 className="text-black dark:text-white font-medium text-xl">
Sources
</h3>
<h3 className="text-black dark:text-white font-medium text-xl">Sources</h3>
</div>
<MessageSources sources={message.sources} />
</div>
@ -95,20 +77,15 @@ const MessageBox = ({
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
'text-black dark:text-white',
isLast && loading ? 'animate-spin' : 'animate-none',
)}
className={cn("text-black dark:text-white", isLast && loading ? "animate-spin" : "animate-none")}
size={20}
/>
<h3 className="text-black dark:text-white font-medium text-xl">
Answer
</h3>
<h3 className="text-black dark:text-white font-medium text-xl">Answer</h3>
</div>
<Markdown
className={cn(
'prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0',
'max-w-none break-words text-black dark:text-white text-sm md:text-base font-medium',
"prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0",
"max-w-none break-words text-black dark:text-white text-sm md:text-base font-medium",
)}
>
{parsedMessage}
@ -125,7 +102,7 @@ const MessageBox = ({
<Copy initialMessage={message.content} message={message} />
<button
onClick={() => {
if (speechStatus === 'started') {
if (speechStatus === "started") {
stop();
} else {
start();
@ -133,11 +110,7 @@ const MessageBox = ({
}}
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
>
{speechStatus === 'started' ? (
<StopCircle size={18} />
) : (
<Volume2 size={18} />
)}
{speechStatus === "started" ? <StopCircle size={18} /> : <Volume2 size={18} />}
</button>
</div>
</div>
@ -145,7 +118,7 @@ const MessageBox = ({
{isLast &&
message.suggestions &&
message.suggestions.length > 0 &&
message.role === 'assistant' &&
message.role === "assistant" &&
!loading && (
<>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
@ -156,10 +129,7 @@ const MessageBox = ({
</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}
>
<div className="flex flex-col space-y-3 text-sm" key={i}>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div
onClick={() => {
@ -167,13 +137,8 @@ const MessageBox = ({
}}
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
>
<p className="transition duration-200 hover:text-[#24A0ED]">
{suggestion}
</p>
<Plus
size={20}
className="text-[#24A0ED] flex-shrink-0"
/>
<p className="transition duration-200 hover:text-[#24A0ED]">{suggestion}</p>
<Plus size={20} className="text-[#24A0ED] flex-shrink-0" />
</div>
</div>
))}
@ -184,14 +149,8 @@ const MessageBox = ({
</div>
</div>
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
<SearchImages
query={history[messageIndex - 1].content}
chat_history={history.slice(0, messageIndex - 1)}
/>
<SearchVideos
chat_history={history.slice(0, messageIndex - 1)}
query={history[messageIndex - 1].content}
/>
<SearchImages query={history[messageIndex - 1].content} chat_history={history.slice(0, messageIndex - 1)} />
<SearchVideos chat_history={history.slice(0, messageIndex - 1)} query={history[messageIndex - 1].content} />
</div>
</div>
)}

View file

@ -1,84 +1,75 @@
import { cn } from '@/lib/utils';
import { ArrowUp } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot';
import { cn } from "@/lib/utils";
import { ArrowUp } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import Attach from "./MessageInputActions/Attach";
import CopilotToggle from "./MessageInputActions/Copilot";
const MessageInput = ({
sendMessage,
loading,
}: {
sendMessage: (message: string) => void;
loading: boolean;
}) => {
const MessageInput = ({ sendMessage, loading }: { sendMessage: (message: string) => void; loading: boolean }) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
const [message, setMessage] = useState("");
const [textareaRows, setTextareaRows] = useState(1);
const [mode, setMode] = useState<'multi' | 'single'>('single');
const [mode, setMode] = useState<"multi" | "single">("single");
useEffect(() => {
if (textareaRows >= 2 && message && mode === 'single') {
setMode('multi');
} else if (!message && mode === 'multi') {
setMode('single');
if (textareaRows >= 2 && message && mode === "single") {
setMode("multi");
} else if (!message && mode === "multi") {
setMode("single");
}
}, [textareaRows, mode, message]);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === '/') {
if (e.key === "/") {
e.preventDefault();
inputRef.current?.focus();
}
};
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<form
onSubmit={(e) => {
onSubmit={e => {
if (loading) return;
e.preventDefault();
sendMessage(message);
setMessage('');
setMessage("");
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !loading) {
onKeyDown={e => {
if (e.key === "Enter" && !e.shiftKey && !loading) {
e.preventDefault();
sendMessage(message);
setMessage('');
setMessage("");
}
}}
className={cn(
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200',
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
"bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200",
mode === "multi" ? "flex-col rounded-lg" : "flex-row rounded-full",
)}
>
{mode === 'single' && <Attach />}
{mode === "single" && <Attach />}
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onChange={e => setMessage(e.target.value)}
onHeightChange={(height, props) => {
setTextareaRows(Math.ceil(height / props.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"
/>
{mode === 'single' && (
{mode === "single" && (
<div className="flex flex-row items-center space-x-4">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
<CopilotToggle copilotEnabled={copilotEnabled} setCopilotEnabled={setCopilotEnabled} />
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
@ -87,14 +78,11 @@ const MessageInput = ({
</button>
</div>
)}
{mode === 'multi' && (
{mode === "multi" && (
<div className="flex flex-row items-center justify-between w-full pt-2">
<Attach />
<div className="flex flex-row items-center space-x-4">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
<CopilotToggle copilotEnabled={copilotEnabled} setCopilotEnabled={setCopilotEnabled} />
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"

View file

@ -1,4 +1,4 @@
import { CopyPlus } from 'lucide-react';
import { CopyPlus } from "lucide-react";
const Attach = () => {
return (

View file

@ -1,5 +1,5 @@
import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react';
import { cn } from "@/lib/utils";
import { Switch } from "@headlessui/react";
const CopilotToggle = ({
copilotEnabled,
@ -18,20 +18,18 @@ const CopilotToggle = ({
<span className="sr-only">Copilot</span>
<span
className={cn(
copilotEnabled
? 'translate-x-6 bg-[#24A0ED]'
: 'translate-x-1 bg-black/50 dark:bg-white/50',
'inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200',
copilotEnabled ? "translate-x-6 bg-[#24A0ED]" : "translate-x-1 bg-black/50 dark:bg-white/50",
"inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200",
)}
/>
</Switch>
<p
onClick={() => setCopilotEnabled(!copilotEnabled)}
className={cn(
'text-xs font-medium transition-colors duration-150 ease-in-out',
"text-xs font-medium transition-colors duration-150 ease-in-out",
copilotEnabled
? 'text-[#24A0ED]'
: 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white',
? "text-[#24A0ED]"
: "text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white",
)}
>
Copilot

View file

@ -1,86 +1,63 @@
import {
BadgePercent,
ChevronDown,
Globe,
Pencil,
ScanEye,
SwatchBook,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Popover, Transition } from '@headlessui/react';
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
import { Fragment } from 'react';
import { BadgePercent, ChevronDown, Globe, Pencil, ScanEye, SwatchBook } from "lucide-react";
import { cn } from "@/lib/utils";
import { Popover, Transition } from "@headlessui/react";
import { SiReddit, SiYoutube } from "@icons-pack/react-simple-icons";
import { Fragment } from "react";
const focusModes = [
{
key: 'webSearch',
title: 'All',
description: 'Searches across all of the internet',
key: "webSearch",
title: "All",
description: "Searches across all of the internet",
icon: <Globe size={20} />,
},
{
key: 'academicSearch',
title: 'Academic',
description: 'Search in published academic papers',
key: "academicSearch",
title: "Academic",
description: "Search in published academic papers",
icon: <SwatchBook size={20} />,
},
{
key: 'writingAssistant',
title: 'Writing',
description: 'Chat without searching the web',
key: "writingAssistant",
title: "Writing",
description: "Chat without searching the web",
icon: <Pencil size={16} />,
},
{
key: 'wolframAlphaSearch',
title: 'Wolfram Alpha',
description: 'Computational knowledge engine',
key: "wolframAlphaSearch",
title: "Wolfram Alpha",
description: "Computational knowledge engine",
icon: <BadgePercent size={20} />,
},
{
key: 'youtubeSearch',
title: 'Youtube',
description: 'Search and watch videos',
key: "youtubeSearch",
title: "Youtube",
description: "Search and watch videos",
icon: (
<SiYoutube
className="h-5 w-auto mr-0.5"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
<SiYoutube className="h-5 w-auto mr-0.5" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />
),
},
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
key: "redditSearch",
title: "Reddit",
description: "Search for discussions and opinions",
icon: (
<SiReddit
className="h-5 w-auto mr-0.5"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
<SiReddit className="h-5 w-auto mr-0.5" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />
),
},
];
const Focus = ({
focusMode,
setFocusMode,
}: {
focusMode: string;
setFocusMode: (mode: string) => void;
}) => {
const Focus = ({ focusMode, setFocusMode }: { focusMode: string; setFocusMode: (mode: string) => void }) => {
return (
<Popover className="fixed w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<Popover.Button
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" ? (
<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>
{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>
) : (
@ -103,26 +80,22 @@ const Focus = ({
onClick={() => setFocusMode(mode.key)}
key={i}
className={cn(
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
"p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition",
focusMode === mode.key
? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
? "bg-light-secondary dark:bg-dark-secondary"
: "hover:bg-light-secondary dark:hover:bg-dark-secondary",
)}
>
<div
className={cn(
'flex flex-row items-center space-x-1',
focusMode === mode.key
? 'text-[#24A0ED]'
: 'text-black dark:text-white',
"flex flex-row items-center space-x-1",
focusMode === mode.key ? "text-[#24A0ED]" : "text-black dark:text-white",
)}
>
{mode.icon}
<p className="text-sm font-medium">{mode.title}</p>
</div>
<p className="text-black/70 dark:text-white/70 text-xs">
{mode.description}
</p>
<p className="text-black/70 dark:text-white/70 text-xs">{mode.description}</p>
</Popover.Button>
))}
</div>

View file

@ -1,19 +1,19 @@
/* eslint-disable @next/next/no-img-element */
import { Dialog, Transition } from '@headlessui/react';
import { Document } from '@langchain/core/documents';
import { Fragment, useState } from 'react';
import { Dialog, Transition } from "@headlessui/react";
import { Document } from "@langchain/core/documents";
import { Fragment, useState } from "react";
const MessageSources = ({ sources }: { sources: Document[] }) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const closeModal = () => {
setIsDialogOpen(false);
document.body.classList.remove('overflow-hidden-scrollable');
document.body.classList.remove("overflow-hidden-scrollable");
};
const openModal = () => {
setIsDialogOpen(true);
document.body.classList.add('overflow-hidden-scrollable');
document.body.classList.add("overflow-hidden-scrollable");
};
return (
@ -38,7 +38,7 @@ 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.replace(/.+\/\/|www.|\..+/g, "")}
</p>
</div>
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
@ -65,9 +65,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
/>
))}
</div>
<p className="text-xs text-black/50 dark:text-white/50">
View {sources.length - 3} more
</p>
<p className="text-xs text-black/50 dark:text-white/50">View {sources.length - 3} more</p>
</button>
)}
<Transition appear show={isDialogOpen} as={Fragment}>
@ -84,9 +82,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
leaveTo="opacity-0 scale-95"
>
<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>
<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) => (
<a
@ -108,10 +104,7 @@ 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.replace(/.+\/\/|www.|\..+/g, "")}
</p>
</div>
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">

View file

@ -1,23 +1,18 @@
import { Clock, Edit, Share, Trash } from 'lucide-react';
import { Message } from './ChatWindow';
import { useEffect, useState } from 'react';
import { formatTimeDifference } from '@/lib/utils';
import { Clock, Edit, Share, Trash } from "lucide-react";
import { Message } from "./ChatWindow";
import { useEffect, useState } from "react";
import { formatTimeDifference } from "@/lib/utils";
const Navbar = ({ messages }: { messages: Message[] }) => {
const [title, setTitle] = useState<string>('');
const [timeAgo, setTimeAgo] = useState<string>('');
const [title, setTitle] = useState<string>("");
const [timeAgo, setTimeAgo] = useState<string>("");
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.substring(0, 20).trim()}...` : messages[0].content;
setTitle(newTitle);
const newTimeAgo = formatTimeDifference(
new Date(),
messages[0].createdAt,
);
const newTimeAgo = formatTimeDifference(new Date(), messages[0].createdAt);
setTimeAgo(newTimeAgo);
}
}, [messages]);
@ -25,10 +20,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
useEffect(() => {
const intervalId = setInterval(() => {
if (messages.length > 0) {
const newTimeAgo = formatTimeDifference(
new Date(),
messages[0].createdAt,
);
const newTimeAgo = formatTimeDifference(new Date(), messages[0].createdAt);
setTimeAgo(newTimeAgo);
}
}, 1000);
@ -39,10 +31,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
return (
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
<Edit
size={17}
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
/>
<Edit size={17} className="active:scale-95 transition duration-100 cursor-pointer lg:hidden" />
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
<Clock size={17} />
<p className="text-xs">{timeAgo} ago</p>
@ -50,14 +39,8 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
<p className="hidden lg:flex">{title}</p>
<div className="flex flex-row items-center space-x-4">
<Share
size={17}
className="active:scale-95 transition duration-100 cursor-pointer"
/>
<Trash
size={17}
className="text-red-400 active:scale-95 transition duration-100 cursor-pointer"
/>
<Share size={17} className="active:scale-95 transition duration-100 cursor-pointer" />
<Trash size={17} className="text-red-400 active:scale-95 transition duration-100 cursor-pointer" />
</div>
</div>
);

View file

@ -1,9 +1,9 @@
/* eslint-disable @next/next/no-img-element */
import { ImagesIcon, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
import { ImagesIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import { Message } from "./ChatWindow";
type Image = {
url: string;
@ -11,13 +11,7 @@ type Image = {
title: string;
};
const SearchImages = ({
query,
chat_history,
}: {
query: string;
chat_history: Message[];
}) => {
const SearchImages = ({ query, chat_history }: { query: string; chat_history: Message[] }) => {
const [images, setImages] = useState<Image[] | null>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
@ -30,24 +24,21 @@ const SearchImages = ({
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem("chatModelProvider");
const chatModel = localStorage.getItem("chatModel");
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
}),
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/images`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
);
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
}),
});
const data = await res.json();
@ -89,11 +80,7 @@ const SearchImages = ({
<img
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
}}
key={i}
src={image.img_src}
@ -105,11 +92,7 @@ const SearchImages = ({
<img
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
}}
key={i}
src={image.img_src}
@ -132,9 +115,7 @@ const SearchImages = ({
/>
))}
</div>
<p className="text-black/70 dark:text-white/70 text-xs">
View {images.length - 3} more
</p>
<p className="text-black/70 dark:text-white/70 text-xs">View {images.length - 3} more</p>
</button>
)}
</div>

View file

@ -1,9 +1,9 @@
/* 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';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from "lucide-react";
import { useState } from "react";
import Lightbox, { GenericSlide, VideoSlide } from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import { Message } from "./ChatWindow";
type Video = {
url: string;
@ -12,25 +12,19 @@ type Video = {
iframe_src: string;
};
declare module 'yet-another-react-lightbox' {
declare module "yet-another-react-lightbox" {
export interface VideoSlide extends GenericSlide {
type: 'video-slide';
type: "video-slide";
src: string;
iframe_src: string;
}
interface SlideTypes {
'video-slide': VideoSlide;
"video-slide": VideoSlide;
}
}
const Searchvideos = ({
query,
chat_history,
}: {
query: string;
chat_history: Message[];
}) => {
const Searchvideos = ({ query, chat_history }: { query: string; chat_history: Message[] }) => {
const [videos, setVideos] = useState<Video[] | null>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
@ -43,24 +37,21 @@ const Searchvideos = ({
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem("chatModelProvider");
const chatModel = localStorage.getItem("chatModel");
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
}),
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/videos`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
);
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
}),
});
const data = await res.json();
@ -69,7 +60,7 @@ const Searchvideos = ({
setSlides(
videos.map((video: Video) => {
return {
type: 'video-slide',
type: "video-slide",
iframe_src: video.iframe_src,
src: video.img_src,
};
@ -104,11 +95,7 @@ const Searchvideos = ({
<div
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
}}
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
key={i}
@ -128,11 +115,7 @@ const Searchvideos = ({
<div
onClick={() => {
setOpen(true);
setSlides([
slides[i],
...slides.slice(0, i),
...slides.slice(i + 1),
]);
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
}}
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
key={i}
@ -163,9 +146,7 @@ const Searchvideos = ({
/>
))}
</div>
<p className="text-black/70 dark:text-white/70 text-xs">
View {videos.length - 3} more
</p>
<p className="text-black/70 dark:text-white/70 text-xs">View {videos.length - 3} more</p>
</button>
)}
</div>
@ -175,7 +156,7 @@ const Searchvideos = ({
slides={slides}
render={{
slide: ({ slide }) =>
slide.type === 'video-slide' ? (
slide.type === "video-slide" ? (
<div className="h-full w-full flex flex-row items-center justify-center">
<iframe
src={slide.iframe_src}

View file

@ -1,13 +1,8 @@
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';
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> {}
@ -16,7 +11,7 @@ const Input = ({ className, ...restProps }: InputProps) => {
<input
{...restProps}
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',
"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,
)}
/>
@ -32,7 +27,7 @@ export const Select = ({ className, options, ...restProps }: SelectProps) => {
<select
{...restProps}
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',
"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,
)}
>
@ -59,27 +54,14 @@ interface SettingsType {
ollamaApiUrl: string;
}
const SettingsDialog = ({
isOpen,
setIsOpen,
}: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}) => {
const SettingsDialog = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (isOpen: boolean) => void }) => {
const [config, setConfig] = useState<SettingsType | null>(null);
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<
string | null
>(null);
const [selectedChatModel, setSelectedChatModel] = useState<string | null>(
null,
);
const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] =
useState<string | null>(null);
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<
string | null
>(null);
const [customOpenAIApiKey, setCustomOpenAIApiKey] = useState<string>('');
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>('');
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<string | null>(null);
const [selectedChatModel, setSelectedChatModel] = useState<string | null>(null);
const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = useState<string | null>(null);
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<string | null>(null);
const [customOpenAIApiKey, setCustomOpenAIApiKey] = useState<string>("");
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
@ -89,52 +71,38 @@ const SettingsDialog = ({
setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
const data = (await res.json()) as SettingsType;
setConfig(data);
const chatModelProvidersKeys = Object.keys(
data.chatModelProviders || {},
);
const embeddingModelProvidersKeys = Object.keys(
data.embeddingModelProviders || {},
);
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
const embeddingModelProvidersKeys = Object.keys(data.embeddingModelProviders || {});
const defaultChatModelProvider =
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
const defaultChatModelProvider = chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : "";
const defaultEmbeddingModelProvider =
embeddingModelProvidersKeys.length > 0
? embeddingModelProvidersKeys[0]
: '';
embeddingModelProvidersKeys.length > 0 ? embeddingModelProvidersKeys[0] : "";
const chatModelProvider =
localStorage.getItem('chatModelProvider') ||
defaultChatModelProvider ||
'';
const chatModelProvider = localStorage.getItem("chatModelProvider") || defaultChatModelProvider || "";
const chatModel =
localStorage.getItem('chatModel') ||
(data.chatModelProviders &&
data.chatModelProviders[chatModelProvider]?.[0]) ||
'';
localStorage.getItem("chatModel") ||
(data.chatModelProviders && data.chatModelProviders[chatModelProvider]?.[0]) ||
"";
const embeddingModelProvider =
localStorage.getItem('embeddingModelProvider') ||
defaultEmbeddingModelProvider ||
'';
localStorage.getItem("embeddingModelProvider") || defaultEmbeddingModelProvider || "";
const embeddingModel =
localStorage.getItem('embeddingModel') ||
(data.embeddingModelProviders &&
data.embeddingModelProviders[embeddingModelProvider]?.[0]) ||
'';
localStorage.getItem("embeddingModel") ||
(data.embeddingModelProviders && data.embeddingModelProviders[embeddingModelProvider]?.[0]) ||
"";
setSelectedChatModelProvider(chatModelProvider);
setSelectedChatModel(chatModel);
setSelectedEmbeddingModelProvider(embeddingModelProvider);
setSelectedEmbeddingModel(embeddingModel);
setCustomOpenAIApiKey(localStorage.getItem('openAIApiKey') || '');
setCustomOpenAIBaseURL(localStorage.getItem('openAIBaseURL') || '');
setCustomOpenAIApiKey(localStorage.getItem("openAIApiKey") || "");
setCustomOpenAIBaseURL(localStorage.getItem("openAIBaseURL") || "");
setIsLoading(false);
};
@ -148,22 +116,19 @@ const SettingsDialog = ({
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
localStorage.setItem('chatModelProvider', selectedChatModelProvider!);
localStorage.setItem('chatModel', selectedChatModel!);
localStorage.setItem(
'embeddingModelProvider',
selectedEmbeddingModelProvider!,
);
localStorage.setItem('embeddingModel', selectedEmbeddingModel!);
localStorage.setItem('openAIApiKey', customOpenAIApiKey!);
localStorage.setItem('openAIBaseURL', customOpenAIBaseURL!);
localStorage.setItem("chatModelProvider", selectedChatModelProvider!);
localStorage.setItem("chatModel", selectedChatModel!);
localStorage.setItem("embeddingModelProvider", selectedEmbeddingModelProvider!);
localStorage.setItem("embeddingModel", selectedEmbeddingModel!);
localStorage.setItem("openAIApiKey", customOpenAIApiKey!);
localStorage.setItem("openAIBaseURL", customOpenAIBaseURL!);
} catch (err) {
console.log(err);
} finally {
@ -176,11 +141,7 @@ const SettingsDialog = ({
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50"
onClose={() => setIsOpen(false)}
>
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@ -204,147 +165,105 @@ const SettingsDialog = ({
leaveTo="opacity-0 scale-95"
>
<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-xl font-medium leading-6 dark:text-white">
Settings
</Dialog.Title>
<Dialog.Title className="text-xl font-medium leading-6 dark:text-white">Settings</Dialog.Title>
{config && !isLoading && (
<div className="flex flex-col space-y-4 mt-6">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Theme
</p>
<p className="text-black/70 dark:text-white/70 text-sm">Theme</p>
<ThemeSwitcher />
</div>
{config.chatModelProviders && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat model Provider
</p>
<p className="text-black/70 dark:text-white/70 text-sm">Chat model Provider</p>
<Select
value={selectedChatModelProvider ?? undefined}
onChange={(e) => {
onChange={e => {
setSelectedChatModelProvider(e.target.value);
setSelectedChatModel(
config.chatModelProviders[e.target.value][0],
);
setSelectedChatModel(config.chatModelProviders[e.target.value][0]);
}}
options={Object.keys(config.chatModelProviders).map(
(provider) => ({
value: provider,
label:
provider.charAt(0).toUpperCase() +
provider.slice(1),
}),
)}
options={Object.keys(config.chatModelProviders).map(provider => ({
value: provider,
label: provider.charAt(0).toUpperCase() + provider.slice(1),
}))}
/>
</div>
)}
{selectedChatModelProvider &&
selectedChatModelProvider != 'custom_openai' && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Model
</p>
<Select
value={selectedChatModel ?? undefined}
onChange={(e) =>
setSelectedChatModel(e.target.value)
}
options={(() => {
const chatModelProvider =
config.chatModelProviders[
selectedChatModelProvider
];
{selectedChatModelProvider && selectedChatModelProvider != "custom_openai" && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">Chat Model</p>
<Select
value={selectedChatModel ?? undefined}
onChange={e => setSelectedChatModel(e.target.value)}
options={(() => {
const chatModelProvider = config.chatModelProviders[selectedChatModelProvider];
return chatModelProvider
? chatModelProvider.length > 0
? chatModelProvider.map((model) => ({
value: model,
label: model,
}))
: [
{
value: '',
label: 'No models available',
disabled: true,
},
]
return chatModelProvider
? chatModelProvider.length > 0
? chatModelProvider.map(model => ({
value: model,
label: model,
}))
: [
{
value: '',
label:
'Invalid provider, please check backend logs',
value: "",
label: "No models available",
disabled: true,
},
];
})()}
]
: [
{
value: "",
label: "Invalid provider, please check backend logs",
disabled: true,
},
];
})()}
/>
</div>
)}
{selectedChatModelProvider && selectedChatModelProvider === "custom_openai" && (
<>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">Model name</p>
<Input
type="text"
placeholder="Model name"
defaultValue={selectedChatModel!}
onChange={e => setSelectedChatModel(e.target.value)}
/>
</div>
)}
{selectedChatModelProvider &&
selectedChatModelProvider === 'custom_openai' && (
<>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Model name
</p>
<Input
type="text"
placeholder="Model name"
defaultValue={selectedChatModel!}
onChange={(e) =>
setSelectedChatModel(e.target.value)
}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Custom OpenAI API Key
</p>
<Input
type="text"
placeholder="Custom OpenAI API Key"
defaultValue={customOpenAIApiKey!}
onChange={(e) =>
setCustomOpenAIApiKey(e.target.value)
}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Custom OpenAI Base URL
</p>
<Input
type="text"
placeholder="Custom OpenAI Base URL"
defaultValue={customOpenAIBaseURL!}
onChange={(e) =>
setCustomOpenAIBaseURL(e.target.value)
}
/>
</div>
</>
)}
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">Custom OpenAI API Key</p>
<Input
type="text"
placeholder="Custom OpenAI API Key"
defaultValue={customOpenAIApiKey!}
onChange={e => setCustomOpenAIApiKey(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">Custom OpenAI Base URL</p>
<Input
type="text"
placeholder="Custom OpenAI Base URL"
defaultValue={customOpenAIBaseURL!}
onChange={e => setCustomOpenAIBaseURL(e.target.value)}
/>
</div>
</>
)}
{/* Embedding models */}
{config.embeddingModelProviders && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Embedding model Provider
</p>
<p className="text-black/70 dark:text-white/70 text-sm">Embedding model Provider</p>
<Select
value={selectedEmbeddingModelProvider ?? undefined}
onChange={(e) => {
onChange={e => {
setSelectedEmbeddingModelProvider(e.target.value);
setSelectedEmbeddingModel(
config.embeddingModelProviders[e.target.value][0],
);
setSelectedEmbeddingModel(config.embeddingModelProviders[e.target.value][0]);
}}
options={Object.keys(
config.embeddingModelProviders,
).map((provider) => ({
label:
provider.charAt(0).toUpperCase() +
provider.slice(1),
options={Object.keys(config.embeddingModelProviders).map(provider => ({
label: provider.charAt(0).toUpperCase() + provider.slice(1),
value: provider,
}))}
/>
@ -352,38 +271,31 @@ const SettingsDialog = ({
)}
{selectedEmbeddingModelProvider && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Embedding Model
</p>
<p className="text-black/70 dark:text-white/70 text-sm">Embedding Model</p>
<Select
value={selectedEmbeddingModel ?? undefined}
onChange={(e) =>
setSelectedEmbeddingModel(e.target.value)
}
onChange={e => setSelectedEmbeddingModel(e.target.value)}
options={(() => {
const embeddingModelProvider =
config.embeddingModelProviders[
selectedEmbeddingModelProvider
];
config.embeddingModelProviders[selectedEmbeddingModelProvider];
return embeddingModelProvider
? embeddingModelProvider.length > 0
? embeddingModelProvider.map((model) => ({
? embeddingModelProvider.map(model => ({
label: model,
value: model,
}))
: [
{
label: 'No embedding models available',
value: '',
label: "No embedding models available",
value: "",
disabled: true,
},
]
: [
{
label:
'Invalid provider, please check backend logs',
value: '',
label: "Invalid provider, please check backend logs",
value: "",
disabled: true,
},
];
@ -392,14 +304,12 @@ const SettingsDialog = ({
</div>
)}
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
OpenAI API Key
</p>
<p className="text-black/70 dark:text-white/70 text-sm">OpenAI API Key</p>
<Input
type="text"
placeholder="OpenAI API Key"
defaultValue={config.openaiApiKey}
onChange={(e) =>
onChange={e =>
setConfig({
...config,
openaiApiKey: e.target.value,
@ -408,14 +318,12 @@ const SettingsDialog = ({
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Ollama API URL
</p>
<p className="text-black/70 dark:text-white/70 text-sm">Ollama API URL</p>
<Input
type="text"
placeholder="Ollama API URL"
defaultValue={config.ollamaApiUrl}
onChange={(e) =>
onChange={e =>
setConfig({
...config,
ollamaApiUrl: e.target.value,
@ -424,14 +332,12 @@ const SettingsDialog = ({
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
GROQ API Key
</p>
<p className="text-black/70 dark:text-white/70 text-sm">GROQ API Key</p>
<Input
type="text"
placeholder="GROQ API Key"
defaultValue={config.groqApiKey}
onChange={(e) =>
onChange={e =>
setConfig({
...config,
groqApiKey: e.target.value,
@ -455,11 +361,7 @@ const SettingsDialog = ({
className="bg-[#24A0ED] flex flex-row items-center space-x-2 text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full px-4 py-2"
disabled={isLoading || isUpdating}
>
{isUpdating ? (
<RefreshCw size={20} className="animate-spin" />
) : (
<CloudUpload size={20} />
)}
{isUpdating ? <RefreshCw size={20} className="animate-spin" /> : <CloudUpload size={20} />}
</button>
</div>
</Dialog.Panel>

View file

@ -1,17 +1,15 @@
'use client';
"use client";
import { cn } from '@/lib/utils';
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useState, type ReactNode } from 'react';
import Layout from './Layout';
import SettingsDialog from './SettingsDialog';
import { cn } from "@/lib/utils";
import { BookOpenText, Home, Search, SquarePen, Settings } from "lucide-react";
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";
import React, { useState, type ReactNode } from "react";
import Layout from "./Layout";
import SettingsDialog from "./SettingsDialog";
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
return (
<div className="flex flex-col items-center gap-y-3 w-full">{children}</div>
);
return <div className="flex flex-col items-center gap-y-3 w-full">{children}</div>;
};
const Sidebar = ({ children }: { children: React.ReactNode }) => {
@ -22,21 +20,21 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
const navLinks = [
{
icon: Home,
href: '/',
active: segments.length === 0 || segments.includes('c'),
label: 'Home',
href: "/",
active: segments.length === 0 || segments.includes("c"),
label: "Home",
},
{
icon: Search,
href: '/discover',
active: segments.includes('discover'),
label: 'Discover',
href: "/discover",
active: segments.includes("discover"),
label: "Discover",
},
{
icon: BookOpenText,
href: '/library',
active: segments.includes('library'),
label: 'Library',
href: "/library",
active: segments.includes("library"),
label: "Library",
},
];
@ -53,10 +51,8 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
key={i}
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',
link.active
? 'text-black dark:text-white'
: 'text-black/70 dark:text-white/70',
"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",
link.active ? "text-black dark:text-white" : "text-black/70 dark:text-white/70",
)}
>
<link.icon />
@ -67,15 +63,9 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
))}
</VerticalIconContainer>
<Settings
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
className="cursor-pointer"
/>
<Settings onClick={() => setIsSettingsOpen(!isSettingsOpen)} className="cursor-pointer" />
<SettingsDialog
isOpen={isSettingsOpen}
setIsOpen={setIsSettingsOpen}
/>
<SettingsDialog isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
</div>
</div>
@ -85,15 +75,11 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
href={link.href}
key={i}
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',
"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",
)}
>
{link.active && (
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />
)}
{link.active && <div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />}
<link.icon />
<p className="text-xs">{link.label}</p>
</Link>

View file

@ -1,11 +1,7 @@
'use client';
import { ThemeProvider } from 'next-themes';
"use client";
import { ThemeProvider } from "next-themes";
const ThemeProviderComponent = ({
children,
}: {
children: React.ReactNode;
}) => {
const ThemeProviderComponent = ({ children }: { children: React.ReactNode }) => {
return (
<ThemeProvider attribute="class" enableSystem={false} defaultTheme="dark">
{children}

View file

@ -1,10 +1,10 @@
'use client';
import { useTheme } from 'next-themes';
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Select } from '../SettingsDialog';
"use client";
import { useTheme } from "next-themes";
import { SunIcon, MoonIcon, MonitorIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Select } from "../SettingsDialog";
type Theme = 'dark' | 'light' | 'system';
type Theme = "dark" | "light" | "system";
const ThemeSwitcher = ({ className }: { className?: string }) => {
const [mounted, setMounted] = useState(false);
@ -22,20 +22,18 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
}, []);
useEffect(() => {
if (isTheme('system')) {
const preferDarkScheme = window.matchMedia(
'(prefers-color-scheme: dark)',
);
if (isTheme("system")) {
const preferDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
const detectThemeChange = (event: MediaQueryListEvent) => {
const theme: Theme = event.matches ? 'dark' : 'light';
const theme: Theme = event.matches ? "dark" : "light";
setTheme(theme);
};
preferDarkScheme.addEventListener('change', detectThemeChange);
preferDarkScheme.addEventListener("change", detectThemeChange);
return () => {
preferDarkScheme.removeEventListener('change', detectThemeChange);
preferDarkScheme.removeEventListener("change", detectThemeChange);
};
}
}, [isTheme, setTheme, theme]);
@ -49,10 +47,10 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
<Select
className={className}
value={theme}
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
onChange={e => handleThemeSwitch(e.target.value as Theme)}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
]}
/>
);

View file

@ -1,13 +1,13 @@
import { Message } from '@/components/ChatWindow';
import { Message } from "@/components/ChatWindow";
export const getSuggestions = async (chatHisory: Message[]) => {
const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem("chatModel");
const chatModelProvider = localStorage.getItem("chatModelProvider");
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/suggestions`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_history: chatHisory,

View file

@ -1,27 +1,20 @@
import clsx, { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
export const formatTimeDifference = (
date1: Date | string,
date2: Date | string,
): string => {
export const formatTimeDifference = (date1: Date | string, date2: Date | string): string => {
date1 = new Date(date1);
date2 = new Date(date2);
const diffInSeconds = Math.floor(
Math.abs(date2.getTime() - date1.getTime()) / 1000,
);
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' : ''}`;
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' : ''}`;
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 / 86400)} day${Math.floor(diffInSeconds / 86400) !== 1 ? "s" : ""}`;
else return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? "s" : ""}`;
};

View file

@ -1,25 +1,25 @@
import type { Config } from 'tailwindcss';
import type { DefaultColors } from 'tailwindcss/types/generated/colors';
import type { Config } from "tailwindcss";
import type { DefaultColors } from "tailwindcss/types/generated/colors";
const themeDark = (colors: DefaultColors) => ({
50: '#0a0a0a',
100: '#111111',
200: '#1c1c1c',
50: "#0a0a0a",
100: "#111111",
200: "#1c1c1c",
});
const themeLight = (colors: DefaultColors) => ({
50: '#fcfcf9',
100: '#f3f3ee',
200: '#e8e8e3',
50: "#fcfcf9",
100: "#f3f3ee",
200: "#e8e8e3",
});
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: 'class',
darkMode: "class",
theme: {
extend: {
borderColor: ({ colors }) => {
@ -47,6 +47,6 @@ const config: Config = {
},
},
},
plugins: [require('@tailwindcss/typography')],
plugins: [require("@tailwindcss/typography")],
};
export default config;

View file

@ -485,6 +485,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@pkgr/core@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@ -1499,6 +1504,19 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
eslint-plugin-prettier@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1"
integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.8.6"
eslint-scope@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
@ -1656,6 +1674,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-fifo@^1.1.0, fast-fifo@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
@ -2687,6 +2710,13 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
@ -3052,6 +3082,14 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
synckit@^0.8.6:
version "0.8.8"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7"
integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==
dependencies:
"@pkgr/core" "^0.1.0"
tslib "^2.6.2"
tar-fs@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
@ -3156,6 +3194,11 @@ ts-node@^10.9.2:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tslib@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"