Compare commits

..

20 Commits
v1.6.0 ... GCP

Author SHA1 Message Date
ItzCrazyKns
35a3eda213 Merge pull request #155 from notedsource/hristo/gcp-deploy-vertexai-models-embeddings
Hristo/gcp deploy vertexai models embeddings
2024-06-01 10:49:38 +05:30
Hristo
dfed6a0ad8 Use container restart policy from main 2024-05-30 17:21:40 -04:00
Hristo
e0d9522435 Merge branch 'master' of github.com:notedsource/Perplexica into hristo/gcp-deploy-vertexai-models-embeddings 2024-05-30 11:19:50 -04:00
Hristo
f7c3bc2823 No auth on root route for health checks, fix suggestions request 2024-05-30 11:18:31 -04:00
Hristo
0ac971e6b4 Merge branch 'hristo/deploy-on-gcp-gke' of github.com:notedsource/Perplexica into hristo/vertexai-embeddings 2024-05-22 15:05:45 -04:00
Hristo
4ff6502dae Restore searxng dockerfile to enable remote builds 2024-05-22 15:04:25 -04:00
Hristo
795309cfe2 Private searxng instance 2024-05-22 14:52:47 -04:00
Hristo
8bf4269208 Add vertexai text embeddings capability 2024-05-21 16:23:34 -04:00
Hristo
4c7942d2e8 Merge branch 'master' of github.com:notedsource/Perplexica into hristo/deploy-on-gcp-gke 2024-05-21 15:41:23 -04:00
Hristo
aa55206a30 Add VertexAI deps using yarn not npm 2024-05-21 15:15:19 -04:00
Hristo
27d7b000d0 Add AI/ML infrence scope to OAuth credentials requested for cluster IAM account 2024-05-21 14:31:14 -04:00
Hristo
8b9b4085ea Fix query appearing twice in chat history
The initial query appears twice in the prompt, this is ignored by OpenAI
models, however it breaks with Gemini models are they fail with an error
stating that AI and User prompts need to alternate.

Tested all search modes with both OpenAI GTP3 turbo and Vertex Gemini
1.0 and this changes appears to now function correctly with both.
2024-05-17 14:10:11 -04:00
Hristo
2e58dab30a Additional changes for VertexAI 2024-05-17 14:08:57 -04:00
Hristo
48018990be Ensure containers are brought backup when exiting on error
This is esp. important for the NodeJS (backend) container as  Node will
exit on any unhandled error, it is best practice to let the errored
process crash and start a new one in its place. It this case we use
docker to do that for us (`restart: always` policy)
2024-05-16 09:53:33 -04:00
Hristo
ebbe18ab45 Adds Google VertexAI as model provider 2024-05-14 15:05:17 -04:00
Hristo
cef75279c5 Add Google VertexAI deps. 2024-05-14 14:51:26 -04:00
Hristo
c56a058a74 Websocket auth, pass access token in gke configs 2024-05-10 19:32:35 -04:00
Hristo
4e20c4ac56 Finalizes option to secure backend http endpoints with a token
- Also fixes to build commands in makefile
2024-05-10 18:11:23 -04:00
Hristo
e6c2042df6 Backend GKE Deploy, access key for backend
- Configs and automation for deploying backend to GKE
- First steps to adding an optional token check for requests to backend
- First steps frontend sending optional token to backend when configured
2024-05-10 16:07:58 -04:00
Hristo
0fedaef537 First pass at setting up GCP deploy config as infrastructure
- Terraform config files to setup cluster, deployments and services
  - Adds only Searxng deployment and test service in this commit

- Makefile to:
  - Build and push images
  - Run terraform with correct project configuration

- Env file template to help setting .env file with project configs
2024-05-08 18:19:59 -04:00
53 changed files with 1157 additions and 559 deletions

20
Makefile Normal file
View File

@@ -0,0 +1,20 @@
.PHONY: run
run:
docker compose -f docker-compose.yaml up
.PHONY: rebuild-run
rebuild-run:
docker compose -f docker-compose.yaml build --no-cache \
&& docker compose -f docker-compose.yaml up
.PHONY: run-app-only
run-app-only:
docker compose -f app-docker-compose.yaml up
.PHONY: rebuild-run-app-only
rebuild-run-app-only:
docker compose -f app-docker-compose.yaml build --no-cache \
&& docker compose -f app-docker-compose.yaml up

View File

@@ -118,6 +118,27 @@ If you wish to use Perplexica as an alternative to traditional search engines li
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
## Deploy Perplexica backend to Google GKE
0: Install `docker` and `terraform` (Process specific to your system)
1a: Copy the `sample.env` file to `.env`
1b: Copy the `deploy/gcp/sample.env` file to `deploy/gcp/.env`
2a: Fillout desired LLM provider access keys etc. in `.env`
- Note: you will have to comeback and edit this file again once you have the address of the K8s backend deploy
2b: Fillout the GCP info in `deploy/gcp/.env`
3: Edit `GCP_REPO` to the correct docker image repo path if you are using something other than Container registry
4: Edit the `PREFIX` if you would like images and GKE entities to be prefixed with something else
5: In `deploy/gcp` run `make init` to initialize terraform
6: Follow the normal Preplexica configuration steps outlined in the project readme
7: Auth docker with the appropriate credential for repo Ex. for `gcr.io` -> `gcloud auth configure-docker`
8: In `deploy/gcp` run `make build-deplpy` to build and push the project images to the repo, create a GKE cluster and deploy the app
9: Once deployed successfully edit the `.env` file in the root project folder and update the `REMOTE_BACKEND_ADDRESS` with the remote k8s deployment address and port
10: In root project folder run `make rebuild-run-app-only`
If you configured everything correctly frontend app will run locally and provide you with a local url to open it.
Now you can run queries against the remotely deployed backend from your local machine. :celebrate:
## Upcoming Features
- [ ] Finalizing Copilot Mode

13
app-docker-compose.yaml Normal file
View File

@@ -0,0 +1,13 @@
services:
perplexica-frontend:
build:
context: .
dockerfile: app.dockerfile
args:
- NEXT_PUBLIC_SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
- NEXT_PUBLIC_API_URL=https://${REMOTE_BACKEND_ADDRESS}/api
- NEXT_PUBLIC_WS_URL=wss://${REMOTE_BACKEND_ADDRESS}
expose:
- 3000
ports:
- 3000:3000

View File

@@ -2,8 +2,11 @@ FROM node:alpine
ARG NEXT_PUBLIC_WS_URL
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_SUPER_SECRET_KEY
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_SUPER_SECRET_KEY=${NEXT_PUBLIC_SUPER_SECRET_KEY}
WORKDIR /home/perplexica
@@ -12,4 +15,4 @@ COPY ui /home/perplexica/
RUN yarn install
RUN yarn build
CMD ["yarn", "start"]
CMD ["yarn", "start"]

6
deploy/gcp/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.env
.auto.tfvars
.terraform
terraform.tfstate
terraform.tfstate.*
.terraform.lock.hcl

103
deploy/gcp/Makefile Normal file
View File

