61 Commits

Author SHA1 Message Date
ItzCrazyKns
874505cd0e feat(package): bump version 2024-11-19 16:32:30 +05:30
ItzCrazyKns
b4a80d8ca0 feat(dockerfile): downgrade node version, closes #473 2024-11-19 14:40:24 +05:30
ItzCrazyKns
c7bab91803 feat(webSearchAgent): prevent excess results 2024-11-19 10:43:50 +05:30
ItzCrazyKns
a58adbfecc Update README.md 2024-11-17 23:01:24 +05:30
ItzCrazyKns
9e746aea5e feat(readme): remove ? from image URL 2024-11-17 23:01:02 +05:30
ItzCrazyKns
5e1331144a feat(readme): update readme cache 2024-11-17 22:59:29 +05:30
ItzCrazyKns
d789c970b1 feat(assets): update screenshot 2024-11-17 22:55:57 +05:30
ItzCrazyKns
e699cb2921 Merge branch 'master' of https://github.com/ItzCrazyKns/Perplexica 2024-11-17 19:49:22 +05:30
ItzCrazyKns
03eed9693b Merge branch 'pr/451' 2024-11-17 19:48:56 +05:30
ItzCrazyKns
011570dd9b Merge pull request #421 from sjiampojamarn/discover-nit
Make Discover link to a new tab
2024-11-17 19:40:05 +05:30
ItzCrazyKns
18529391f4 Merge branch 'master' of https://github.com/ItzCrazyKns/Perplexica 2024-11-14 13:35:15 +05:30
ItzCrazyKns
a1a7470ca6 feat(package): update markdown-to-jsx 2024-11-14 13:35:10 +05:30
ItzCrazyKns
10c5ac1076 Merge pull request #448 from bastipnt/master
add db setup to CONTRIBUTING.md
2024-11-09 20:54:14 +05:30
Sharun
7c01d2656e fix(EmptyChatMessageInput): focus on mount 2024-11-04 22:00:08 -06:00
litc0de
afb4786ac0 add db setup to CONTRIBUTING.md 2024-11-03 10:33:01 +01:00
ItzCrazyKns
1e99fe8d69 feat(package): bump version 2024-10-31 11:08:49 +05:30
ItzCrazyKns
012dfa5a74 feat(listLineOutputParser): handle unclosed tags 2024-10-30 10:29:21 +05:30
ItzCrazyKns
65d057a05e feat(suggestions): handle custom OpenAI 2024-10-30 10:29:06 +05:30
ItzCrazyKns
3e7645614f feat(image-search): handle custom OpenAI 2024-10-30 10:28:40 +05:30
ItzCrazyKns
7c6ee2ead1 feat(video-search): handle custom OpenAI 2024-10-30 10:28:31 +05:30
ItzCrazyKns
540f38ae68 feat(empty-chat): add settings for mobile 2024-10-30 09:14:09 +05:30
ItzCrazyKns
f1c0b5435b feat(delete-chat): use window.location to refresh page 2024-10-30 09:11:48 +05:30
ItzCrazyKns
b33e5fefba feat(navbar): remove comments 2024-10-29 20:00:31 +05:30
ItzCrazyKns
03d0ff2ca4 feat(navbar): make delete & plus button work 2024-10-29 19:59:58 +05:30
sjiampojamarn
687cbb365f Discover link to new page 2024-10-20 17:23:43 -07:00
ItzCrazyKns
dfb532e4d3 feat(package): bump version 2024-10-18 18:45:23 +05:30
ItzCrazyKns
c8cd959496 feat(dockerfile): update backend image 2024-10-18 17:29:26 +05:30
ItzCrazyKns
4576d3de13 feat(dockerfile): update docker image 2024-10-18 17:26:02 +05:30
ItzCrazyKns
8057f28b20 feat(settings): handle no models 2024-10-18 17:07:09 +05:30
ItzCrazyKns
36bb265e1f feat(dockerfile): revert base image 2024-10-18 12:27:56 +05:30
ItzCrazyKns
71fc19f525 feat(dockerfile): update registry 2024-10-18 12:24:55 +05:30
ItzCrazyKns
c7c0ebe5b6 feat(dockerfile): use NPM registry 2024-10-18 12:15:04 +05:30
ItzCrazyKns
8fe1b7c5e3 feat(webSearchAgent): revert prompt 2024-10-18 12:01:56 +05:30
ItzCrazyKns
6e0d3baef6 feat(dockerfile): update docker image 2024-10-18 11:50:56 +05:30
ItzCrazyKns
54e0bb317a feat(groq): update deprecated models 2024-10-18 11:05:57 +05:30
ItzCrazyKns
3e6e57dab0 feat(chat-window): fix rewrite, use messageID 2024-10-17 18:51:11 +05:30
ItzCrazyKns
5aad2febda feat(messageHandler): fix duplicate messageIDs 2024-10-17 18:50:43 +05:30
ItzCrazyKns
24e1919c5e feat(dockerfile): update image to prevent python errors 2024-10-17 10:46:18 +05:30
ItzCrazyKns
c7abd96b05 feat(readme): add networking 2024-10-17 10:01:00 +05:30
ItzCrazyKns
3a01eebc04 feat(chat): prevent ws not open errors 2024-10-15 18:04:50 +05:30
ItzCrazyKns
7532c436db feat(package): bump version 2024-10-15 16:23:13 +05:30
ItzCrazyKns
b9509a5d41 feat(app): lint & beautify 2024-10-15 16:21:29 +05:30
ItzCrazyKns
9db847c366 feat(library): enhance UI 2024-10-15 16:21:15 +05:30
ItzCrazyKns
19bf71cefc feat(chat-window): only send init msg if ready 2024-10-15 16:21:00 +05:30
ItzCrazyKns
61c0347ef2 feat(app): add discover 2024-10-15 16:20:45 +05:30
ItzCrazyKns
0a7167eb04 feat(search-api): add optimizationMode 2024-10-11 10:54:08 +05:30
ItzCrazyKns
7cce853618 feat(providers): add optimization modes 2024-10-11 10:35:59 +05:30
ItzCrazyKns
877735b852 feat(package): update headlessui 2024-10-11 10:35:33 +05:30
ItzCrazyKns
1680a1786e feat(image-build): improve build time by caching 2024-10-03 10:41:05 +05:30
ItzCrazyKns
66f1e19ce8 feat(image-build): use Docker buildx, publish multi arch images 2024-10-03 09:37:15 +05:30
ItzCrazyKns
ae3fc5f802 feat(docs): modify updating docs 2024-10-02 22:54:16 +05:30
ItzCrazyKns
9f88d16ef1 feat(docker-compose): use env vars from compose 2024-10-02 22:54:00 +05:30
ItzCrazyKns
c233362e70 feat(dockerfile): specify default args 2024-10-02 22:53:45 +05:30
ItzCrazyKns
1aaf172246 feat(build-workflow): update head 2024-10-02 22:01:49 +05:30
ItzCrazyKns
4bba674134 feat(build-workflow): update branch 2024-10-02 22:00:46 +05:30
ItzCrazyKns
dcfe43ebda trigger build 2024-10-02 22:00:04 +05:30
ItzCrazyKns
fc5e35b1b1 feat(docker): add prebuilt images 2024-10-02 21:59:40 +05:30
ItzCrazyKns
425a08432b feat(groq): add Llama 3.2 2024-09-26 21:37:05 +05:30
ItzCrazyKns
e3488366c1 Update SEARCH.md 2024-09-25 17:56:19 +05:30
ItzCrazyKns
8902abdcee Update SEARCH.md 2024-09-25 17:54:35 +05:30
ItzCrazyKns
15203c123d feat(docs): update search docs 2024-09-25 17:49:16 +05:30
44 changed files with 1173 additions and 278 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 641 KiB

73
.github/workflows/docker-build.yaml vendored Normal file
View File