@@ -0,0 +1,103 @@
# Adds all the deployment relevant sensitive information about project
include .env
# Adds secrets/ keys we have define for the project locally and deployment
include ../../.env
# Use `location-id-docker.pkg` for artifact registry Ex. west-1-docker.pkg
GCP_REPO=gcr.io
PREFIX=perplexica
SEARCH_PORT=8080
BACKEND_PORT=3001
SEARCH_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-searxng:latest
BACKEND_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-backend:latest
APP_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-app:latest
CLUSTER_NAME=$(PREFIX)-cluster
.PHONY: build-deploy
build-deploy: docker-build-all deploy
.PHONY: docker-build-all
docker-build-all: docker-build-push-searxng docker-build-push-backend docker-build-push-app
.PHONY: show_config
show_config:
@echo $(GCP_PROJECT_ID) \
&& echo $(CLUSTER_NAME) \
&& echo $(GCP_REGION) \
&& echo $(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& echo $(SEARCH_IMAGE_TAG) \
&& echo $(BACKEND_IMAGE_TAG) \
&& echo $(APP_IMAGE_TAG) \
&& echo $(SEARCH_PORT) \
&& echo $(BACKEND_PORT) \
&& echo $(OPENAI) \
&& echo $(SUPER_SECRET_KEY)
.PHONY: docker-build-push-searxng
docker-build-push-searxng:
cd ../../ && docker build -f ./deploy/gcp/searxng.dockerfile -t $(SEARCH_IMAGE_TAG) . --platform="linux/amd64"
docker push $(SEARCH_IMAGE_TAG)
.PHONY: docker-build-push-backend
docker-build-push-backend:
cd ../../ && docker build -f ./backend.dockerfile -t $(BACKEND_IMAGE_TAG) . --platform="linux/amd64"
docker push $(BACKEND_IMAGE_TAG)
.PHONY: docker-build-push-app
docker-build-push-app:
#
# cd ../../ && docker build -f ./app.dockerfile -t $(APP_IMAGE_TAG) . --platform="linux/amd64"
# docker push $(APP_IMAGE_TAG)
.PHONY: init
init:
terraform init
.PHONY: deploy
deploy:
export TF_VAR_project_id=$(GCP_PROJECT_ID) \
&& export TF_VAR_cluster_name=$(CLUSTER_NAME) \
&& export TF_VAR_region=$(GCP_REGION) \
&& export TF_VAR_key_file=$(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& export TF_VAR_search_image=$(SEARCH_IMAGE_TAG) \
&& export TF_VAR_backend_image=$(BACKEND_IMAGE_TAG) \
&& export TF_VAR_app_image=$(APP_IMAGE_TAG) \
&& export TF_VAR_search_port=$(SEARCH_PORT) \
&& export TF_VAR_backend_port=$(BACKEND_PORT) \
&& export TF_VAR_open_ai=$(OPENAI) \
&& export TF_VAR_secret_key=$(SUPER_SECRET_KEY) \
&& terraform apply
.PHONY: teardown
teardown:
export TF_VAR_project_id=$(GCP_PROJECT_ID) \
&& export TF_VAR_cluster_name=$(CLUSTER_NAME) \
&& export TF_VAR_region=$(GCP_REGION) \
&& export TF_VAR_key_file=$(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& export TF_VAR_search_image=$(SEARCH_IMAGE_TAG) \
&& export TF_VAR_backend_image=$(BACKEND_IMAGE_TAG) \
&& export TF_VAR_app_image=$(APP_IMAGE_TAG) \
&& export TF_VAR_search_port=$(SEARCH_PORT) \
&& export TF_VAR_backend_port=$(BACKEND_PORT) \
&& export TF_VAR_open_ai=$(OPENAI) \
&& export TF_VAR_secret_key=$(SUPER_SECRET_KEY) \
&& terraform destroy
.PHONY: auth-kubectl
auth-kubectl:
gcloud container clusters get-credentials $(CLUSTER_NAME) --region=$(GCP_REGION)
.PHONY: rollout-new-version-backend
rollout-new-version-backend: auth-kubectl
kubectl rollout restart deploy backend

View File

@@ -0,0 +1,60 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.28.0"
}
}
}
variable "project_id" {
description = "The ID of the project in which resources will be deployed."
type = string
}
variable "name" {
description = "The GKE Cluster name"
type = string
}
variable "region" {
description = "The GCP region to deploy to."
type = string
}
variable "key_file" {
description = "The path to the GCP service account key file."
type = string
}
provider "google" {
credentials = file(var.key_file)
project = var.project_id
region = var.region
}
resource "google_container_cluster" "cluster" {
name = var.name
location = var.region
initial_node_count = 1
remove_default_node_pool = true
}
resource "google_container_node_pool" "primary_preemptible_nodes" {
name = "${google_container_cluster.cluster.name}-node-pool"
location = var.region
cluster = google_container_cluster.cluster.name
node_count = 1
node_config {
machine_type = "n1-standard-4"
disk_size_gb = 25
spot = true
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring",
]
}
}

238
deploy/gcp/main.tf Normal file
View File

@@ -0,0 +1,238 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.28.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
provider "google" {
credentials = file(var.key_file)
project = var.project_id
region = var.region
}
data "google_client_config" "default" {
depends_on = [module.gke-cluster]
}
# Defer reading the cluster data until the GKE cluster exists.
data "google_container_cluster" "default" {
name = var.cluster_name
depends_on = [module.gke-cluster]
location = var.region
}
provider "kubernetes" {
host = "https://${data.google_container_cluster.default.endpoint}"
token = data.google_client_config.default.access_token
cluster_ca_certificate = base64decode(
data.google_container_cluster.default.master_auth[0].cluster_ca_certificate,
)
}
#####################################################################################################
# SearXNG - Search engine deployment and service
#####################################################################################################
resource "kubernetes_deployment" "searxng" {
metadata {
name = "searxng"
labels = {
app = "searxng"
}
}
spec {
replicas = 1
selector {
match_labels = {
component = "searxng"
}
}
template {
metadata {
labels = {
component = "searxng"
}
}
spec {
container {
image = var.search_image
name = "searxng-container"
port {
container_port = var.search_port
}
}
}
}
}
}
resource "kubernetes_service" "searxng_service" {
metadata {
name = "searxng-service"
namespace = "default"
annotations = {
"networking.gke.io/load-balancer-type" = "Internal" # Remove to create an external loadbalancer
}
}
spec {
selector = {
component = "searxng"
}
port {
port = var.search_port
target_port = var.search_port
}
type = "LoadBalancer"
}
}
#####################################################################################################
# Perplexica - backend deployment and service
#####################################################################################################
resource "kubernetes_deployment" "backend" {
metadata {
name = "backend"
labels = {
app = "backend"
}
}
spec {
replicas = 1
selector {
match_labels = {
component = "backend"
}
}
template {
metadata {
labels = {
component = "backend"
}
}
spec {
container {
image = var.backend_image
name = "backend-container"
port {
container_port = var.backend_port
}
env {
# searxng service ip
name = "SEARXNG_API_URL"
value = "http://${kubernetes_service.searxng_service.status[0].load_balancer[0].ingress[0].ip}:${var.search_port}"
}
env {
# openai key
name = "OPENAI"
value = var.open_ai
}
env {
# port
name = "PORT"
value = var.backend_port
}
env {
# Access key for backend
name = "SUPER_SECRET_KEY"
value = var.secret_key
}
}
}
}
}
}
resource "kubernetes_service" "backend_service" {
metadata {
name = "backend-service"
namespace = "default"
}
spec {
selector = {
component = "backend"
}
port {
port = var.backend_port
target_port = var.backend_port
}
type = "LoadBalancer"
}
}
#####################################################################################################
# Variable and module definitions
#####################################################################################################
variable "project_id" {
description = "The ID of the project in which the resources will be deployed."
type = string
}
variable "key_file" {
description = "The path to the GCP service account key file."
type = string
}
variable "region" {
description = "The GCP region to deploy to."
type = string
}
variable "cluster_name" {
description = "The GCP region to deploy to."
type = string
}
variable "search_image" {
description = "Tag for the searxng image"
type = string
}
variable "backend_image" {
description = "Tag for the Perplexica backend image"
type = string
}
variable "app_image" {
description = "Tag for the app image"
type = string
}
variable "open_ai" {
description = "OPENAI access key"
type = string
}
variable "secret_key" {
description = "Access key to secure backend endpoints"
type = string
}
variable "search_port" {
description = "Port for searxng service"
type = number
}
variable "backend_port" {
description = "Port for backend service"
type = number
}
module "gke-cluster" {
source = "./gke-cluster"
project_id = var.project_id
name = var.cluster_name
region = var.region
key_file = var.key_file
}

7
deploy/gcp/sample.env Normal file
View File