@ -0,0 +1,73 @@
name: Build & Push Docker Images
on:
push:
branches:
- master
release:
types: [published]
jobs:
build-and-push:
runs-on: ubuntu-latest
strategy:
matrix:
service: [backend, app]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Log in to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract version from release tag
if: github.event_name == 'release'
id: version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image for ${{ matrix.service }}
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: |
docker buildx create --use
if [[ "${{ matrix.service }}" == "backend" ]]; then \
DOCKERFILE=backend.dockerfile; \
IMAGE_NAME=perplexica-backend; \
else \
DOCKERFILE=app.dockerfile; \
IMAGE_NAME=perplexica-frontend; \
fi
docker buildx build --platform linux/amd64,linux/arm64 \
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:main \
--cache-to=type=inline \
-f $DOCKERFILE \
-t itzcrazykns1337/${IMAGE_NAME}:main \
--push .
- name: Build and push release Docker image for ${{ matrix.service }}
if: github.event_name == 'release'
run: |
docker buildx create --use
if [[ "${{ matrix.service }}" == "backend" ]]; then \
DOCKERFILE=backend.dockerfile; \
IMAGE_NAME=perplexica-backend; \
else \
DOCKERFILE=app.dockerfile; \
IMAGE_NAME=perplexica-frontend; \
fi
docker buildx build --platform linux/amd64,linux/arm64 \
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \
--cache-to=type=inline \
-f $DOCKERFILE \
-t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \
--push .

3
.gitignore vendored
View File

@ -35,4 +35,5 @@ logs/
Thumbs.db
# Db
db.sqlite
db.sqlite
/searxng

View File

@ -18,7 +18,8 @@ Before diving into coding, setting up your local environment is key. Here's what
1. In the root directory, locate the `sample.config.toml` file.
2. Rename it to `config.toml` and fill in the necessary configuration fields specific to the backend.
3. Run `npm install` to install dependencies.
4. Use `npm run dev` to start the backend in development mode.
4. Run `npm run db:push` to set up the local sqlite.
5. Use `npm run dev` to start the backend in development mode.
### Frontend

View File

@ -1,6 +1,6 @@
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
![preview](.assets/perplexica-screenshot.png)
![preview](.assets/perplexica-screenshot.png?)
## Table of Contents <!-- omit in toc -->
@ -13,6 +13,7 @@
- [Ollama Connection Errors](#ollama-connection-errors)
- [Using as a Search Engine](#using-as-a-search-engine)
- [Using Perplexica's API](#using-perplexicas-api)
- [Expose Perplexica to a network](#expose-perplexica-to-network)
- [One-Click Deployment](#one-click-deployment)
- [Upcoming Features](#upcoming-features)
- [Support Us](#support-us)
@ -133,6 +134,10 @@ Perplexica also provides an API for developers looking to integrate its powerful
For more details, check out the full documentation [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/API/SEARCH.md).
## Expose Perplexica to network
You can access Perplexica over your home network by following our networking guide [here](https://github.com/ItzCrazyKns/Perplexica/blob/master/docs/installation/NETWORKING.md).
## One-Click Deployment
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
@ -144,8 +149,8 @@ For more details, check out the full documentation [here](https://github.com/Itz
- [x] History Saving features
- [x] Introducing various Focus Modes
- [x] Adding API support
- [x] Adding Discover
- [ ] Finalizing Copilot Mode
- [ ] Adding Discover
## Support Us

View File

@ -1,7 +1,7 @@
FROM node:alpine
FROM node:20.18.0-alpine
ARG NEXT_PUBLIC_WS_URL
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
ARG NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
@ -9,7 +9,7 @@ WORKDIR /home/perplexica
COPY ui /home/perplexica/
RUN yarn install
RUN yarn install --frozen-lockfile
RUN yarn build
CMD ["yarn", "start"]

View File

@ -1,20 +1,16 @@
FROM node:slim
ARG SEARXNG_API_URL
ENV SEARXNG_API_URL=${SEARXNG_API_URL}
FROM node:18-slim
WORKDIR /home/perplexica
COPY src /home/perplexica/src
COPY tsconfig.json /home/perplexica/
COPY config.toml /home/perplexica/
COPY drizzle.config.ts /home/perplexica/
COPY package.json /home/perplexica/
COPY yarn.lock /home/perplexica/
RUN mkdir /home/perplexica/data
RUN yarn install
RUN yarn install --frozen-lockfile --network-timeout 600000
RUN yarn build
CMD ["yarn", "start"]

View File

@ -13,8 +13,9 @@ services:
build:
context: .
dockerfile: backend.dockerfile
args:
- SEARXNG_API_URL=http://searxng:8080
image: itzcrazykns1337/perplexica-backend:main
environment:
- SEARXNG_API_URL=http://searxng:8080
depends_on:
- searxng
ports:
@ -35,6 +36,7 @@ services:
args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
image: itzcrazykns1337/perplexica-frontend:main
depends_on:
- perplexica-backend
ports:

View File

@ -6,7 +6,9 @@ Perplexicas Search API makes it easy to use our AI-powered search engine. You
## Endpoint
### **POST** `/api/search`
### **POST** `http://localhost:3001/api/search`
**Note**: Replace `3001` with any other port if you've changed the default PORT
### Request
@ -24,15 +26,19 @@ The API accepts a JSON object in the request body, where you define the focus mo
"provider": "openai",
"model": "text-embedding-3-large"
},
"optimizationMode": "speed",
"focusMode": "webSearch",
"query": "What is Perplexica",
"history": []
"history": [
["human", "Hi, how are you?"],
["assistant", "I am doing well, how can I help you today?"]
]
}
```
### Request Parameters
- **`chatModel`** (object, optional): Defines the chat model to be used for the query.
- **`chatModel`** (object, optional): Defines the chat model to be used for the query. For model details you can send a GET request at `http://localhost:3001/api/models`. Make sure to use the key value (For example "gpt-4o-mini" instead of the display name "GPT 4 omni mini").
- `provider`: Specifies the provider for the chat model (e.g., `openai`, `ollama`).
- `model`: The specific model from the chosen provider (e.g., `gpt-4o-mini`).
@ -40,7 +46,7 @@ The API accepts a JSON object in the request body, where you define the focus mo
- `customOpenAIBaseURL`: If youre using a custom OpenAI instance, provide the base URL.
- `customOpenAIKey`: The API key for a custom OpenAI instance.
- **`embeddingModel`** (object, optional): Defines the embedding model for similarity-based searching.
- **`embeddingModel`** (object, optional): Defines the embedding model for similarity-based searching. For model details you can send a GET request at `http://localhost:3001/api/models`. Make sure to use the key value (For example "text-embedding-3-large" instead of the display name "Text Embedding 3 Large").
- `provider`: The provider for the embedding model (e.g., `openai`).
- `model`: The specific embedding model (e.g., `text-embedding-3-large`).
@ -49,9 +55,15 @@ The API accepts a JSON object in the request body, where you define the focus mo
- `webSearch`, `academicSearch`, `writingAssistant`, `wolframAlphaSearch`, `youtubeSearch`, `redditSearch`.
- **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes:
- `speed`: Prioritize speed and return the fastest answer.
- `balanced`: Provide a balanced answer with good speed and reasonable quality.
- **`query`** (string, required): The search query or question.
- **`history`** (array, optional): An array of message pairs representing the conversation history. Each pair consists of a role (either 'human' or 'assistant') and the message content. This allows the system to use the context of the conversation to refine results. Example:
```json
[
["human", "What is Perplexica?"],

View File

@ -10,15 +10,21 @@ To update Perplexica to the latest version, follow these steps:
git clone https://github.com/ItzCrazyKns/Perplexica.git
```
2. Navigate to the Project Directory
2. Navigate to the Project Directory.
3. Update and Rebuild Docker Containers:
3. Pull latest images from registry.
```bash
docker compose up -d --build
docker compose pull
```
4. Once the command completes running go to http://localhost:3000 and verify the latest changes.
4. Update and Recreate containers.
```bash
docker compose up -d
```
5. Once the command completes running go to http://localhost:3000 and verify the latest changes.
## For non Docker users

View File

@ -1,6 +1,6 @@
{
"name": "perplexica-backend",
"version": "1.9.0-rc3",
"version": "1.9.3",
"license": "MIT",
"author": "ItzCrazyKns",
"scripts": {
@ -18,6 +18,7 @@
"@types/html-to-text": "^9.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/readable-stream": "^4.0.11",
"@types/ws": "^8.5.12",
"drizzle-kit": "^0.22.7",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",

View File

@ -115,12 +115,7 @@ const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => {
const res = await searchSearxng(input, {
language: 'en',
engines: [
'arxiv',
'google scholar',
'internetarchivescholar',
'pubmed',
],
engines: ['arxiv', 'google scholar', 'pubmed'],
});
const documents = res.results.map(
@ -143,6 +138,7 @@ const createBasicAcademicSearchRetrieverChain = (llm: BaseChatModel) => {
const createBasicAcademicSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const basicAcademicSearchRetrieverChain =
createBasicAcademicSearchRetrieverChain(llm);
@ -168,26 +164,32 @@ const createBasicAcademicSearchAnsweringChain = (
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
if (optimizationMode === 'speed') {
return docsWithContent.slice(0, 15);
} else if (optimizationMode === 'balanced') {
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
return sortedDocs;
}
};
return RunnableSequence.from([
@ -224,12 +226,17 @@ const basicAcademicSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = new eventEmitter();
try {
const basicAcademicSearchAnsweringChain =
createBasicAcademicSearchAnsweringChain(llm, embeddings);
createBasicAcademicSearchAnsweringChain(
llm,
embeddings,
optimizationMode,
);
const stream = basicAcademicSearchAnsweringChain.streamEvents(
{
@ -258,8 +265,15 @@ const handleAcademicSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = basicAcademicSearch(message, history, llm, embeddings);
const emitter = basicAcademicSearch(
message,
history,
llm,
embeddings,
optimizationMode,
);
return emitter;
};

View File

@ -138,6 +138,7 @@ const createBasicRedditSearchRetrieverChain = (llm: BaseChatModel) => {
const createBasicRedditSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const basicRedditSearchRetrieverChain =
createBasicRedditSearchRetrieverChain(llm);
@ -163,27 +164,33 @@ const createBasicRedditSearchAnsweringChain = (
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
if (optimizationMode === 'speed') {
return docsWithContent.slice(0, 15);
} else if (optimizationMode === 'balanced') {
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.filter((sim) => sim.similarity > 0.3)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
const sortedDocs = similarity
.filter((sim) => sim.similarity > 0.3)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
return sortedDocs;
}
};
return RunnableSequence.from([
@ -220,12 +227,13 @@ const basicRedditSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = new eventEmitter();
try {
const basicRedditSearchAnsweringChain =
createBasicRedditSearchAnsweringChain(llm, embeddings);
createBasicRedditSearchAnsweringChain(llm, embeddings, optimizationMode);
const stream = basicRedditSearchAnsweringChain.streamEvents(
{
chat_history: history,
@ -253,8 +261,15 @@ const handleRedditSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = basicRedditSearch(message, history, llm, embeddings);
const emitter = basicRedditSearch(
message,
history,
llm,
embeddings,
optimizationMode,
);
return emitter;
};

View File

@ -216,12 +216,54 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
await Promise.all(
docGroups.map(async (doc) => {
const res = await llm.invoke(`
You are a text summarizer. You need to summarize the text provided inside the \`text\` XML block.
You need to summarize the text into 1 or 2 sentences capturing the main idea of the text.
You need to make sure that you don't miss any point while summarizing the text.
You will also be given a \`query\` XML block which will contain the query of the user. Try to answer the query in the summary from the text provided.
If the query says Summarize then you just need to summarize the text without answering the query.
Only return the summarized text without any other messages, text or XML block.
You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
- **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
- **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
- **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
The text will be shared inside the \`text\` XML tag, and the query inside the \`query\` XML tag.
<example>
1. \`<text>
Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.
It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications
by using containers.
</text>
<query>
What is Docker and how does it work?
</query>
Response:
Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application
deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in
any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed.
\`
2. \`<text>
The theory of relativity, or simply relativity, encompasses two interrelated theories of Albert Einstein: special relativity and general
relativity. However, the word "relativity" is sometimes used in reference to Galilean invariance. The term "theory of relativity" was based
on the expression "relative theory" used by Max Planck in 1906. The theory of relativity usually encompasses two interrelated theories by
Albert Einstein: special relativity and general relativity. Special relativity applies to all physical phenomena in the absence of gravity.
General relativity explains the law of gravitation and its relation to other forces of nature. It applies to the cosmological and astrophysical
realm, including astronomy.
</text>
<query>
summarize
</query>
Response:
The theory of relativity, developed by Albert Einstein, encompasses two main theories: special relativity and general relativity. Special
relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravitation and its
relation to other forces of nature. The theory of relativity is based on the concept of "relative theory," as introduced by Max Planck in
1906. It is a fundamental theory in physics that has revolutionized our understanding of the universe.
\`
</example>
Everything below is the actual data you will be working with. Good luck!
<query>
${question}
@ -273,6 +315,7 @@ const createBasicWebSearchRetrieverChain = (llm: BaseChatModel) => {
const createBasicWebSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const basicWebSearchRetrieverChain = createBasicWebSearchRetrieverChain(llm);
@ -294,34 +337,40 @@ const createBasicWebSearchAnsweringChain = (
}
if (query.toLocaleLowerCase() === 'summarize') {
return docs;
return docs.slice(0, 15)
}
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.embedQuery(query),
]);
if (optimizationMode === 'speed') {
return docsWithContent.slice(0, 15);
} else if (optimizationMode === 'balanced') {
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.filter((sim) => sim.similarity > 0.3)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
const sortedDocs = similarity
.filter((sim) => sim.similarity > 0.3)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
return sortedDocs;
}
};
return RunnableSequence.from([
@ -358,6 +407,7 @@ const basicWebSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = new eventEmitter();
@ -365,6 +415,7 @@ const basicWebSearch = (
const basicWebSearchAnsweringChain = createBasicWebSearchAnsweringChain(
llm,
embeddings,
optimizationMode,
);
const stream = basicWebSearchAnsweringChain.streamEvents(
@ -394,8 +445,15 @@ const handleWebSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = basicWebSearch(message, history, llm, embeddings);
const emitter = basicWebSearch(
message,
history,
llm,
embeddings,
optimizationMode,
);
return emitter;
};

View File

@ -138,6 +138,7 @@ const createBasicYoutubeSearchRetrieverChain = (llm: BaseChatModel) => {
const createBasicYoutubeSearchAnsweringChain = (
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const basicYoutubeSearchRetrieverChain =
createBasicYoutubeSearchRetrieverChain(llm);
@ -163,27 +164,33 @@ const createBasicYoutubeSearchAnsweringChain = (
(doc) => doc.pageContent && doc.pageContent.length > 0,
);
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embeddings.embedQuery(query),
]);
if (optimizationMode === 'speed') {
return docsWithContent.slice(0, 15);
} else {
const [docEmbeddings, queryEmbedding] = await Promise.all([
embeddings.embedDocuments(
docsWithContent.map((doc) => doc.pageContent),
),
embeddings.embedQuery(query),
]);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
const similarity = docEmbeddings.map((docEmbedding, i) => {
const sim = computeSimilarity(queryEmbedding, docEmbedding);
return {
index: i,
similarity: sim,
};
});
return {
index: i,
similarity: sim,
};
});
const sortedDocs = similarity
.filter((sim) => sim.similarity > 0.3)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
const sortedDocs = similarity
.filter((sim) => sim.similarity > 0.3)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index]);
return sortedDocs;
return sortedDocs;
}
};
return RunnableSequence.from([
@ -220,12 +227,13 @@ const basicYoutubeSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = new eventEmitter();
try {
const basicYoutubeSearchAnsweringChain =
createBasicYoutubeSearchAnsweringChain(llm, embeddings);
createBasicYoutubeSearchAnsweringChain(llm, embeddings, optimizationMode);
const stream = basicYoutubeSearchAnsweringChain.streamEvents(
{
@ -254,8 +262,15 @@ const handleYoutubeSearch = (
history: BaseMessage[],
llm: BaseChatModel,
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
) => {
const emitter = basicYoutubeSearch(message, history, llm, embeddings);
const emitter = basicYoutubeSearch(
message,
history,
llm,
embeddings,
optimizationMode,
);
return emitter;
};

View File

@ -23,7 +23,7 @@ class LineListOutputParser extends BaseOutputParser<string[]> {
const startKeyIndex = text.indexOf(`<${this.key}>`);
const endKeyIndex = text.indexOf(`</${this.key}>`);
if (startKeyIndex === -1 || endKeyIndex === -1) {
if (startKeyIndex === -1 && endKeyIndex === -1) {
return [];
}

View File

@ -9,6 +9,45 @@ export const loadGroqChatModels = async () => {
try {
const chatModels = {
'llama-3.2-3b-preview': {
displayName: 'Llama 3.2 3B',
model: new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama-3.2-3b-preview',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
},
'llama-3.2-11b-vision-preview': {
displayName: 'Llama 3.2 11B Vision',
model: new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama-3.2-11b-vision-preview',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
},
'llama-3.2-90b-vision-preview': {
displayName: 'Llama 3.2 90B Vision',
model: new ChatOpenAI(
{
openAIApiKey: groqApiKey,
modelName: 'llama-3.2-90b-vision-preview',
temperature: 0.7,
},
{
baseURL: 'https://api.groq.com/openai/v1',
},
),
},
'llama-3.1-70b-versatile': {
displayName: 'Llama 3.1 70B',
model: new ChatOpenAI(

48
src/routes/discover.ts Normal file
View File

@ -0,0 +1,48 @@
import express from 'express';
import { searchSearxng } from '../lib/searxng';
import logger from '../utils/logger';
const router = express.Router();
router.get('/', async (req, res) => {
try {
const data = (
await Promise.all([
searchSearxng('site:businessinsider.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:www.exchangewire.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:yahoo.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:businessinsider.com tech', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:www.exchangewire.com tech', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:yahoo.com tech', {
engines: ['bing news'],
pageno: 1,
}),
])
)
.map((result) => result.results)
.flat()
.sort(() => Math.random() - 0.5);
return res.json({ blogs: data });
} catch (err: any) {
logger.error(`Error in discover route: ${err.message}`);
return res.status(500).json({ message: 'An error has occurred' });
}
});
export default router;

View File

@ -4,14 +4,28 @@ 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 { ChatOpenAI } from '@langchain/openai';
const router = express.Router();
interface ChatModel {
provider: string;
model: string;
customOpenAIBaseURL?: string;
customOpenAIKey?: string;
}
interface ImageSearchBody {
query: string;
chatHistory: any[];
chatModel?: ChatModel;
}
router.post('/', async (req, res) => {
try {
let { query, chat_history, chat_model_provider, chat_model } = req.body;
let body: ImageSearchBody = req.body;
chat_history = chat_history.map((msg: any) => {
const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
@ -19,22 +33,50 @@ router.post('/', async (req, res) => {
}
});
const chatModels = await getAvailableChatModelProviders();
const provider = chat_model_provider ?? Object.keys(chatModels)[0];
const chatModel = chat_model ?? Object.keys(chatModels[provider])[0];
const chatModelProviders = await getAvailableChatModelProviders();
const chatModelProvider =
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
const chatModel =
body.chatModel?.model ||
Object.keys(chatModelProviders[chatModelProvider])[0];
let llm: BaseChatModel | undefined;
if (chatModels[provider] && chatModels[provider][chatModel]) {
llm = chatModels[provider][chatModel].model as BaseChatModel | undefined;
if (body.chatModel?.provider === 'custom_openai') {
if (
!body.chatModel?.customOpenAIBaseURL ||
!body.chatModel?.customOpenAIKey
) {
return res
.status(400)
.json({ message: 'Missing custom OpenAI base URL or key' });
}
llm = new ChatOpenAI({
modelName: body.chatModel.model,
openAIApiKey: body.chatModel.customOpenAIKey,
temperature: 0.7,
configuration: {
baseURL: body.chatModel.customOpenAIBaseURL,
},
}) as unknown as BaseChatModel;
} else if (
chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel]
) {
llm = chatModelProviders[chatModelProvider][chatModel]
.model as unknown as BaseChatModel | undefined;
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
return;
return res.status(400).json({ message: 'Invalid model selected' });
}
const images = await handleImageSearch({ query, chat_history }, llm);
const images = await handleImageSearch(
{ query: body.query, chat_history: chatHistory },
llm,
);
res.status(200).json({ images });
} catch (err) {

View File

@ -6,6 +6,7 @@ import modelsRouter from './models';
import suggestionsRouter from './suggestions';
import chatsRouter from './chats';
import searchRouter from './search';
import discoverRouter from './discover';
const router = express.Router();
@ -16,5 +17,6 @@ router.use('/models', modelsRouter);
router.use('/suggestions', suggestionsRouter);
router.use('/chats', chatsRouter);
router.use('/search', searchRouter);
router.use('/discover', discoverRouter);
export default router;

View File

@ -14,6 +14,18 @@ router.get('/', async (req, res) => {
getAvailableEmbeddingModelProviders(),
]);
Object.keys(chatModelProviders).forEach((provider) => {
Object.keys(chatModelProviders[provider]).forEach((model) => {
delete chatModelProviders[provider][model].model;
});
});
Object.keys(embeddingModelProviders).forEach((provider) => {
Object.keys(embeddingModelProviders[provider]).forEach((model) => {
delete embeddingModelProviders[provider][model].model;
});
});
res.status(200).json({ chatModelProviders, embeddingModelProviders });
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });

View File

@ -25,6 +25,7 @@ interface embeddingModel {
}
interface ChatRequestBody {
optimizationMode: 'speed' | 'balanced';
focusMode: string;
chatModel?: chatModel;
embeddingModel?: embeddingModel;
@ -41,6 +42,7 @@ router.post('/', async (req, res) => {
}
body.history = body.history || [];
body.optimizationMode = body.optimizationMode || 'balanced';
const history: BaseMessage[] = body.history.map((msg) => {
if (msg[0] === 'human') {
@ -119,7 +121,13 @@ router.post('/', async (req, res) => {
return res.status(400).json({ message: 'Invalid focus mode' });
}
const emitter = searchHandler(body.query, history, llm, embeddings);
const emitter = searchHandler(
body.query,
history,
llm,
embeddings,
body.optimizationMode,
);
let message = '';
let sources = [];

View File

@ -4,14 +4,27 @@ 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 { ChatOpenAI } from '@langchain/openai';
const router = express.Router();
interface ChatModel {
provider: string;
model: string;
customOpenAIBaseURL?: string;
customOpenAIKey?: string;
}
interface SuggestionsBody {
chatHistory: any[];
chatModel?: ChatModel;
}
router.post('/', async (req, res) => {
try {
let { chat_history, chat_model, chat_model_provider } = req.body;
let body: SuggestionsBody = req.body;
chat_history = chat_history.map((msg: any) => {
const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
@ -19,22 +32,50 @@ router.post('/', async (req, res) => {
}
});
const chatModels = await getAvailableChatModelProviders();
const provider = chat_model_provider ?? Object.keys(chatModels)[0];
const chatModel = chat_model ?? Object.keys(chatModels[provider])[0];
const chatModelProviders = await getAvailableChatModelProviders();
const chatModelProvider =
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
const chatModel =
body.chatModel?.model ||
Object.keys(chatModelProviders[chatModelProvider])[0];
let llm: BaseChatModel | undefined;
if (chatModels[provider] && chatModels[provider][chatModel]) {
llm = chatModels[provider][chatModel].model as BaseChatModel | undefined;
if (body.chatModel?.provider === 'custom_openai') {
if (
!body.chatModel?.customOpenAIBaseURL ||
!body.chatModel?.customOpenAIKey
) {
return res
.status(400)
.json({ message: 'Missing custom OpenAI base URL or key' });
}
llm = new ChatOpenAI({
modelName: body.chatModel.model,
openAIApiKey: body.chatModel.customOpenAIKey,
temperature: 0.7,
configuration: {
baseURL: body.chatModel.customOpenAIBaseURL,
},
}) as unknown as BaseChatModel;
} else if (
chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel]
) {
llm = chatModelProviders[chatModelProvider][chatModel]
.model as unknown as BaseChatModel | undefined;
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
return;
return res.status(400).json({ message: 'Invalid model selected' });
}
const suggestions = await generateSuggestions({ chat_history }, llm);
const suggestions = await generateSuggestions(
{ chat_history: chatHistory },
llm,
);
res.status(200).json({ suggestions: suggestions });
} catch (err) {

View File

@ -4,14 +4,28 @@ import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import handleVideoSearch from '../agents/videoSearchAgent';
import { ChatOpenAI } from '@langchain/openai';
const router = express.Router();
interface ChatModel {
provider: string;
model: string;
customOpenAIBaseURL?: string;
customOpenAIKey?: string;
}
interface VideoSearchBody {
query: string;
chatHistory: any[];
chatModel?: ChatModel;
}
router.post('/', async (req, res) => {
try {
let { query, chat_history, chat_model_provider, chat_model } = req.body;
let body: VideoSearchBody = req.body;
chat_history = chat_history.map((msg: any) => {
const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
@ -19,22 +33,50 @@ router.post('/', async (req, res) => {
}
});
const chatModels = await getAvailableChatModelProviders();
const provider = chat_model_provider ?? Object.keys(chatModels)[0];
const chatModel = chat_model ?? Object.keys(chatModels[provider])[0];
const chatModelProviders = await getAvailableChatModelProviders();
const chatModelProvider =
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
const chatModel =
body.chatModel?.model ||
Object.keys(chatModelProviders[chatModelProvider])[0];
let llm: BaseChatModel | undefined;
if (chatModels[provider] && chatModels[provider][chatModel]) {
llm = chatModels[provider][chatModel].model as BaseChatModel | undefined;
if (body.chatModel?.provider === 'custom_openai') {
if (
!body.chatModel?.customOpenAIBaseURL ||
!body.chatModel?.customOpenAIKey
) {
return res
.status(400)
.json({ message: 'Missing custom OpenAI base URL or key' });
}
llm = new ChatOpenAI({
modelName: body.chatModel.model,
openAIApiKey: body.chatModel.customOpenAIKey,
temperature: 0.7,
configuration: {
baseURL: body.chatModel.customOpenAIBaseURL,
},
}) as unknown as BaseChatModel;
} else if (
chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel]
) {
llm = chatModelProviders[chatModelProvider][chatModel]
.model as unknown as BaseChatModel | undefined;
}
if (!llm) {
res.status(500).json({ message: 'Invalid LLM model selected' });
return;
return res.status(400).json({ message: 'Invalid model selected' });
}
const videos = await handleVideoSearch({ chat_history, query }, llm);
const videos = await handleVideoSearch(
{ chat_history: chatHistory, query: body.query },
llm,
);
res.status(200).json({ videos });
} catch (err) {

View File

@ -78,6 +78,18 @@ export const handleConnection = async (
ws.close();
}
const interval = setInterval(() => {
if (ws.readyState === ws.OPEN) {
ws.send(
JSON.stringify({
type: 'signal',
data: 'open',
}),
);
clearInterval(interval);
}
}, 5);
ws.on(
'message',
async (message) =>

View File

@ -10,8 +10,8 @@ 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 { chats, messages as messagesSchema } from '../db/schema';
import { eq, asc, gt } from 'drizzle-orm';
import crypto from 'crypto';
type Message = {
@ -22,7 +22,7 @@ type Message = {
type WSMessage = {
message: Message;
copilot: boolean;
optimizationMode: string;
type: string;
focusMode: string;
history: Array<[string, string]>;
@ -71,7 +71,7 @@ const handleEmitterEvents = (
emitter.on('end', () => {
ws.send(JSON.stringify({ type: 'messageEnd', messageId: messageId }));
db.insert(messages)
db.insert(messagesSchema)
.values({
content: recievedMessage,
chatId: chatId,
@ -106,7 +106,9 @@ export const handleMessage = async (
const parsedWSMessage = JSON.parse(message) as WSMessage;
const parsedMessage = parsedWSMessage.message;
const id = crypto.randomBytes(7).toString('hex');
const humanMessageId =
parsedMessage.messageId ?? crypto.randomBytes(7).toString('hex');
const aiMessageId = crypto.randomBytes(7).toString('hex');
if (!parsedMessage.content)
return ws.send(
@ -138,9 +140,10 @@ export const handleMessage = async (
history,
llm,
embeddings,
parsedWSMessage.optimizationMode,
);
handleEmitterEvents(emitter, ws, id, parsedMessage.chatId);
handleEmitterEvents(emitter, ws, aiMessageId, parsedMessage.chatId);
const chat = await db.query.chats.findFirst({
where: eq(chats.id, parsedMessage.chatId),
@ -158,18 +161,29 @@ export const handleMessage = async (
.execute();
}
await db
.insert(messages)
.values({
content: parsedMessage.content,
chatId: parsedMessage.chatId,
messageId: id,
role: 'user',
metadata: JSON.stringify({
createdAt: new Date(),
}),
})
.execute();
const messageExists = await db.query.messages.findFirst({
where: eq(messagesSchema.messageId, humanMessageId),
});
if (!messageExists) {
await db
.insert(messagesSchema)
.values({
content: parsedMessage.content,
chatId: parsedMessage.chatId,
messageId: humanMessageId,
role: 'user',
metadata: JSON.stringify({
createdAt: new Date(),
}),
})
.execute();
} else {
await db
.delete(messagesSchema)
.where(gt(messagesSchema.id, messageExists.id))
.execute();
}
} else {
ws.send(
JSON.stringify({

113
ui/app/discover/page.tsx Normal file
View File

@ -0,0 +1,113 @@
'use client';
import { Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { toast } from 'sonner';
interface Discover {
title: string;
content: string;
url: string;
thumbnail: string;
}
const Page = () => {
const [discover, setDiscover] = useState<Discover[] | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message);
}
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
setDiscover(data.blogs);
} catch (err: any) {
console.error('Error fetching data:', err.message);
toast.error('Error fetching data');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return loading ? (
<div className="flex flex-row items-center justify-center min-h-screen">
<svg
aria-hidden="true"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100.003 78.2051 78.1951 100.003 50.5908 100C22.9765 99.9972 0.997224 78.018 1 50.4037C1.00281 22.7993 22.8108 0.997224 50.4251 1C78.0395 1.00281 100.018 22.8108 100 50.4251ZM9.08164 50.594C9.06312 73.3997 27.7909 92.1272 50.5966 92.1457C73.4023 92.1642 92.1298 73.4365 92.1483 50.6308C92.1669 27.8251 73.4392 9.0973 50.6335 9.07878C27.8278 9.06026 9.10003 27.787 9.08164 50.594Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4037 97.8624 35.9116 96.9801 33.5533C95.1945 28.8227 92.871 24.3692 90.0681 20.348C85.6237 14.1775 79.4473 9.36872 72.0454 6.45794C64.6435 3.54717 56.3134 2.65431 48.3133 3.89319C45.869 4.27179 44.3768 6.77534 45.014 9.20079C45.6512 11.6262 48.1343 13.0956 50.5786 12.717C56.5073 11.8281 62.5542 12.5399 68.0406 14.7911C73.527 17.0422 78.2187 20.7487 81.5841 25.4923C83.7976 28.5886 85.4467 32.059 86.4416 35.7474C87.1273 38.1189 89.5423 39.6781 91.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
) : (
<>
<div>
<div className="flex flex-col pt-4">
<div className="flex items-center">
<Search />
<h1 className="text-3xl font-medium p-2">Discover</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
{discover &&
discover?.map((item, i) => (
<Link
href={`/?q=Summary: ${item.url}`}
key={i}
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
target="_blank"
>
<img
className="object-cover w-full aspect-video"
src={
new URL(item.thumbnail).origin +
new URL(item.thumbnail).pathname +
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
}
alt={item.title}
/>
<div className="px-6 py-4">
<div className="font-bold text-lg mb-2">
{item.title.slice(0, 100)}...
</div>
<p className="text-black-70 dark:text-white/70 text-sm">
{item.content.slice(0, 100)}...
</p>
</div>
</Link>
))}
</div>
</div>
</>
);
};
export default Page;

View File

@ -1,7 +1,7 @@
'use client';
import DeleteChat from '@/components/DeleteChat';
import { formatTimeDifference } from '@/lib/utils';
import { cn, formatTimeDifference } from '@/lib/utils';
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
@ -58,13 +58,12 @@ const Page = () => {
</div>
) : (
<div>
<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">
<div className="flex flex-col pt-4">
<div className="flex items-center">
<BookOpenText />
<h2 className="text-black dark:text-white lg:text-3xl lg:font-medium">
Library
</h2>
<h1 className="text-3xl font-medium p-2">Library</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
{chats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-screen">
@ -74,10 +73,15 @@ const Page = () => {
</div>
)}
{chats.length > 0 && (
<div className="flex flex-col pt-16 lg:pt-24">
<div className="flex flex-col pb-20 lg:pb-2">
{chats.map((chat, i) => (
<div
className="flex flex-col space-y-4 border-b border-white-200 dark:border-dark-200 py-6 lg:mx-4"
className={cn(
'flex flex-col space-y-4 py-6',
i !== chats.length - 1
? 'border-b border-white-200 dark:border-dark-200'
: '',
)}
key={i}
>
<Link

View File

@ -171,11 +171,22 @@ const useSocket = (
}
}, 10000);
ws.onopen = () => {
console.log('[DEBUG] open');
clearTimeout(timeoutId);
setIsWSReady(true);
};
ws.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
if (data.type === 'signal' && data.data === 'open') {
const interval = setInterval(() => {
if (ws.readyState === 1) {
setIsWSReady(true);
clearInterval(interval);
}
}, 5);
clearTimeout(timeoutId);
console.log('[DEBUG] opened');
}
if (data.type === 'error') {
toast.error(data.data);
}
});
ws.onerror = () => {
clearTimeout(timeoutId);
@ -189,13 +200,6 @@ const useSocket = (
console.log('[DEBUG] closed');
};
ws.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
if (data.type === 'error') {
toast.error(data.data);
}
});
setWs(ws);
};
@ -278,6 +282,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [focusMode, setFocusMode] = useState('webSearch');
const [optimizationMode, setOptimizationMode] = useState('speed');
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
@ -313,6 +318,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
console.log('[DEBUG] closed');
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const messagesRef = useRef<Message[]>([]);
@ -324,11 +330,13 @@ const ChatWindow = ({ id }: { id?: string }) => {
useEffect(() => {
if (isMessagesLoaded && isWSReady) {
setIsReady(true);
console.log('[DEBUG] ready');
}
}, [isMessagesLoaded, isWSReady]);
const sendMessage = async (message: string) => {
const sendMessage = async (message: string, messageId?: string) => {
if (loading) return;
setLoading(true);
setMessageAppeared(false);
@ -336,16 +344,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
let recievedMessage = '';
let added = false;
const messageId = crypto.randomBytes(7).toString('hex');
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
ws?.send(
JSON.stringify({
type: 'message',
message: {
messageId: messageId,
chatId: chatId!,
content: message,
},
focusMode: focusMode,
optimizationMode: optimizationMode,
history: [...chatHistory, ['human', message]],
}),
);
@ -467,15 +477,15 @@ const ChatWindow = ({ id }: { id?: string }) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
sendMessage(message.content);
sendMessage(message.content, message.messageId);
};
useEffect(() => {
if (isReady && initialMessage) {
if (isReady && initialMessage && ws?.readyState === 1) {
sendMessage(initialMessage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isReady, initialMessage]);
}, [ws?.readyState, isReady, initialMessage, isWSReady]);
if (hasError) {
return (
@ -494,7 +504,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
<div>
{messages.length > 0 ? (
<>
<Navbar messages={messages} />
<Navbar chatId={chatId!} messages={messages} />
<Chat
loading={loading}
messages={messages}
@ -508,6 +518,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
)}
</div>

View File

@ -1,5 +1,13 @@
import { Delete, Trash } from 'lucide-react';
import { Dialog, Transition } from '@headlessui/react';
import { Trash } from 'lucide-react';
import {
Description,
Dialog,
DialogBackdrop,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { Fragment, useState } from 'react';
import { toast } from 'sonner';
import { Chat } from '@/app/library/page';
@ -8,10 +16,12 @@ const DeleteChat = ({
chatId,
chats,
setChats,
redirect = false,
}: {
chatId: string;
chats: Chat[];
setChats: (chats: Chat[]) => void;
redirect?: boolean;
}) => {
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
@ -36,6 +46,10 @@ const DeleteChat = ({
const newChats = chats.filter((chat) => chat.id !== chatId);
setChats(newChats);
if (redirect) {
window.location.href = '/';
}
} catch (err: any) {
toast.error(err.message);
} finally {
@ -64,10 +78,10 @@ const DeleteChat = ({
}
}}
>
<Dialog.Backdrop className="fixed inset-0 bg-black/30" />
<DialogBackdrop className="fixed inset-0 bg-black/30" />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
@ -76,13 +90,13 @@ const DeleteChat = ({
leaveFrom="opacity-100 scale-200"
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">
<DialogPanel 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">
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
Delete Confirmation
</Dialog.Title>
<Dialog.Description className="text-sm dark:text-white/70 text-black/70">
</DialogTitle>
<Description className="text-sm dark:text-white/70 text-black/70">
Are you sure you want to delete this chat?
</Dialog.Description>
</Description>
<div className="flex flex-row items-end justify-end space-x-4 mt-6">
<button
onClick={() => {
@ -101,8 +115,8 @@ const DeleteChat = ({
Delete
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>

View File

@ -1,16 +1,32 @@
import { Settings } from 'lucide-react';
import EmptyChatMessageInput from './EmptyChatMessageInput';
import SettingsDialog from './SettingsDialog';
import { useState } from 'react';
const EmptyChat = ({
sendMessage,
focusMode,
setFocusMode,
optimizationMode,
setOptimizationMode,
}: {
sendMessage: (message: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
return (
<div className="relative">
<SettingsDialog isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
<Settings
className="cursor-pointer lg:hidden"
onClick={() => setIsSettingsOpen(true)}
/>
</div>
<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.
@ -19,6 +35,8 @@ const EmptyChat = ({
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
</div>

View File

@ -3,15 +3,20 @@ import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import CopilotToggle from './MessageInputActions/Copilot';
import Focus from './MessageInputActions/Focus';
import Optimization from './MessageInputActions/Optimization';
const EmptyChatMessageInput = ({
sendMessage,
focusMode,
setFocusMode,
optimizationMode,
setOptimizationMode,
}: {
sendMessage: (message: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
@ -35,6 +40,8 @@ const EmptyChatMessageInput = ({
document.addEventListener('keydown', handleKeyDown);
inputRef.current?.focus();
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
@ -66,14 +73,13 @@ const EmptyChatMessageInput = ({
placeholder="Ask anything..."
/>
<div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-1 -mx-2">
<div className="flex flex-row items-center space-x-4">
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
{/* <Attach /> */}
</div>
<div className="flex flex-row items-center space-x-4 -mx-2">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
<button
disabled={message.trim().length === 0}

View File

@ -186,10 +186,10 @@ const MessageBox = ({
<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)}
chatHistory={history.slice(0, messageIndex - 1)}
/>
<SearchVideos
chat_history={history.slice(0, messageIndex - 1)}
chatHistory={history.slice(0, messageIndex - 1)}
query={history[messageIndex - 1].content}
/>
</div>

View File

@ -7,7 +7,12 @@ import {
SwatchBook,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Popover, Transition } from '@headlessui/react';
import {
Popover,
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
import { Fragment } from 'react';
@ -70,10 +75,10 @@ const Focus = ({
setFocusMode: (mode: string) => void;
}) => {
return (
<Popover className="fixed w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<Popover.Button
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<PopoverButton
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"
className=" 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' ? (
<div className="flex flex-row items-center space-x-1">
@ -86,7 +91,7 @@ const Focus = ({
) : (
<ScanEye />
)}
</Popover.Button>
</PopoverButton>
<Transition
as={Fragment}
enter="transition ease-out duration-150"
@ -96,10 +101,10 @@ const Focus = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute z-10 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto">
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{focusModes.map((mode, i) => (
<Popover.Button
<PopoverButton
onClick={() => setFocusMode(mode.key)}
key={i}
className={cn(
@ -123,10 +128,10 @@ const Focus = ({
<p className="text-black/70 dark:text-white/70 text-xs">
{mode.description}
</p>
</Popover.Button>
</PopoverButton>
))}
</div>
</Popover.Panel>
</PopoverPanel>
</Transition>
</Popover>
);

View File

@ -0,0 +1,104 @@
import { ChevronDown, Sliders, Star, Zap } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import { Fragment } from 'react';
const OptimizationModes = [
{
key: 'speed',
title: 'Speed',
description: 'Prioritize speed and get the quickest possible answer.',
icon: <Zap size={20} className="text-[#FF9800]" />,
},
{
key: 'balanced',
title: 'Balanced',
description: 'Find the right balance between speed and accuracy',
icon: <Sliders size={20} className="text-[#4CAF50]" />,
},
{
key: 'quality',
title: 'Quality (Soon)',
description: 'Get the most thorough and accurate answer',
icon: (
<Star
size={16}
className="text-[#2196F3] dark:text-[#BBDEFB] fill-[#BBDEFB] dark:fill-[#2196F3]"
/>
),
},
];
const Optimization = ({
optimizationMode,
setOptimizationMode,
}: {
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<PopoverButton
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"
>
<div className="flex flex-row items-center space-x-1">
{
OptimizationModes.find((mode) => mode.key === optimizationMode)
?.icon
}
<p className="text-xs font-medium">
{
OptimizationModes.find((mode) => mode.key === optimizationMode)
?.title
}
</p>
<ChevronDown size={20} />
</div>
</PopoverButton>
<Transition
as={Fragment}
enter="transition ease-out duration-150"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0">
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{OptimizationModes.map((mode, i) => (
<PopoverButton
onClick={() => setOptimizationMode(mode.key)}
key={i}
disabled={mode.key === 'quality'}
className={cn(
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-1 duration-200 cursor-pointer transition',
optimizationMode === mode.key
? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
mode.key === 'quality' && 'opacity-50 cursor-not-allowed',
)}
>
<div className="flex flex-row items-center space-x-1 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>
</PopoverButton>
))}
</div>
</PopoverPanel>
</Transition>
</Popover>
);
};
export default Optimization;

View File

@ -1,5 +1,11 @@
/* eslint-disable @next/next/no-img-element */
import { Dialog, Transition } from '@headlessui/react';
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { Document } from '@langchain/core/documents';
import { Fragment, useState } from 'react';
@ -74,7 +80,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
@ -83,10 +89,10 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
leaveFrom="opacity-100 scale-200"
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">
<DialogPanel 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">
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
Sources
</Dialog.Title>
</DialogTitle>
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
{sources.map((source, i) => (
<a
@ -122,8 +128,8 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
</a>
))}
</div>
</Dialog.Panel>
</Transition.Child>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>

View File

@ -2,8 +2,15 @@ import { Clock, Edit, Share, Trash } from 'lucide-react';
import { Message } from './ChatWindow';
import { useEffect, useState } from 'react';
import { formatTimeDifference } from '@/lib/utils';
import DeleteChat from './DeleteChat';
const Navbar = ({ messages }: { messages: Message[] }) => {
const Navbar = ({
chatId,
messages,
}: {
messages: Message[];
chatId: string;
}) => {
const [title, setTitle] = useState<string>('');
const [timeAgo, setTimeAgo] = useState<string>('');
@ -39,10 +46,12 @@ 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}
<a
href="/"
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
/>
>
<Edit size={17} />
</a>
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
<Clock size={17} />
<p className="text-xs">{timeAgo} ago</p>
@ -54,10 +63,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
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"
/>
<DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} />
</div>
</div>
);

View File

@ -13,10 +13,10 @@ type Image = {
const SearchImages = ({
query,
chat_history,
chatHistory,
}: {
query: string;
chat_history: Message[];
chatHistory: Message[];
}) => {
const [images, setImages] = useState<Image[] | null>(null);
const [loading, setLoading] = useState(false);
@ -33,6 +33,9 @@ const SearchImages = ({
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
{
@ -42,9 +45,15 @@ const SearchImages = ({
},
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
);

View File

@ -26,10 +26,10 @@ declare module 'yet-another-react-lightbox' {
const Searchvideos = ({
query,
chat_history,
chatHistory,
}: {
query: string;
chat_history: Message[];
chatHistory: Message[];
}) => {
const [videos, setVideos] = useState<Video[] | null>(null);
const [loading, setLoading] = useState(false);
@ -46,6 +46,9 @@ const Searchvideos = ({
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
{
@ -55,9 +58,15 @@ const Searchvideos = ({
},
body: JSON.stringify({
query: query,
chat_history: chat_history,
chat_model_provider: chatModelProvider,
chat_model: chatModel,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
);

View File

@ -1,5 +1,11 @@
import { cn } from '@/lib/utils';
import { Dialog, Transition } from '@headlessui/react';
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
import React, {
Fragment,
@ -122,7 +128,9 @@ const SettingsDialog = ({
const chatModel =
localStorage.getItem('chatModel') ||
(data.chatModelProviders &&
data.chatModelProviders[chatModelProvider]?.[0].name) ||
data.chatModelProviders[chatModelProvider]?.length > 0
? data.chatModelProviders[chatModelProvider][0].name
: undefined) ||
'';
const embeddingModelProvider =
localStorage.getItem('embeddingModelProvider') ||
@ -188,7 +196,7 @@ const SettingsDialog = ({
className="relative z-50"
onClose={() => setIsOpen(false)}
>
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
@ -198,10 +206,10 @@ const SettingsDialog = ({
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-white/50 dark:bg-black/50" />
</Transition.Child>
</TransitionChild>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
@ -210,10 +218,10 @@ const SettingsDialog = ({
leaveFrom="opacity-100 scale-200"
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">
<DialogPanel 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">
<DialogTitle className="text-xl font-medium leading-6 dark:text-white">
Settings
</Dialog.Title>
</DialogTitle>
{config && !isLoading && (
<div className="flex flex-col space-y-4 mt-6">
<div className="flex flex-col space-y-1">
@ -491,8 +499,8 @@ const SettingsDialog = ({
)}
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>

View File

@ -4,15 +4,24 @@ export const getSuggestions = async (chatHisory: Message[]) => {
const chatModel = localStorage.getItem('chatModel');
const chatModelProvider = localStorage.getItem('chatModelProvider');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/suggestions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_history: chatHisory,
chat_model: chatModel,
chat_model_provider: chatModelProvider,
chatHistory: chatHisory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIKey,
customOpenAIBaseURL,
}),
},
}),
});

View File

@ -1,6 +1,6 @@
{
"name": "perplexica-frontend",
"version": "1.9.0-rc3",
"version": "1.9.3",
"license": "MIT",
"author": "ItzCrazyKns",
"scripts": {
@ -11,14 +11,14 @@
"format:write": "prettier . --write"
},
"dependencies": {
"@headlessui/react": "^1.7.18",
"@headlessui/react": "^2.1.9",
"@icons-pack/react-simple-icons": "^9.4.0",
"@langchain/openai": "^0.0.25",
"@tailwindcss/typography": "^0.5.12",
"clsx": "^2.1.0",
"langchain": "^0.1.30",
"lucide-react": "^0.363.0",
"markdown-to-jsx": "^7.4.5",
"markdown-to-jsx": "^7.6.2",
"next": "14.1.4",
"next-themes": "^0.3.0",
"react": "^18",

View File

@ -66,13 +66,51 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
"@headlessui/react@^1.7.18":
version "1.7.18"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.18.tgz#30af4634d2215b2ca1aa29d07f33d02bea82d9d7"
integrity sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==
"@floating-ui/core@^1.6.0":
version "1.6.8"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12"
integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==
dependencies:
"@tanstack/react-virtual" "^3.0.0-beta.60"
client-only "^0.0.1"
"@floating-ui/utils" "^0.2.8"
"@floating-ui/dom@^1.0.0":
version "1.6.11"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.11.tgz#8631857838d34ee5712339eb7cbdfb8ad34da723"
integrity sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==
dependencies:
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.8"
"@floating-ui/react-dom@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31"
integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==
dependencies:
"@floating-ui/dom" "^1.0.0"
"@floating-ui/react@^0.26.16":
version "0.26.24"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.24.tgz#072b9dfeca4e79ef4e3000ef1c28e0ffc86f4ed4"
integrity sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==
dependencies:
"@floating-ui/react-dom" "^2.1.2"
"@floating-ui/utils" "^0.2.8"
tabbable "^6.0.0"
"@floating-ui/utils@^0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@headlessui/react@^2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.1.9.tgz#d8d3ff64255177a87706cc4f24f42aeac65b1695"
integrity sha512-ckWw7vlKtnoa1fL2X0fx1a3t/Li9MIKDVXn3SgG65YlxvDAsNrY39PPCxVM7sQRA7go2fJsuHSSauKFNaJHH7A==
dependencies:
"@floating-ui/react" "^0.26.16"
"@react-aria/focus" "^3.17.1"
"@react-aria/interactions" "^3.21.3"
"@tanstack/react-virtual" "^3.8.1"
"@humanwhocodes/config-array@^0.11.14":
version "0.11.14"
@ -278,6 +316,57 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@react-aria/focus@^3.17.1":
version "3.18.3"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.18.3.tgz#4fe32de1e7530beab8da2e7b89f0f17d22a47e5e"
integrity sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==
dependencies:
"@react-aria/interactions" "^3.22.3"
"@react-aria/utils" "^3.25.3"
"@react-types/shared" "^3.25.0"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"
"@react-aria/interactions@^3.21.3", "@react-aria/interactions@^3.22.3":
version "3.22.3"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.22.3.tgz#3ba50db12f6ed443ae061eed79e41509eaa3d8e6"
integrity sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==
dependencies:
"@react-aria/ssr" "^3.9.6"
"@react-aria/utils" "^3.25.3"
"@react-types/shared" "^3.25.0"
"@swc/helpers" "^0.5.0"
"@react-aria/ssr@^3.9.6":
version "3.9.6"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.6.tgz#a9e8b351acdc8238f2b5215b0ce904636c6ea690"
integrity sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==
dependencies:
"@swc/helpers" "^0.5.0"
"@react-aria/utils@^3.25.3":
version "3.25.3"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.25.3.tgz#cad9bffc07b045cdc283df2cb65c18747acbf76d"
integrity sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==
dependencies:
"@react-aria/ssr" "^3.9.6"
"@react-stately/utils" "^3.10.4"
"@react-types/shared" "^3.25.0"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"
"@react-stately/utils@^3.10.4":
version "3.10.4"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.4.tgz#310663a834b67048d305e1680ed258130092fe51"
integrity sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==
dependencies:
"@swc/helpers" "^0.5.0"
"@react-types/shared@^3.25.0":
version "3.25.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.25.0.tgz#7223baf72256e918a3c29081bb1ecc6fad4fbf58"
integrity sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==
"@rushstack/eslint-patch@^1.3.3":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf"
@ -290,6 +379,13 @@
dependencies:
tslib "^2.4.0"
"@swc/helpers@^0.5.0":
version "0.5.13"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.13.tgz#33e63ff3cd0cade557672bd7888a39ce7d115a8c"
integrity sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==
dependencies:
tslib "^2.4.0"
"@tailwindcss/typography@^0.5.12":
version "0.5.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.12.tgz#c0532fd594427b7f4e8e38eff7bf272c63a1dca4"
@ -300,17 +396,17 @@
lodash.merge "^4.6.2"
postcss-selector-parser "6.0.10"
"@tanstack/react-virtual@^3.0.0-beta.60":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz#fb70f9c6baee753a5a0f7618ac886205d5a02af9"
integrity sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==
"@tanstack/react-virtual@^3.8.1":
version "3.10.8"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz#bf4b06f157ed298644a96ab7efc1a2b01ab36e3c"
integrity sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==
dependencies:
"@tanstack/virtual-core" "3.2.0"
"@tanstack/virtual-core" "3.10.8"
"@tanstack/virtual-core@3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz#874d36135e4badce2719e7bdc556ce240cbaff14"
integrity sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==
"@tanstack/virtual-core@3.10.8":
version "3.10.8"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4"
integrity sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==
"@types/json5@^0.0.29":
version "0.0.29"
@ -779,11 +875,16 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
client-only@0.0.1, client-only@^0.0.1:
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
clsx@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
@ -2109,10 +2210,10 @@ lucide-react@^0.363.0:
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.363.0.tgz#2bb1f9d09b830dda86f5118fcd097f87247fe0e3"
integrity sha512-AlsfPCsXQyQx7wwsIgzcKOL9LwC498LIMAo+c0Es5PkHJa33xwmYAkkSoKoJWWWSYQEStqu58/jT4tL2gi32uQ==
markdown-to-jsx@^7.4.5:
version "7.4.6"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.6.tgz#1ea0018c549bf00c9ce35e8f4ea57e48028d9cf7"
integrity sha512-3cyNxI/PwotvYkjg6KmFaN1uyN/7NqETteD2DobBB8ro/FR9jsHIh4Fi7ywAz0s9QHRKCmGlOUggs5GxSWACKA==
markdown-to-jsx@^7.6.2:
version "7.6.2"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.6.2.tgz#254cbf7d412a37073486c0a2dd52266d2191a793"
integrity sha512-gEcyiJXzBxmId2Y/kydLbD6KRNccDiUy/Src1cFGn3s2X0LZZ/hUiEc2VisFyA5kUE3SXclTCczjQiAuqKZiFQ==
md5@^2.3.0:
version "2.3.0"
@ -2995,6 +3096,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tabbable@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwind-merge@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.2.tgz#87341e7604f0e20499939e152cd2841f41f7a3df"

View File

@ -690,6 +690,13 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
"@types/ws@^8.5.12":
version "8.5.12"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
dependencies:
"@types/node" "*"
"@xenova/transformers@^2.17.1":
version "2.17.1"
resolved "https://registry.yarnpkg.com/@xenova/transformers/-/transformers-2.17.1.tgz#712f7a72c76c8aa2075749382f83dc7dd4e5a9a5"