@@ -0,0 +1,7 @@
# Rename this file to .env
# 0: Update to your GCP project id
# 1: Update to the path where the GCP service account credential file is kept
# 2: Update the region to your desired GCP region
GCP_PROJECT_ID=name-of-your-gcp-project
GCP_SERVICE_ACCOUNT_KEY_FILE=/Path/to/your/gcp-service-account-key-file.json
GCP_REGION=us-east1

View File

@@ -0,0 +1,3 @@
FROM searxng/searxng
COPY searxng/ /etc/searxng/

View File

@@ -14,13 +14,22 @@ services:
context: .
dockerfile: backend.dockerfile
args:
- SEARXNG_API_URL=http://searxng:8080
- SEARXNG_API_URL=null
volumes:
- "/Volumes/keys/headllamp/keys/:/var/keys/"
- "${GOOGLE_APPLICATION_CREDENTIALS}:/var/keys/gcp_service_account.json"
environment:
SEARXNG_API_URL: 'http://searxng:8080'
SUPER_SECRET_KEY: ${SUPER_SECRET_KEY}
OPENAI: ${OPENAI}
GROQ: ${GROQ}
OLLAMA_API_URL: ${OLLAMA_API_URL}
GOOGLE_APPLICATION_CREDENTIALS: /var/keys/gcp_service_account.json
USE_JWT: ${USE_JWT}
depends_on:
- searxng
ports:
- 3001:3001
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- perplexica-network
restart: unless-stopped
@@ -30,6 +39,7 @@ services:
context: .
dockerfile: app.dockerfile
args:
- NEXT_PUBLIC_SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
depends_on:

View File

@@ -1,6 +1,6 @@
{
"name": "perplexica-backend",
"version": "1.6.0",
"version": "1.5.0",
"license": "MIT",
"author": "ItzCrazyKns",
"scripts": {
@@ -21,6 +21,7 @@
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@langchain/google-vertexai": "^0.0.16",
"@langchain/openai": "^0.0.25",
"@xenova/transformers": "^2.17.1",
"axios": "^1.6.8",

24
sample.env Normal file
View File

@@ -0,0 +1,24 @@
# Copy this file over to .env and fill in the desired config.
# .env will become available to docker compose and these values will be
# used when running docker compose up
# Edit to set OpenAI access key
OPENAI=ADD OPENAI KEY HERE
# Uncomment and edit to set GROQ access key
# GROQ: ${GROQ}
# Uncomment and edit to set OLLAMA Url
# OLLAMA_API_URL: ${OLLAMA_API_URL}
# Address and port of the remotely deployed Perplexica backend
REMOTE_BACKEND_ADDRESS=111.111.111.111:0000
# Uncomment and edit to configure backend to reject requests without token
# leave commented to have open access to all endpoints
# Secret key to "secure" backend
# SUPER_SECRET_KEY=THISISASUPERSECRETKEYSERIOUSLY
# Uncomment and edit to configure a specific service account key file to use to
# auth with VertexAI when running (backend) full Perplexica stack locally
# GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/gcp-service-account-key-file.json

View File

@@ -209,7 +209,6 @@ const createBasicAcademicSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([
['system', basicAcademicSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,

View File

@@ -205,7 +205,6 @@ const createBasicRedditSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([
['system', basicRedditSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,

View File

@@ -203,7 +203,6 @@ const createBasicWebSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([
['system', basicWebSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,

View File

@@ -165,7 +165,6 @@ const createBasicWolframAlphaSearchAnsweringChain = (llm: BaseChatModel) => {
ChatPromptTemplate.fromMessages([
['system', basicWolframAlphaSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,

View File

@@ -46,7 +46,6 @@ const createWritingAssistantChain = (llm: BaseChatModel) => {
ChatPromptTemplate.fromMessages([
['system', writingAssistantPrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,

View File

@@ -205,7 +205,6 @@ const createBasicYoutubeSearchAnsweringChain = (
ChatPromptTemplate.fromMessages([
['system', basicYoutubeSearchResponsePrompt],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
llm,
strParser,

View File

@@ -3,7 +3,8 @@ import express from 'express';
import cors from 'cors';
import http from 'http';
import routes from './routes';
import { getPort } from './config';
import { requireAccessKey } from './auth';
import { getAccessKey, getPort } from './config';
import logger from './utils/logger';
const port = getPort();
@@ -13,11 +14,21 @@ const server = http.createServer(app);
const corsOptions = {
origin: '*',
allowedHeaders: ['Authorization', 'Content-Type'],
};
app.use(cors(corsOptions));
if (getAccessKey()) {
app.all('/api/*', requireAccessKey);
}
app.use(express.json());
app.get('/', (_, res) => {
res.status(200).json({ status: 'ok' });
});
app.use('/api', routes);
app.get('/api', (_, res) => {
res.status(200).json({ status: 'ok' });

29
src/auth.ts Normal file
View File

@@ -0,0 +1,29 @@
import { auth } from 'google-auth-library';
import { getAccessKey } from './config';
export const requireAccessKey = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader) {
if (!checkAccessKey(authHeader)) {
return res.sendStatus(403);
}
next();
} else {
res.sendStatus(401);
}
};
export const checkAccessKey = (authHeader) => {
const token = authHeader.split(' ')[1];
return Boolean(authHeader && token === getAccessKey());
};
export const hasGCPCredentials = async () => {
try {
const credentials = await auth.getCredentials();
return Object.keys(credentials).length > 0;
} catch (e) {
return false;
}
};

View File

@@ -8,6 +8,7 @@ interface Config {
GENERAL: {
PORT: number;
SIMILARITY_MEASURE: string;
SUPER_SECRET_KEY: string;
};
API_KEYS: {
OPENAI: string;
@@ -28,18 +29,43 @@ const loadConfig = () =>
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
) as any as Config;
const loadEnv = () => {
return {
GENERAL: {
PORT: Number(process.env.PORT),
SIMILARITY_MEASURE: process.env.SIMILARITY_MEASURE,
SUPER_SECRET_KEY: process.env.SUPER_SECRET_KEY,
},
API_KEYS: {
OPENAI: process.env.OPENAI,
GROQ: process.env.GROQ,
},
API_ENDPOINTS: {
SEARXNG: process.env.SEARXNG_API_URL,
OLLAMA: process.env.OLLAMA_API_URL,
},
} as Config;
};
export const getPort = () => loadConfig().GENERAL.PORT;
export const getAccessKey = () =>
loadEnv().GENERAL.SUPER_SECRET_KEY || loadConfig().GENERAL.SUPER_SECRET_KEY;
export const getSimilarityMeasure = () =>
loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI;
export const getOpenaiApiKey = () =>
loadEnv().API_KEYS.OPENAI || loadConfig().API_KEYS.OPENAI;
export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ;
export const getGroqApiKey = () =>
loadEnv().API_KEYS.GROQ || loadConfig().API_KEYS.GROQ;
export const getSearxngApiEndpoint = () => loadConfig().API_ENDPOINTS.SEARXNG;
export const getSearxngApiEndpoint = () =>
loadEnv().API_ENDPOINTS.SEARXNG || loadConfig().API_ENDPOINTS.SEARXNG;
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
export const getOllamaApiEndpoint = () =>
loadEnv().API_ENDPOINTS.OLLAMA || loadConfig().API_ENDPOINTS.OLLAMA;
export const updateConfig = (config: RecursivePartial<Config>) => {
const currentConfig = loadConfig();

View File

@@ -1,7 +1,10 @@
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { VertexAI } from "@langchain/google-vertexai";
import { GoogleVertexAIEmbeddings } from "@langchain/community/embeddings/googlevertexai";
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { HuggingFaceTransformersEmbeddings } from './huggingfaceTransformer';
import { hasGCPCredentials } from '../auth';
import {
getGroqApiKey,
getOllamaApiEndpoint,
@@ -117,6 +120,23 @@ export const getAvailableChatModelProviders = async () => {
}
}
if (await hasGCPCredentials()) {
try {
models['vertexai'] = {
'gemini-1.5-pro (preview-0409)': new VertexAI({
temperature: 0.7,
modelName: 'gemini-1.5-pro-preview-0409',
}),
'gemini-1.0-pro (Latest)': new VertexAI({
temperature: 0.7,
modelName: 'gemini-1.0-pro',
}),
};
} catch (err) {
logger.error(`Error loading VertexAI models: ${err}`);
}
}
models['custom_openai'] = {};
return models;
@@ -167,6 +187,16 @@ export const getAvailableEmbeddingModelProviders = async () => {
}
}
if (await hasGCPCredentials()) {
try {
models['vertexai'] = {
'Text Gecko default': new GoogleVertexAIEmbeddings(),
}
} catch (err) {
logger.error(`Error loading VertexAI embeddings: ${err}`);
}
}
try {
models['local'] = {
'BGE Small': new HuggingFaceTransformersEmbeddings({

View File

@@ -9,6 +9,8 @@ import type { Embeddings } from '@langchain/core/embeddings';
import type { IncomingMessage } from 'http';
import logger from '../utils/logger';
import { ChatOpenAI } from '@langchain/openai';
import { getAccessKey } from '../config';
import { checkAccessKey } from '../auth';
export const handleConnection = async (
ws: WebSocket,
@@ -18,6 +20,20 @@ export const handleConnection = async (
const searchParams = new URL(request.url, `http://${request.headers.host}`)
.searchParams;
if (getAccessKey()) {
const securtyProtocolHeader = request.headers['sec-websocket-protocol'];
if (!checkAccessKey(securtyProtocolHeader)) {
ws.send(
JSON.stringify({
type: 'error',
data: 'Incorrect or missing authentication token.',
key: 'FAILED_AUTHORIZATION',
}),
);
ws.close();
}
}
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
getAvailableChatModelProviders(),
getAvailableEmbeddingModelProviders(),

View File

@@ -4,7 +4,6 @@ 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'],
@@ -25,20 +24,18 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html className="h-full" lang="en" suppressHydrationWarning>
<html className="h-full" lang="en">
<body className={cn('h-full', montserrat.className)}>
<ThemeProvider>
<Sidebar>{children}</Sidebar>
<Toaster
toastOptions={{
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',
},
}}
/>
</ThemeProvider>
<Sidebar>{children}</Sidebar>
<Toaster
toastOptions={{
unstyled: true,
classNames: {
toast:
'bg-[#111111] text-white rounded-lg p-4 flex flex-row items-center space-x-2',
},
}}
/>
</body>
</html>
);

View File

@@ -66,7 +66,7 @@ const Chat = ({
sendMessage={sendMessage}
/>
{!isLast && msg.role === 'assistant' && (
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="h-px w-full bg-[#1C1C1C]" />
)}
</Fragment>
);

View File

@@ -8,6 +8,8 @@ import EmptyChat from './EmptyChat';
import { toast } from 'sonner';
import { useSearchParams } from 'next/navigation';
import { getSuggestions } from '@/lib/actions';
import { clientFetch } from '@/lib/utils';
import { getAccessKey } from '@/lib/config';
export type Message = {
id: string;
@@ -37,14 +39,11 @@ const useSocket = (url: string, setIsReady: (ready: boolean) => void) => {
!embeddingModel ||
!embeddingModelProvider
) {
const providers = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/models`,
{
headers: {
'Content-Type': 'application/json',
},
const providers = await clientFetch('/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;
@@ -100,7 +99,14 @@ const useSocket = (url: string, setIsReady: (ready: boolean) => void) => {
wsURL.search = searchParams.toString();
const ws = new WebSocket(wsURL.toString());
let protocols: any[] = [];
const secretToken = getAccessKey();
if (secretToken) {
protocols = ['Authorization', `${secretToken}`];
}
const ws = new WebSocket(wsURL.toString(), protocols);
ws.onopen = () => {
console.log('[DEBUG] open');
@@ -323,7 +329,7 @@ const ChatWindow = () => {
<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]"
className="w-8 h-8 text-[#202020] animate-spin fill-[#ffffff3b]"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,5 +1,4 @@
import EmptyChatMessageInput from './EmptyChatMessageInput';
import ThemeSwitcher from './theme/Switcher';
const EmptyChat = ({
sendMessage,
@@ -11,19 +10,15 @@ const EmptyChat = ({
setFocusMode: (mode: string) => void;
}) => {
return (
<div className="relative">
<ThemeSwitcher size={17} className="absolute top-2 right-0 lg:hidden" />
<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}
/>
</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-white/70 text-3xl font-medium -mt-8">
Research begins here.
</h2>
<EmptyChatMessageInput
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}
/>
</div>
);
};

View File

@@ -1,8 +1,7 @@
import { ArrowRight } from 'lucide-react';
import { useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import CopilotToggle from './MessageInputActions/Copilot';
import Focus from './MessageInputActions/Focus';
import { CopilotToggle, Focus } from './MessageInputActions';
const EmptyChatMessageInput = ({
sendMessage,
@@ -32,12 +31,12 @@ const EmptyChatMessageInput = ({
}}
className="w-full"
>
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
<div className="flex flex-col bg-[#111111] px-5 pt-5 pb-2 rounded-lg w-full border border-[#1C1C1C]">
<TextareaAutosize
value={message}
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"
className="bg-transparent placeholder:text-white/50 text-sm text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..."
/>
<div className="flex flex-row items-center justify-between mt-4">
@@ -52,7 +51,7 @@ const EmptyChatMessageInput = ({
/>
<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"
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowRight className="bg-background" size={17} />
</button>

View File

@@ -1,6 +1,6 @@
const Layout = ({ children }: { children: React.ReactNode }) => {
return (
<main className="lg:pl-20 bg-light-primary dark:bg-dark-primary min-h-screen">
<main className="lg:pl-20 bg-[#0A0A0A] min-h-screen">
<div className="max-w-screen-lg lg:mx-auto mx-4">{children}</div>
</main>
);

View File

@@ -19,7 +19,7 @@ const Copy = ({
setCopied(true);
setTimeout(() => setCopied(false), 1000);
}}
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"
className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
>
{copied ? <Check size={18} /> : <ClipboardList size={18} />}
</button>

View File

@@ -10,7 +10,7 @@ const Rewrite = ({
return (
<button
onClick={() => rewrite(messageId)}
className="py-2 px-3 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 flex flex-row items-center space-x-1"
className="py-2 px-3 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white flex flex-row items-center space-x-1"
>
<ArrowLeftRight size={18} />
<p className="text-xs font-medium">Rewrite</p>

View File

@@ -55,7 +55,7 @@ const MessageBox = ({
message.content.replace(
regex,
(_, number) =>
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-[#1C1C1C] px-1 rounded ml-1 no-underline text-xs text-white/70 relative">${number}</a>`,
),
);
}
@@ -70,7 +70,7 @@ const MessageBox = ({
<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">
<h2 className="text-white font-medium text-3xl lg:w-9/12">
{message.content}
</h2>
</div>
@@ -85,10 +85,8 @@ const MessageBox = ({
{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>
<BookCopy className="text-white" size={20} />
<h3 className="text-white font-medium text-xl">Sources</h3>
</div>
<MessageSources sources={message.sources} />
</div>
@@ -97,27 +95,20 @@ const MessageBox = ({
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
'text-black dark:text-white',
'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-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',
)}
>
<Markdown className="prose max-w-none break-words prose-invert prose-p:leading-relaxed prose-pre:p-0 text-white text-sm md:text-base font-medium">
{parsedMessage}
</Markdown>
{loading && isLast ? null : (
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
<div className="flex flex-row items-center justify-between w-full text-white py-4 -mx-2">
<div className="flex flex-row items-center space-x-1">
{/* <button 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 text-black dark:hover:text-white">
{/* <button className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white">
<Share size={18} />
</button> */}
<Rewrite rewrite={rewrite} messageId={message.id} />
@@ -132,7 +123,7 @@ const MessageBox = ({
start();
}
}}
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"
className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
>
{speechStatus === 'started' ? (
<StopCircle size={18} />
@@ -149,8 +140,8 @@ const MessageBox = ({
message.role === 'assistant' &&
!loading && (
<>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="h-px w-full bg-[#1C1C1C]" />
<div className="flex flex-col space-y-3 text-white">
<div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 />
<h3 className="text-xl font-medium">Related</h3>
@@ -161,7 +152,7 @@ const MessageBox = ({
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 className="h-px w-full bg-[#1C1C1C]" />
<div
onClick={() => {
sendMessage(suggestion);
@@ -171,10 +162,7 @@ const MessageBox = ({
<p className="transition duration-200 hover:text-[#24A0ED]">
{suggestion}
</p>
<Plus
size={20}
className="text-[#24A0ED] flex-shrink-0"
/>
<Plus size={20} className="text-[#24A0ED]" />
</div>
</div>
))}

View File

@@ -1,9 +1,9 @@
const MessageBoxLoading = () => {
return (
<div className="flex flex-col space-y-2 w-full lg:w-9/12 bg-light-primary dark:bg-dark-primary animate-pulse rounded-lg p-3">
<div className="h-2 rounded-full w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="h-2 rounded-full w-9/12 bg-light-secondary dark:bg-dark-secondary" />
<div className="h-2 rounded-full w-10/12 bg-light-secondary dark:bg-dark-secondary" />
<div className="flex flex-col space-y-2 w-full lg:w-9/12 bg-[#111111] animate-pulse rounded-lg p-3">
<div className="h-2 rounded-full w-full bg-[#1c1c1c]" />
<div className="h-2 rounded-full w-9/12 bg-[#1c1c1c]" />
<div className="h-2 rounded-full w-10/12 bg-[#1c1c1c]" />
</div>
);
};

View File

@@ -2,8 +2,7 @@ import { cn } from '@/lib/utils';
import { ArrowUp } from 'lucide-react';
import { useEffect, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot';
import { Attach, CopilotToggle } from './MessageInputActions';
const MessageInput = ({
sendMessage,
@@ -41,7 +40,7 @@ const MessageInput = ({
}
}}
className={cn(
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200',
'bg-[#111111] p-4 flex items-center overflow-hidden border border-[#1C1C1C]',
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
)}
>
@@ -52,7 +51,7 @@ const MessageInput = ({
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"
className="transition bg-transparent placeholder:text-white/50 placeholder:text-sm text-sm 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' && (
@@ -63,7 +62,7 @@ const MessageInput = ({
/>
<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"
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>
@@ -79,7 +78,7 @@ const MessageInput = ({
/>
<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"
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>

View File

@@ -1,16 +1,28 @@
import {
BadgePercent,
ChevronDown,
CopyPlus,
Globe,
Pencil,
ScanEye,
SwatchBook,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Popover, Transition } from '@headlessui/react';
import { Popover, Switch, Transition } from '@headlessui/react';
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
import { Fragment } from 'react';
export const Attach = () => {
return (
<button
type="button"
className="p-2 text-white/50 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
>
<CopyPlus />
</button>
);
};
const focusModes = [
{
key: 'webSearch',
@@ -62,7 +74,7 @@ const focusModes = [
},
];
const Focus = ({
export const Focus = ({
focusMode,
setFocusMode,
}: {
@@ -73,7 +85,7 @@ const Focus = ({
<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"
className="p-2 text-white/50 rounded-xl hover:bg-[#1c1c1c] active:scale-95 transition duration-200 hover:text-white"
>
{focusMode !== 'webSearch' ? (
<div className="flex flex-row items-center space-x-1">
@@ -97,7 +109,7 @@ const Focus = ({
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">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-[#0A0A0A] border rounded-lg border-[#1c1c1c] w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto">
{focusModes.map((mode, i) => (
<Popover.Button
onClick={() => setFocusMode(mode.key)}
@@ -105,24 +117,20 @@ const Focus = ({
className={cn(
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
focusMode === mode.key
? 'bg-light-secondary dark:bg-dark-secondary'
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
? 'bg-[#111111]'
: 'hover:bg-[#111111]',
)}
>
<div
className={cn(
'flex flex-row items-center space-x-1',
focusMode === mode.key
? 'text-[#24A0ED]'
: 'text-black dark:text-white',
focusMode === mode.key ? 'text-[#24A0ED]' : '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-white/70 text-xs">{mode.description}</p>
</Popover.Button>
))}
</div>
@@ -132,4 +140,41 @@ const Focus = ({
);
};
export default Focus;
export const CopilotToggle = ({
copilotEnabled,
setCopilotEnabled,
}: {
copilotEnabled: boolean;
setCopilotEnabled: (enabled: boolean) => void;
}) => {
return (
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
<Switch
checked={copilotEnabled}
onChange={setCopilotEnabled}
className="bg-[#111111] border border-[#1C1C1C] relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
>
<span className="sr-only">Copilot</span>
<span
className={cn(
copilotEnabled
? 'translate-x-6 bg-[#24A0ED]'
: 'translate-x-1 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',
copilotEnabled
? 'text-[#24A0ED]'
: 'text-white/50 group-hover:text-white',
)}
>
Copilot
</p>
</div>
);
};

View File

@@ -1,14 +0,0 @@
import { CopyPlus } from 'lucide-react';
const Attach = () => {
return (
<button
type="button"
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
>
<CopyPlus />
</button>
);
};
export default Attach;

View File

@@ -1,43 +0,0 @@
import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react';
const CopilotToggle = ({
copilotEnabled,
setCopilotEnabled,
}: {
copilotEnabled: boolean;
setCopilotEnabled: (enabled: boolean) => void;
}) => {
return (
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
<Switch
checked={copilotEnabled}
onChange={setCopilotEnabled}
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
>
<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',
)}
/>
</Switch>
<p
onClick={() => setCopilotEnabled(!copilotEnabled)}
className={cn(
'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',
)}
>
Copilot
</p>
</div>
);
};
export default CopilotToggle;

View File

@@ -20,12 +20,12 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
{sources.slice(0, 3).map((source, i) => (
<a
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
key={i}
href={source.metadata.url}
target="_blank"
>
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
<p className="text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
{source.metadata.title}
</p>
<div className="flex flex-row items-center justify-between">
@@ -37,12 +37,12 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
alt="favicon"
className="rounded-lg h-4 w-4"
/>
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
<p className="text-xs text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
{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">
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
<div className="flex flex-row items-center space-x-1 text-white/50 text-xs">
<div className="bg-white/50 h-[4px] w-[4px] rounded-full" />
<span>{i + 1}</span>
</div>
</div>
@@ -51,7 +51,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
{sources.length > 3 && (
<button
onClick={openModal}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 rounded-lg px-4 py-2 flex flex-col justify-between space-y-2"
>
<div className="flex flex-row items-center space-x-1">
{sources.slice(3, 6).map((source, i) => (
@@ -65,7 +65,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
/>
))}
</div>
<p className="text-xs text-black/50 dark:text-white/50">
<p className="text-xs text-white/50">
View {sources.length - 3} more
</p>
</button>
@@ -83,19 +83,19 @@ 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">
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-[#111111] border border-[#1c1c1c] p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title className="text-lg font-medium leading-6 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
className="bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 border border-light-200 dark:border-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
className="bg-[#111111] hover:bg-[#1c1c1c] border border-[#1c1c1c] transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
key={i}
href={source.metadata.url}
target="_blank"
>
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
<p className="text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
{source.metadata.title}
</p>
<div className="flex flex-row items-center justify-between">
@@ -107,15 +107,15 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
alt="favicon"
className="rounded-lg h-4 w-4"
/>
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
<p className="text-xs text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
{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">
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
<div className="flex flex-row items-center space-x-1 text-white/50 text-xs">
<div className="bg-white/50 h-[4px] w-[4px] rounded-full" />
<span>{i + 1}</span>
</div>
</div>

View File

@@ -2,7 +2,6 @@ import { Clock, Edit, Share, Trash } from 'lucide-react';
import { Message } from './ChatWindow';
import { useEffect, useState } from 'react';
import { formatTimeDifference } from '@/lib/utils';
import ThemeSwitcher from './theme/Switcher';
const Navbar = ({ messages }: { messages: Message[] }) => {
const [title, setTitle] = useState<string>('');
@@ -39,7 +38,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">
<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-white/70 border-b bg-[#0A0A0A] border-[#1C1C1C]">
<Edit
size={17}
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
@@ -49,9 +48,6 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
<p className="text-xs">{timeAgo} ago</p>
</div>
<p className="hidden lg:flex">{title}</p>
<ThemeSwitcher size={17} className="lg:hidden ml-auto mr-4" />
<div className="flex flex-row items-center space-x-4">
<Share
size={17}

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
import { clientFetch } from '@/lib/utils';
type Image = {
url: string;
@@ -33,21 +34,18 @@ const SearchImages = ({
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 clientFetch('/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();
@@ -62,7 +60,7 @@ const SearchImages = ({
);
setLoading(false);
}}
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
className="border border-dashed border-[#1C1C1C] hover:bg-[#1c1c1c] active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full"
>
<div className="flex flex-row items-center space-x-2">
<ImagesIcon size={17} />
@@ -76,7 +74,7 @@ const SearchImages = ({
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
className="bg-[#1C1C1C] h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
/>
))}
</div>
@@ -120,7 +118,7 @@ const SearchImages = ({
{images.length > 4 && (
<button
onClick={() => setOpen(true)}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
>
<div className="flex flex-row items-center space-x-1">
{images.slice(3, 6).map((image, i) => (
@@ -132,7 +130,7 @@ const SearchImages = ({
/>
))}
</div>
<p className="text-black/70 dark:text-white/70 text-xs">
<p className="text-white/70 text-xs">
View {images.length - 3} more
</p>
</button>

View File

@@ -4,6 +4,7 @@ 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 { clientFetch } from '@/lib/utils';
type Video = {
url: string;
@@ -46,21 +47,18 @@ const Searchvideos = ({
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 clientFetch('/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();
@@ -77,7 +75,7 @@ const Searchvideos = ({
);
setLoading(false);
}}
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
className="border border-dashed border-[#1C1C1C] hover:bg-[#1c1c1c] active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full"
>
<div className="flex flex-row items-center space-x-2">
<VideoIcon size={17} />
@@ -91,7 +89,7 @@ const Searchvideos = ({
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
className="bg-[#1C1C1C] h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
/>
))}
</div>
@@ -118,7 +116,7 @@ const Searchvideos = ({
alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg"
/>
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} />
<p className="text-xs">Video</p>
</div>
@@ -142,7 +140,7 @@ const Searchvideos = ({
alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg"
/>
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} />
<p className="text-xs">Video</p>
</div>
@@ -151,7 +149,7 @@ const Searchvideos = ({
{videos.length > 4 && (
<button
onClick={() => setOpen(true)}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
>
<div className="flex flex-row items-center space-x-1">
{videos.slice(3, 6).map((video, i) => (
@@ -163,7 +161,7 @@ const Searchvideos = ({
/>
))}
</div>
<p className="text-black/70 dark:text-white/70 text-xs">
<p className="text-white/70 text-xs">
View {videos.length - 3} more
</p>
</button>

View File

@@ -1,52 +1,7 @@
import { cn } from '@/lib/utils';
import { Dialog, Transition } from '@headlessui/react';
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
import React, {
Fragment,
useEffect,
useMemo,
useState,
type SelectHTMLAttributes,
} from 'react';
import ThemeSwitcher from './theme/Switcher';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = ({ className, ...restProps }: InputProps) => {
return (
<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',
className,
)}
/>
);
};
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
options: { value: string; label: string; disabled?: boolean }[];
}
export const Select = ({ className, options, ...restProps }: SelectProps) => {
return (
<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',
className,
)}
>
{options.map(({ label, value, disabled }) => {
return (
<option key={value} value={value} disabled={disabled}>
{label}
</option>
);
})}
</select>
);
};
import React, { Fragment, useEffect, useState } from 'react';
import { clientFetch } from '@/lib/utils';
interface SettingsType {
chatModelProviders: {
@@ -88,7 +43,7 @@ const SettingsDialog = ({
if (isOpen) {
const fetchConfig = async () => {
setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
const res = await clientFetch('/config', {
headers: {
'Content-Type': 'application/json',
},
@@ -148,7 +103,7 @@ const SettingsDialog = ({
setIsUpdating(true);
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
await clientFetch('/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -191,7 +146,7 @@ const SettingsDialog = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-white/50 dark:bg-black/50" />
<div className="fixed inset-0 bg-black/50" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
@@ -204,24 +159,18 @@ 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">
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-[#111111] border border-[#1c1c1c] p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title className="text-xl font-medium leading-6 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>
<ThemeSwitcher />
</div>
{config.chatModelProviders && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
<p className="text-white/70 text-sm">
Chat model Provider
</p>
<Select
<select
value={selectedChatModelProvider ?? undefined}
onChange={(e) => {
setSelectedChatModelProvider(e.target.value);
@@ -229,99 +178,97 @@ const SettingsDialog = ({
config.chatModelProviders[e.target.value][0],
);
}}
options={Object.keys(config.chatModelProviders).map(
(provider) => ({
value: provider,
label:
provider.charAt(0).toUpperCase() +
provider.slice(1),
}),
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
>
{Object.keys(config.chatModelProviders).map(
(provider) => (
<option key={provider} value={provider}>
{provider.charAt(0).toUpperCase() +
provider.slice(1)}
</option>
),
)}
/>
</select>
</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
<p className="text-white/70 text-sm">Chat Model</p>
<select
value={selectedChatModel ?? undefined}
onChange={(e) =>
setSelectedChatModel(e.target.value)
}
options={(() => {
const chatModelProvider =
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
>
{config.chatModelProviders[
selectedChatModelProvider
] ? (
config.chatModelProviders[
selectedChatModelProvider
].length > 0 ? (
config.chatModelProviders[
selectedChatModelProvider
];
return chatModelProvider
? chatModelProvider.length > 0
? chatModelProvider.map((model) => ({
value: model,
label: model,
}))
: [
{
value: '',
label: 'No models available',
disabled: true,
},
]
: [
{
value: '',
label:
'Invalid provider, please check backend logs',
disabled: true,
},
];
})()}
/>
].map((model) => (
<option key={model} value={model}>
{model}
</option>
))
) : (
<option value="" disabled>
No models available
</option>
)
) : (
<option value="" disabled>
Invalid provider, please check backend logs
</option>
)}
</select>
</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
<p className="text-white/70 text-sm">Model name</p>
<input
type="text"
placeholder="Model name"
defaultValue={selectedChatModel!}
onChange={(e) =>
setSelectedChatModel(e.target.value)
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
<p className="text-white/70 text-sm">
Custom OpenAI API Key
</p>
<Input
<input
type="text"
placeholder="Custom OpenAI API Key"
defaultValue={customOpenAIApiKey!}
onChange={(e) =>
setCustomOpenAIApiKey(e.target.value)
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
<p className="text-white/70 text-sm">
Custom OpenAI Base URL
</p>
<Input
<input
type="text"
placeholder="Custom OpenAI Base URL"
defaultValue={customOpenAIBaseURL!}
onChange={(e) =>
setCustomOpenAIBaseURL(e.target.value)
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
</>
@@ -329,10 +276,10 @@ const SettingsDialog = ({
{/* Embedding models */}
{config.embeddingModelProviders && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
<p className="text-white/70 text-sm">
Embedding model Provider
</p>
<Select
<select
value={selectedEmbeddingModelProvider ?? undefined}
onChange={(e) => {
setSelectedEmbeddingModelProvider(e.target.value);
@@ -340,63 +287,58 @@ const SettingsDialog = ({
config.embeddingModelProviders[e.target.value][0],
);
}}
options={Object.keys(
config.embeddingModelProviders,
).map((provider) => ({
label:
provider.charAt(0).toUpperCase() +
provider.slice(1),
value: provider,
}))}
/>
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
>
{Object.keys(config.embeddingModelProviders).map(
(provider) => (
<option key={provider} value={provider}>
{provider.charAt(0).toUpperCase() +
provider.slice(1)}
</option>
),
)}
</select>
</div>
)}
{selectedEmbeddingModelProvider && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Embedding Model
</p>
<Select
<p className="text-white/70 text-sm">Embedding Model</p>
<select
value={selectedEmbeddingModel ?? undefined}
onChange={(e) =>
setSelectedEmbeddingModel(e.target.value)
}
options={(() => {
const embeddingModelProvider =
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
>
{config.embeddingModelProviders[
selectedEmbeddingModelProvider
] ? (
config.embeddingModelProviders[
selectedEmbeddingModelProvider
].length > 0 ? (
config.embeddingModelProviders[
selectedEmbeddingModelProvider
];
return embeddingModelProvider
? embeddingModelProvider.length > 0
? embeddingModelProvider.map((model) => ({
label: model,
value: model,
}))
: [
{
label: 'No embedding models available',
value: '',
disabled: true,
},
]
: [
{
label:
'Invalid provider, please check backend logs',
value: '',
disabled: true,
},
];
})()}
/>
].map((model) => (
<option key={model} value={model}>
{model}
</option>
))
) : (
<option value="" disabled selected>
No embedding models available
</option>
)
) : (
<option value="" disabled selected>
Invalid provider, please check backend logs
</option>
)}
</select>
</div>
)}
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
OpenAI API Key
</p>
<Input
<p className="text-white/70 text-sm">OpenAI API Key</p>
<input
type="text"
placeholder="OpenAI API Key"
defaultValue={config.openaiApiKey}
@@ -406,13 +348,12 @@ const SettingsDialog = ({
openaiApiKey: e.target.value,
})
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Ollama API URL
</p>
<Input
<p className="text-white/70 text-sm">Ollama API URL</p>
<input
type="text"
placeholder="Ollama API URL"
defaultValue={config.ollamaApiUrl}
@@ -422,13 +363,12 @@ const SettingsDialog = ({
ollamaApiUrl: e.target.value,
})
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
GROQ API Key
</p>
<Input
<p className="text-white/70 text-sm">GROQ API Key</p>
<input
type="text"
placeholder="GROQ API Key"
defaultValue={config.groqApiKey}
@@ -438,17 +378,18 @@ const SettingsDialog = ({
groqApiKey: e.target.value,
})
}
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
/>
</div>
</div>
)}
{isLoading && (
<div className="w-full flex items-center justify-center mt-6 text-black/70 dark:text-white/70 py-6">
<div className="w-full flex items-center justify-center mt-6 text-white/70 py-6">
<RefreshCcw className="animate-spin" />
</div>
)}
<div className="w-full mt-6 space-y-2">
<p className="text-xs text-black/50 dark:text-white/50">
<p className="text-xs text-white/50">
We&apos;ll refresh the page after updating the settings.
</p>
<button

View File

@@ -4,16 +4,10 @@ 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 React, { Fragment, useState } from 'react';
import Layout from './Layout';
import { Dialog, Transition } from '@headlessui/react';
import SettingsDialog from './SettingsDialog';
import ThemeSwitcher from './theme/Switcher';
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
return (
<div className="flex flex-col items-center gap-y-3 w-full">{children}</div>
);
};
const Sidebar = ({ children }: { children: React.ReactNode }) => {
const segments = useSelectedLayoutSegments();
@@ -44,35 +38,31 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col">
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-light-secondary dark:bg-dark-secondary px-2 py-8">
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-[#111111] px-2 py-8">
<a href="/">
<SquarePen className="cursor-pointer" />
<SquarePen className="text-white cursor-pointer" />
</a>
<VerticalIconContainer>
<div className="flex flex-col items-center gap-y-3 w-full">
{navLinks.map((link, i) => (
<Link
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-white/10 hover:text-white duration-150 transition w-full py-2 rounded-lg',
link.active ? 'text-white' : 'text-white/70',
)}
>
<link.icon />
{link.active && (
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-black dark:bg-white" />
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-white" />
)}
</Link>
))}
</VerticalIconContainer>
</div>
<Settings
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
className="cursor-pointer"
className="text-white cursor-pointer"
/>
<SettingsDialog
isOpen={isSettingsOpen}
setIsOpen={setIsSettingsOpen}
@@ -80,20 +70,18 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
</div>
</div>
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-light-primary dark:bg-dark-primary px-4 py-4 shadow-sm lg:hidden">
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-[#111111] px-4 py-4 shadow-sm lg:hidden">
{navLinks.map((link, i) => (
<Link
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',
link.active ? 'text-white' : 'text-white/70',
)}
>
{link.active && (
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-white" />
)}
<link.icon />
<p className="text-xs">{link.label}</p>

View File

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

View File

@@ -1,62 +0,0 @@
'use client';
import { useTheme } from 'next-themes';
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
import { Select } from '../SettingsDialog';
type Theme = 'dark' | 'light' | 'system';
const ThemeSwitcher = ({ className }: { className?: string }) => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
const isTheme = useCallback((t: Theme) => t === theme, [theme]);
const handleThemeSwitch = (theme: Theme) => {
setTheme(theme);
};
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (isTheme('system')) {
const preferDarkScheme = window.matchMedia(
'(prefers-color-scheme: dark)',
);
const detectThemeChange = (event: MediaQueryListEvent) => {
const theme: Theme = event.matches ? 'dark' : 'light';
setTheme(theme);
};
preferDarkScheme.addEventListener('change', detectThemeChange);
return () => {
preferDarkScheme.removeEventListener('change', detectThemeChange);
};
}
}, [isTheme, setTheme, theme]);
// Avoid Hydration Mismatch
if (!mounted) {
return null;
}
return (
<Select
className={className}
value={theme}
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
]}
/>
);
};
export default ThemeSwitcher;

View File

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

22
ui/lib/config.ts Normal file
View File

@@ -0,0 +1,22 @@
interface Config {
GENERAL: {
NEXT_PUBLIC_SUPER_SECRET_KEY: string;
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_WS_URL: string;
};
}
const loadEnv = () => {
return {
GENERAL: {
NEXT_PUBLIC_SUPER_SECRET_KEY: process.env.NEXT_PUBLIC_SUPER_SECRET_KEY!,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL!,
},
} as Config;
};
export const getAccessKey = () =>
loadEnv().GENERAL.NEXT_PUBLIC_SUPER_SECRET_KEY;
export const getBackendURL = () => loadEnv().GENERAL.NEXT_PUBLIC_API_URL;

View File

@@ -1,5 +1,6 @@
import clsx, { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { getAccessKey, getBackendURL } from './config';
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
@@ -19,3 +20,20 @@ export const formatTimeDifference = (date1: Date, date2: Date): string => {
else
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`;
};
export const clientFetch = async (path: string, payload: any): Promise<any> => {
let headers = payload.headers;
const url = `${getBackendURL()}${path}`;
const secretToken = getAccessKey();
if (secretToken) {
if (headers == null) {
headers = {};
}
headers['Authorization'] = `Bearer ${secretToken}`;
payload.headers = headers;
}
return await fetch(url, payload);
};

View File

@@ -1,6 +1,6 @@
{
"name": "perplexica-frontend",
"version": "1.6.0",
"version": "1.5.0",
"license": "MIT",
"author": "ItzCrazyKns",
"scripts": {
@@ -20,7 +20,6 @@
"lucide-react": "^0.363.0",
"markdown-to-jsx": "^7.4.5",
"next": "14.1.4",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"react-text-to-speech": "^0.14.5",

View File

@@ -1,17 +1,4 @@
import type { Config } from 'tailwindcss';
import type { DefaultColors } from 'tailwindcss/types/generated/colors';
const themeDark = (colors: DefaultColors) => ({
50: '#0a0a0a',
100: '#111111',
200: '#1c1c1c',
});
const themeLight = (colors: DefaultColors) => ({
50: '#fcfcf9',
100: '#f3f3ee',
200: '#e8e8e3',
});
const config: Config = {
content: [
@@ -19,33 +6,8 @@ const config: Config = {
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class',
theme: {
extend: {
borderColor: ({ colors }) => {
return {
light: themeLight(colors),
dark: themeDark(colors),
};
},
colors: ({ colors }) => {
const colorsDark = themeDark(colors);
const colorsLight = themeLight(colors);
return {
dark: {
primary: colorsDark[50],
secondary: colorsDark[100],
...colorsDark,
},
light: {
primary: colorsLight[50],
secondary: colorsLight[100],
...colorsLight,
},
};
},
},
extend: {},
},
plugins: [require('@tailwindcss/typography')],
};

View File

@@ -2244,11 +2244,6 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next-themes@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==
next@14.1.4:
version "14.1.4"
resolved "https://registry.yarnpkg.com/next/-/next-14.1.4.tgz#203310f7310578563fd5c961f0db4729ce7a502d"
@@ -2859,16 +2854,8 @@ streamsearch@^1.1.0:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -2932,14 +2919,7 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==

192
yarn.lock
View File

@@ -79,6 +79,24 @@
uuid "^9.0.0"
zod "^3.22.3"
"@langchain/core@>0.1.56 <0.3.0", "@langchain/core@>0.1.56 <0.3.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.2.0.tgz#19c6374a5ad80daf8e14cb58582bc988109a1403"
integrity sha512-UbCJUp9eh2JXd9AW/vhPbTgtZoMgTqJgSan5Wf/EP27X8JM65lWdCOpJW+gHyBXvabbyrZz3/EGaptTUL5gutw==
dependencies:
ansi-styles "^5.0.0"
camelcase "6"
decamelize "1.2.0"
js-tiktoken "^1.0.12"
langsmith "~0.1.7"
ml-distance "^4.0.0"
mustache "^4.2.0"
p-queue "^6.6.2"
p-retry "4"
uuid "^9.0.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/core@~0.1.44", "@langchain/core@~0.1.45":
version "0.1.52"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.1.52.tgz#7619310b83ffa841628efe2e1eda873ca714d068"
@@ -96,6 +114,32 @@
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/google-common@~0.0.15":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/google-common/-/google-common-0.0.16.tgz#e2ff43eaebcf7bea84a067f8bdaf7f01e23bc1c0"
integrity sha512-eQMdqEYfzcavkE5Cpk7LCUlFx2Gb+skNZci/DlS2zot4XCSVg8QDIYOkL+PrXtTZBsp36SyOnNfzHUzdbU8cPA==
dependencies:
"@langchain/core" ">0.1.56 <0.3.0"
uuid "^9.0.0"
zod-to-json-schema "^3.22.4"
"@langchain/google-gauth@~0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/google-gauth/-/google-gauth-0.0.16.tgz#164c865c0d6363385f3375e54e2ed66c6ed06cfd"
integrity sha512-mp68iw/XA/lbBwh8+6vV7FFsibP595mt+OZdEFU9QewpUv99YVHH1FT+mNoQAI9p6uZpSHYQ3Iip70nIU976sw==
dependencies:
"@langchain/core" ">0.1.56 <0.3.0"
"@langchain/google-common" "~0.0.15"
google-auth-library "^8.9.0"
"@langchain/google-vertexai@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@langchain/google-vertexai/-/google-vertexai-0.0.16.tgz#388ddf21dc9537d4632acc5c046583fe9ac8022a"
integrity sha512-tJTyPxg3vYSqhNyqx6/UViPNdn3NPeZL29JqNen26x/w4JYYMpde0Dm20KCd5TCsbdUfrkk7tMyJZjr2e30jMg==
dependencies:
"@langchain/core" ">0.1.56 <0.3.0"
"@langchain/google-gauth" "~0.0.16"
"@langchain/openai@^0.0.25", "@langchain/openai@~0.0.19":
version "0.0.25"
resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.25.tgz#8332abea1e3acb9b1169f90636e518c0ee90622e"
@@ -357,6 +401,13 @@ acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agentkeepalive@^4.2.1:
version "4.5.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923"
@@ -392,6 +443,11 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
arrify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
async@^3.2.3:
version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
@@ -459,11 +515,16 @@ base-64@^0.1.0:
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
base64-js@^1.3.1, base64-js@^1.5.1:
base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bignumber.js@^9.0.0:
version "9.1.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c"
integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==
binary-extensions@^2.0.0, binary-extensions@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@@ -516,6 +577,11 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@@ -716,7 +782,7 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@^4:
debug@4, debug@^4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -787,6 +853,13 @@ dotenv@^16.4.5:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -888,11 +961,21 @@ express@^4.19.2:
utils-merge "1.0.1"
vary "~1.1.2"
extend@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
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"
integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
fast-text-encoding@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==
fecha@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
@@ -985,6 +1068,24 @@ function-bind@^1.1.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
gaxios@^5.0.0, gaxios@^5.0.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013"
integrity sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==
dependencies:
extend "^3.0.2"
https-proxy-agent "^5.0.0"
is-stream "^2.0.0"
node-fetch "^2.6.9"
gcp-metadata@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408"
integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==
dependencies:
gaxios "^5.0.0"
json-bigint "^1.0.0"
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
@@ -1008,6 +1109,28 @@ glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
google-auth-library@^8.9.0:
version "8.9.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.9.0.tgz#15a271eb2ec35d43b81deb72211bd61b1ef14dd0"
integrity sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^5.0.0"
gcp-metadata "^5.3.0"
gtoken "^6.1.0"
jws "^4.0.0"
lru-cache "^6.0.0"
google-p12-pem@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a"
integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==
dependencies:
node-forge "^1.3.1"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@@ -1015,6 +1138,15 @@ gopd@^1.0.1:
dependencies:
get-intrinsic "^1.1.3"
gtoken@^6.1.0:
version "6.1.2"
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc"
integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==
dependencies:
gaxios "^5.0.1"
google-p12-pem "^4.0.0"
jws "^4.0.0"
guid-typescript@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc"
@@ -1060,6 +1192,14 @@ http-errors@2.0.0:
statuses "2.0.1"
toidentifier "1.0.1"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@@ -1143,6 +1283,13 @@ is-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
js-tiktoken@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.12.tgz#af0f5cf58e5e7318240d050c8413234019424211"
integrity sha512-L7wURW1fH9Qaext0VzaUDpFGVQgjkdE3Dgsy9/+yXyGEpBKnylTd0mU0bfbNkKDlXRb6TEsZkwuflu1B8uQbJQ==
dependencies:
base64-js "^1.5.1"
js-tiktoken@^1.0.7, js-tiktoken@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.10.tgz#2b343ec169399dcee8f9ef9807dbd4fafd3b30dc"
@@ -1157,11 +1304,35 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
jsonpointer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
safe-buffer "^5.0.1"
kuler@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
@@ -1349,6 +1520,11 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mustache@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
@@ -1376,13 +1552,18 @@ node-domexception@1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^2.6.7:
node-fetch@^2.6.7, node-fetch@^2.6.9:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies:
whatwg-url "^5.0.0"
node-forge@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
nodemon@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.0.tgz#ff7394f2450eb6a5e96fe4180acd5176b29799c9"
@@ -2079,6 +2260,11 @@ zod-to-json-schema@^3.22.3:
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz#3646e81cfc318dbad2a22519e5ce661615418673"
integrity sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q==
zod-to-json-schema@^3.22.4:
version "3.23.0"
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.0.tgz#4fc60e88d3c709eedbfaae3f92f8a7bf786469f2"
integrity sha512-az0uJ243PxsRIa2x1WmNE/pnuA05gUq/JB8Lwe1EDCCL/Fz9MgjYQ0fPlyc2Tcv6aF2ZA7WM5TWaRZVEFaAIag==
zod@^3.22.3, zod@^3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"