Compare commits

..

23 Commits

Author SHA1 Message Date
realies
2a8cc6e1b6 Merge a661450633 into 89b5229ce9 2025-03-08 04:41:32 +00:00
realies
a661450633 fix(docker-usage): single image setup 2025-03-08 04:41:27 +00:00
realies
b3b8a05bd2 fix(docker-usage): init 2025-03-08 02:32:57 +00:00
ItzCrazyKns
89b5229ce9 Merge pull request #663 from ericdachen/master
Update Readme
2025-03-05 11:11:07 +05:30
ItzCrazyKns
7756340dd9 Update README.md 2025-03-05 11:09:19 +05:30
ItzCrazyKns
bbd2e9c359 feat(readme): update warp banner 2025-03-05 11:05:25 +05:30
ItzCrazyKns
a32eb1dda3 feat(readme): lint & beautify, update anchor URL 2025-03-05 10:55:02 +05:30
Eric Chen
aa834f7f04 Update README.md 2025-03-04 14:45:10 -05:00
Eric Chen
064c0fbe42 Update README.md 2025-03-04 12:16:10 -05:00
Eric Chen
bf4cf8eaeb Update README.md 2025-03-04 12:14:17 -05:00
ItzCrazyKns
a24992a3db Merge pull request #655 from ShortCipher5/patch-1
chore: Add Sealos 1-click deployment
2025-03-01 21:56:01 +05:30
ShortCipher5
d584067bb1 Update README.md 2025-02-27 23:26:45 -08:00
ItzCrazyKns
df4350f966 Merge branch 'master' of https://github.com/ItzCrazyKns/Perplexica 2025-02-26 10:40:34 +05:30
ItzCrazyKns
652ca2fdf4 Merge pull request #649 from QuietlyChan/fix/light-theme-ui-bug
fix(ui): improve dark mode text color for attachment buttons
2025-02-26 10:36:41 +05:30
QuietlyChan
216576128d fix(ui): update attachment text color for light and dark modes 2025-02-25 19:26:58 +08:00
QuietlyChan
bb3f180583 fix(ui): improve dark mode text color for attachment buttons 2025-02-25 17:26:33 +08:00
ItzCrazyKns
4d24d73161 Merge pull request #631 from user1007017/patch-1
Update README.md grammatical error
2025-02-20 10:37:33 +05:30
wellCh4n
2e166c217b fix(MessageBox): break too long message title 2025-02-19 10:34:51 +08:00
ItzCrazyKns
4c73caadf6 feat(custom-openai): save live changes 2025-02-17 16:24:41 +05:30
user1007017
5f0b87f4a9 Update README.md 2025-02-15 19:06:46 +01:00
ItzCrazyKns
115e6b2a71 Merge branch 'master' of https://github.com/ItzCrazyKns/Perplexica 2025-02-15 12:52:30 +05:30
ItzCrazyKns
a5c79c92ed feat(settings): add embedding provider settings 2025-02-15 12:52:27 +05:30
ItzCrazyKns
db3cea446e Update UPDATING.md 2025-02-15 12:33:43 +05:30
32 changed files with 551 additions and 268 deletions

View File

@@ -10,9 +10,6 @@ on:
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
service: [backend, app]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -36,38 +33,24 @@ jobs:
id: version id: version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image for ${{ matrix.service }} - name: Build and push Docker image (latest)
if: github.ref == 'refs/heads/master' && github.event_name == 'push' if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: | run: |
docker buildx create --use docker buildx create --use
if [[ "${{ matrix.service }}" == "backend" ]]; then \ docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
DOCKERFILE=backend.dockerfile; \ --cache-from=type=registry,ref=itzcrazykns1337/perplexica:latest \
IMAGE_NAME=perplexica-backend; \
else \
DOCKERFILE=app.dockerfile; \
IMAGE_NAME=perplexica-frontend; \
fi
docker buildx build --platform linux/amd64,linux/arm64 \
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:main \
--cache-to=type=inline \ --cache-to=type=inline \
-f $DOCKERFILE \ -f docker/Dockerfile \
-t itzcrazykns1337/${IMAGE_NAME}:main \ -t itzcrazykns1337/perplexica:latest \
--push . --push .
- name: Build and push release Docker image for ${{ matrix.service }} - name: Build and push Docker image (release)
if: github.event_name == 'release' if: github.event_name == 'release'
run: | run: |
docker buildx create --use docker buildx create --use
if [[ "${{ matrix.service }}" == "backend" ]]; then \ docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
DOCKERFILE=backend.dockerfile; \ --cache-from=type=registry,ref=itzcrazykns1337/perplexica:${{ env.RELEASE_VERSION }} \
IMAGE_NAME=perplexica-backend; \
else \
DOCKERFILE=app.dockerfile; \
IMAGE_NAME=perplexica-frontend; \
fi
docker buildx build --platform linux/amd64,linux/arm64 \
--cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \
--cache-to=type=inline \ --cache-to=type=inline \
-f $DOCKERFILE \ -f docker/Dockerfile \
-t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \ -t itzcrazykns1337/perplexica:${{ env.RELEASE_VERSION }} \
--push . --push .

View File

@@ -1,7 +1,22 @@
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc --> # 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
[![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT) <div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://www.warp.dev/perplexica">
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b">
</a>
### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica)
[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica)
</div>
<hr/>
[![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT)
![preview](.assets/perplexica-screenshot.png?) ![preview](.assets/perplexica-screenshot.png?)
@@ -11,12 +26,13 @@
- [Preview](#preview) - [Preview](#preview)
- [Features](#features) - [Features](#features)
- [Installation](#installation) - [Installation](#installation)
- [Getting Started with Docker (Recommended)](#getting-started-with-docker-recommended) - [Docker Installation (Recommended)](#docker-installation-recommended)
- [Non-Docker Installation](#non-docker-installation) - [Non-Docker Installation](#non-docker-installation)
- [Nginx Reverse Proxy](#nginx-reverse-proxy)
- [Ollama Connection Errors](#ollama-connection-errors) - [Ollama Connection Errors](#ollama-connection-errors)
- [Using as a Search Engine](#using-as-a-search-engine) - [Using as a Search Engine](#using-as-a-search-engine)
- [Using Perplexica's API](#using-perplexicas-api) - [Using Perplexica's API](#using-perplexicas-api)
- [Expose Perplexica to a network](#expose-perplexica-to-network) - [Expose Perplexica to a Network](#expose-perplexica-to-a-network)
- [One-Click Deployment](#one-click-deployment) - [One-Click Deployment](#one-click-deployment)
- [Upcoming Features](#upcoming-features) - [Upcoming Features](#upcoming-features)
- [Support Us](#support-us) - [Support Us](#support-us)
@@ -44,7 +60,7 @@ Want to know more about its architecture and how it works? You can read it [here
- **Normal Mode:** Processes your query and performs a web search. - **Normal Mode:** Processes your query and performs a web search.
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes: - **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
- **All Mode:** Searches the entire web to find the best results. - **All Mode:** Searches the entire web to find the best results.
- **Writing Assistant Mode:** Helpful for writing tasks that does not require searching the web. - **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web.
- **Academic Search Mode:** Finds articles and papers, ideal for academic research. - **Academic Search Mode:** Finds articles and papers, ideal for academic research.
- **YouTube Search Mode:** Finds YouTube videos based on the search query. - **YouTube Search Mode:** Finds YouTube videos based on the search query.
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha. - **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.
@@ -56,9 +72,9 @@ It has many more features like image and video search. Some of the planned featu
## Installation ## Installation
There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. Using Docker is highly recommended. Perplexica can be installed using Docker (recommended) or directly on your system.
### Getting Started with Docker (Recommended) ### Docker Installation (Recommended)
1. Ensure Docker is installed and running on your system. 1. Ensure Docker is installed and running on your system.
2. Clone the Perplexica repository: 2. Clone the Perplexica repository:
@@ -86,10 +102,15 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
docker compose up -d docker compose up -d
``` ```
6. Wait a few minutes for the setup to complete. You can access Perplexica at http://localhost:3000 in your web browser. 6. Wait a few minutes for the setup to complete. You can access Perplexica at http://localhost:8080 in your web browser.
**Note**: After the containers are built, you can start Perplexica directly from Docker without having to open a terminal. **Note**: After the containers are built, you can start Perplexica directly from Docker without having to open a terminal.
The Docker configuration is located in the `docker/` directory, containing:
- Dockerfile with multi-stage build for efficient images
- Service configurations for the integrated process manager
- Nginx reverse proxy configuration
### Non-Docker Installation ### Non-Docker Installation
1. Install SearXNG and allow `JSON` format in the SearXNG settings. 1. Install SearXNG and allow `JSON` format in the SearXNG settings.
@@ -103,6 +124,17 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker.
See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like exposing it your network, etc. See the [installation documentation](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/installation) for more information like exposing it your network, etc.
### Nginx Reverse Proxy
Perplexica includes an Nginx reverse proxy that provides several key benefits:
- **Single Port Access**: Access both frontend and backend through a single port (8080)
- **Dynamic Configuration**: Works with any domain or IP without rebuilding
- **WebSocket Support**: Automatic WebSocket URL configuration based on the current domain
- **Security Headers**: Enhanced security with proper HTTP headers
When using Docker, the reverse proxy is automatically configured. Access Perplexica at `http://localhost:8080` or `http://your-ip:8080` after starting the containers.
### Ollama Connection Errors ### Ollama Connection Errors
If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can: If you're encountering an Ollama connection error, it is likely due to the backend being unable to connect to Ollama's API. To fix this issue you can:
@@ -128,7 +160,7 @@ If you wish to use Perplexica as an alternative to traditional search engines li
1. Open your browser's settings. 1. Open your browser's settings.
2. Navigate to the 'Search Engines' section. 2. Navigate to the 'Search Engines' section.
3. Add a new site search with the following URL: `http://localhost:3000/?q=%s`. Replace `localhost` with your IP address or domain name, and `3000` with the port number if Perplexica is not hosted locally. 3. Add a new site search with the following URL: `http://localhost:8080/?q=%s`. Replace `localhost` with your IP address or domain name if needed.
4. Click the add button. Now, you can use Perplexica directly from your browser's search bar. 4. Click the add button. Now, you can use Perplexica directly from your browser's search bar.
## Using Perplexica's API ## Using Perplexica's API
@@ -137,12 +169,19 @@ Perplexica also provides an API for developers looking to integrate its powerful
For more details, check out the full documentation [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/API/SEARCH.md). For more details, check out the full documentation [here](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/API/SEARCH.md).
## Expose Perplexica to network ## Expose Perplexica to a Network
You can access Perplexica over your home network by following our networking guide [here](https://github.com/ItzCrazyKns/Perplexica/blob/master/docs/installation/NETWORKING.md). Perplexica can be easily accessed over your home network or exposed to the internet through the Nginx reverse proxy. With this setup:
1. **Local Network Access**: Access Perplexica from any device on your network using `http://server-ip:8080`
2. **Domain Configuration**: If you have a domain name, point it to your server and access Perplexica with `http://your-domain.com:8080`
3. **SSL Support**: Configure SSL certificates in Nginx for secure `https://` access
For more network configuration details, see our [networking guide](https://github.com/ItzCrazyKns/Perplexica/blob/master/docs/installation/NETWORKING.md).
## One-Click Deployment ## One-Click Deployment
[![Deploy to Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://usw.sealos.io/?openapp=system-template%3FtemplateName%3Dperplexica)
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267) [![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
## Upcoming Features ## Upcoming Features

View File

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

View File

@@ -1,17 +0,0 @@
FROM node:18-slim
WORKDIR /home/perplexica
COPY src /home/perplexica/src
COPY tsconfig.json /home/perplexica/
COPY drizzle.config.ts /home/perplexica/
COPY package.json /home/perplexica/
COPY yarn.lock /home/perplexica/
RUN mkdir /home/perplexica/data
RUN mkdir /home/perplexica/uploads
RUN yarn install --frozen-lockfile --network-timeout 600000
RUN yarn build
CMD ["yarn", "start"]

View File

@@ -2,49 +2,43 @@ services:
searxng: searxng:
image: docker.io/searxng/searxng:latest image: docker.io/searxng/searxng:latest
volumes: volumes:
- ./searxng:/etc/searxng:rw - ./searxng:/etc/searxng
ports:
- 4000:8080
networks: networks:
- perplexica-network - perplexica-network
restart: unless-stopped restart: unless-stopped
perplexica-backend: perplexica:
build: image: itzcrazykns1337/perplexica:latest
context: .
dockerfile: backend.dockerfile
image: itzcrazykns1337/perplexica-backend:main
environment:
- SEARXNG_API_URL=http://searxng:8080
depends_on:
- searxng
ports: ports:
- 3001:3001 - "8080:8080"
environment:
- SEARXNG_API_URL=http://searxng:4000
- SIMILARITY_MEASURE=cosine
- KEEP_ALIVE=5m
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- GROQ_API_KEY=${GROQ_API_KEY:-}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- OLLAMA_API_URL=${OLLAMA_API_URL:-}
- CUSTOM_OPENAI_API_KEY=${CUSTOM_OPENAI_API_KEY:-}
- CUSTOM_OPENAI_API_URL=${CUSTOM_OPENAI_API_URL:-}
- CUSTOM_OPENAI_MODEL_NAME=${CUSTOM_OPENAI_MODEL_NAME:-}
volumes: volumes:
- backend-dbstore:/home/perplexica/data - backend-dbstore:/app/backend/data
- uploads:/home/perplexica/uploads - uploads:/app/backend/uploads
- ./config.toml:/home/perplexica/config.toml
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - 'host.docker.internal:host-gateway'
networks:
- perplexica-network
restart: unless-stopped
perplexica-frontend:
build:
context: .
dockerfile: app.dockerfile
args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
image: itzcrazykns1337/perplexica-frontend:main
depends_on: depends_on:
- perplexica-backend - searxng
ports:
- 3000:3000
networks: networks:
- perplexica-network - perplexica-network
restart: unless-stopped restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
networks: networks:
perplexica-network: perplexica-network:

93
docker/Dockerfile Normal file
View File

@@ -0,0 +1,93 @@
# Multi-stage build for Perplexica
# Stage 1: Build the backend
FROM node:lts-alpine as backend-builder
WORKDIR /app
COPY src ./src
COPY tsconfig.json drizzle.config.ts package.json yarn.lock ./
RUN yarn install --frozen-lockfile --network-timeout 600000 && \
yarn build
# Stage 2: Build the frontend
FROM node:lts-alpine as frontend-builder
WORKDIR /app
COPY ui ./
ARG NEXT_PUBLIC_API_URL=/api
ARG NEXT_PUBLIC_WS_URL=auto
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
RUN yarn install --frozen-lockfile && \
yarn build
# Stage 3: Final image
FROM node:lts-alpine
# Install curl and jq for GitHub API access
RUN apk add --no-cache curl jq
# Determine latest S6 overlay version at build time
RUN S6_OVERLAY_VERSION=$(curl -s https://api.github.com/repos/just-containers/s6-overlay/releases/latest | jq -r .tag_name | sed 's/^v//') && \
echo "Using S6 overlay version: $S6_OVERLAY_VERSION" && \
echo "$S6_OVERLAY_VERSION" > /tmp/s6-version
# Use Docker's TARGETARCH for automatic architecture detection
ARG TARGETARCH
# Install additional required packages and create directory structure in one layer
RUN apk add --no-cache \
nginx \
tzdata \
bash && \
mkdir -p /app/backend /app/frontend /app/data /app/uploads
# Map Docker's architecture names to s6-overlay architecture names and download/install
RUN S6_OVERLAY_VERSION=$(cat /tmp/s6-version) && \
case "${TARGETARCH}" in \
"amd64") S6_OVERLAY_ARCH="x86_64" ;; \
"arm64") S6_OVERLAY_ARCH="aarch64" ;; \
"arm") S6_OVERLAY_ARCH="arm" ;; \
*) echo "Unsupported architecture: ${TARGETARCH}. Only amd64, arm64, and arm are supported." && exit 1 ;; \
esac && \
echo "Target architecture: ${TARGETARCH} -> S6 architecture: ${S6_OVERLAY_ARCH}" && \
echo "Downloading s6-overlay v${S6_OVERLAY_VERSION} for architecture: ${S6_OVERLAY_ARCH}" && \
curl -L -s -o /tmp/s6-overlay-noarch.tar.xz "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \
curl -L -s -o /tmp/s6-overlay-arch.tar.xz "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz" && \
tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz && \
curl -L -s -o /tmp/s6-overlay-symlinks-noarch.tar.xz "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-symlinks-noarch.tar.xz" && \
tar -C / -Jxpf /tmp/s6-overlay-symlinks-noarch.tar.xz && \
rm -f /tmp/s6-overlay-*.tar.xz /tmp/s6-version
# Copy configuration files
COPY docker/etc/s6-overlay/services /etc/services.d/
COPY docker/etc/nginx/nginx.conf /etc/nginx/nginx.conf
# Make service scripts executable
RUN chmod +x /etc/services.d/*/run /etc/services.d/*/finish
# Copy application files from builders
COPY --from=backend-builder /app/dist /app/backend/dist
COPY --from=backend-builder /app/node_modules /app/backend/node_modules
COPY --from=backend-builder /app/package.json /app/backend/package.json
COPY --from=backend-builder /app/drizzle.config.ts /app/backend/drizzle.config.ts
# Copy only the schema file for Drizzle migrations
COPY --from=backend-builder /app/src/db/schema.ts /app/backend/src/db/schema.ts
COPY --from=frontend-builder /app/.next /app/frontend/.next
COPY --from=frontend-builder /app/node_modules /app/frontend/node_modules
COPY --from=frontend-builder /app/package.json /app/frontend/package.json
COPY --from=frontend-builder /app/public /app/frontend/public
# Configure volumes and ports
VOLUME ["/app/backend/data", "/app/backend/uploads"]
EXPOSE 8080
# Set up healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/ || exit 1
ENTRYPOINT ["/init"]

View File

@@ -0,0 +1,55 @@
events {
worker_connections 1024;
}
http {
port_in_redirect on;
absolute_redirect off;
server {
listen 8080;
server_name localhost;
# Global timeout settings for all locations
proxy_read_timeout 86400s; # 24 hours
proxy_send_timeout 86400s; # 24 hours
proxy_connect_timeout 60s; # Connection establishment timeout
# API requests
location /api {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket requests
location /ws {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Frontend requests
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
server_tokens off;
}
}

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
s6-svc -d /var/run/s6/services/frontend
s6-svc -d /var/run/s6/services/nginx

View File

@@ -0,0 +1,8 @@
#!/usr/bin/with-contenv bash
cd /app/backend
# Run database migrations before starting the app
yarn db:push
# Start the application
exec node dist/app.js

View File

@@ -0,0 +1,2 @@
#!/usr/bin/with-contenv bash
s6-svc -d /var/run/s6/services/nginx

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
cd /app/frontend
exec node_modules/.bin/next start

View File

@@ -0,0 +1,2 @@
#!/usr/bin/with-contenv bash
exec nginx -g "daemon off;"

View File

@@ -1,109 +1,46 @@
# Expose Perplexica to a network # Accessing Perplexica over a Network
This guide will show you how to make Perplexica available over a network. Follow these steps to allow computers on the same network to interact with Perplexica. Choose the instructions that match the operating system you are using. This guide explains how to access Perplexica over your network using the nginx reverse proxy included in the Docker setup.
## Windows ## Basic Network Access
1. Open PowerShell as Administrator Perplexica is automatically accessible from any device on your network:
2. Navigate to the directory containing the `docker-compose.yaml` file
3. Stop and remove the existing Perplexica containers and images:
1. Start Perplexica using Docker Compose:
```bash ```bash
docker compose down --rmi all docker compose up -d
``` ```
4. Open the `docker-compose.yaml` file in a text editor like Notepad++ 2. Find your server's IP address:
- **Windows**: `ipconfig` in Command Prompt
- **macOS**: `ifconfig | grep "inet "` in Terminal
- **Linux**: `ip addr show | grep "inet "` in Terminal
5. Replace `127.0.0.1` with the IP address of the server Perplexica is running on in these two lines: 3. Access Perplexica from any device on your network:
```
```bash http://YOUR_SERVER_IP:8080
args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
``` ```
6. Save and close the `docker-compose.yaml` file ## Custom Port Configuration
7. Rebuild and restart the Perplexica container: If you need to use a different port instead of the default 8080:
```bash 1. Modify the `docker-compose.yaml` file:
docker compose up -d --build ```yaml
perplexica:
ports:
- "YOUR_CUSTOM_PORT:8080"
``` ```
## macOS 2. Restart the containers:
1. Open the Terminal application
2. Navigate to the directory with the `docker-compose.yaml` file:
```bash ```bash
cd /path/to/docker-compose.yaml docker compose down && docker compose up -d
``` ```
3. Stop and remove existing containers and images: ## Troubleshooting
```bash If you encounter issues accessing Perplexica over your network:
docker compose down --rmi all
```
4. Open `docker-compose.yaml` in a text editor like Sublime Text: 1. **Firewall Settings**: Ensure port 8080 (or your custom port) is allowed in your firewall
2. **Docker Logs**: Check for any connection issues with `docker logs perplexica`
```bash 3. **Network Access**: Make sure your devices are on the same network and can reach the server
nano docker-compose.yaml
```
5. Replace `127.0.0.1` with the server IP in these lines:
```bash
args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
```
6. Save and exit the editor
7. Rebuild and restart Perplexica:
```bash
docker compose up -d --build
```
## Linux
1. Open the terminal
2. Navigate to the `docker-compose.yaml` directory:
```bash
cd /path/to/docker-compose.yaml
```
3. Stop and remove containers and images:
```bash
docker compose down --rmi all
```
4. Edit `docker-compose.yaml`:
```bash
nano docker-compose.yaml
```
5. Replace `127.0.0.1` with the server IP:
```bash
args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001
```
6. Save and exit the editor
7. Rebuild and restart Perplexica:
```bash
docker compose up -d --build
```

View File

@@ -7,34 +7,43 @@ To update Perplexica to the latest version, follow these steps:
1. Clone the latest version of Perplexica from GitHub: 1. Clone the latest version of Perplexica from GitHub:
```bash ```bash
git clone https://github.com/ItzCrazyKns/Perplexica.git git clone https://github.com/ItzCrazyKns/Perplexica.git
``` ```
2. Navigate to the Project Directory. 2. Navigate to the project directory.
3. Pull latest images from registry. 3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
4. Pull the latest images from the registry.
```bash ```bash
docker compose pull docker compose pull
``` ```
4. Update and Recreate containers. 5. Update and recreate the containers.
```bash ```bash
docker compose up -d docker compose up -d
``` ```
5. Once the command completes running go to http://localhost:3000 and verify the latest changes. 6. Once the command completes, go to http://localhost:3000 and verify the latest changes.
## For non Docker users ## For non-Docker users
1. Clone the latest version of Perplexica from GitHub: 1. Clone the latest version of Perplexica from GitHub:
```bash ```bash
git clone https://github.com/ItzCrazyKns/Perplexica.git git clone https://github.com/ItzCrazyKns/Perplexica.git
``` ```
2. Navigate to the Project Directory 2. Navigate to the project directory.
3. Execute `npm i` in both the `ui` folder and the root directory.
4. Once packages are updated, execute `npm run build` in both the `ui` folder and the root directory. 3. Check for changes in the configuration files. If the `sample.config.toml` file contains new fields, delete your existing `config.toml` file, rename `sample.config.toml` to `config.toml`, and update the configuration accordingly.
5. Finally, start both the frontend and the backend by running `npm run start` in both the `ui` folder and the root directory.
4. Execute `npm i` in both the `ui` folder and the root directory.
5. Once the packages are updated, execute `npm run build` in both the `ui` folder and the root directory.
6. Finally, start both the frontend and the backend by running `npm run start` in both the `ui` folder and the root directory.
---

View File

@@ -8,7 +8,7 @@ import { ChatOpenAI } from '@langchain/openai';
const suggestionGeneratorPrompt = ` const suggestionGeneratorPrompt = `
You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information. You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information.
You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information. You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information.
Make sure the suggestions are medium in length and are informative and relevant to the conversation. Make sure the suggestions are medium in length and are informative and relevant to the conversation.
Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example: Provide these suggestions separated by newlines between the XML tags <suggestions> and </suggestions>. For example:

View File

@@ -13,20 +13,20 @@ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
const VideoSearchChainPrompt = ` const VideoSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Example: Example:
1. Follow up question: How does a car work? 1. Follow up question: How does a car work?
Rephrased: How does a car work? Rephrased: How does a car work?
2. Follow up question: What is the theory of relativity? 2. Follow up question: What is the theory of relativity?
Rephrased: What is theory of relativity Rephrased: What is theory of relativity
3. Follow up question: How does an AC work? 3. Follow up question: How does an AC work?
Rephrased: How does an AC work Rephrased: How does an AC work
Conversation: Conversation:
{chat_history} {chat_history}
Follow up question: {query} Follow up question: {query}
Rephrased question: Rephrased question:
`; `;

View File

@@ -41,39 +41,83 @@ type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>; [P in keyof T]?: RecursivePartial<T[P]>;
}; };
const loadConfig = () => const loadConfig = () => {
toml.parse( try {
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'), return toml.parse(
) as any as Config; fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
) as any as Config;
} catch (error) {
// Return default config if file doesn't exist
return {
GENERAL: {
PORT: 3001,
SIMILARITY_MEASURE: 'cosine',
KEEP_ALIVE: '5m',
},
MODELS: {
OPENAI: {
API_KEY: '',
},
GROQ: {
API_KEY: '',
},
ANTHROPIC: {
API_KEY: '',
},
GEMINI: {
API_KEY: '',
},
OLLAMA: {
API_URL: '',
},
CUSTOM_OPENAI: {
API_URL: '',
API_KEY: '',
MODEL_NAME: '',
},
},
API_ENDPOINTS: {
SEARXNG: '',
},
};
}
};
export const getPort = () => loadConfig().GENERAL.PORT; export const getPort = () =>
process.env.PORT ? parseInt(process.env.PORT, 10) : loadConfig().GENERAL.PORT;
export const getSimilarityMeasure = () => export const getSimilarityMeasure = () =>
loadConfig().GENERAL.SIMILARITY_MEASURE; process.env.SIMILARITY_MEASURE || loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE; export const getKeepAlive = () =>
process.env.KEEP_ALIVE || loadConfig().GENERAL.KEEP_ALIVE;
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY; export const getOpenaiApiKey = () =>
process.env.OPENAI_API_KEY || loadConfig().MODELS.OPENAI.API_KEY;
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY; export const getGroqApiKey = () =>
process.env.GROQ_API_KEY || loadConfig().MODELS.GROQ.API_KEY;
export const getAnthropicApiKey = () => loadConfig().MODELS.ANTHROPIC.API_KEY; export const getAnthropicApiKey = () =>
process.env.ANTHROPIC_API_KEY || loadConfig().MODELS.ANTHROPIC.API_KEY;
export const getGeminiApiKey = () => loadConfig().MODELS.GEMINI.API_KEY; export const getGeminiApiKey = () =>
process.env.GEMINI_API_KEY || loadConfig().MODELS.GEMINI.API_KEY;
export const getSearxngApiEndpoint = () => export const getSearxngApiEndpoint = () =>
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG; process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL; export const getOllamaApiEndpoint = () =>
process.env.OLLAMA_API_URL || loadConfig().MODELS.OLLAMA.API_URL;
export const getCustomOpenaiApiKey = () => export const getCustomOpenaiApiKey = () =>
loadConfig().MODELS.CUSTOM_OPENAI.API_KEY; process.env.CUSTOM_OPENAI_API_KEY || loadConfig().MODELS.CUSTOM_OPENAI.API_KEY;
export const getCustomOpenaiApiUrl = () => export const getCustomOpenaiApiUrl = () =>
loadConfig().MODELS.CUSTOM_OPENAI.API_URL; process.env.CUSTOM_OPENAI_API_URL || loadConfig().MODELS.CUSTOM_OPENAI.API_URL;
export const getCustomOpenaiModelName = () => export const getCustomOpenaiModelName = () =>
loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME; process.env.CUSTOM_OPENAI_MODEL_NAME || loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME;
const mergeConfigs = (current: any, update: any): any => { const mergeConfigs = (current: any, update: any): any => {
if (update === null || update === undefined) { if (update === null || update === undefined) {

View File

@@ -50,7 +50,7 @@ export const academicSearchResponsePrompt = `
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web. - You are set on focus mode 'Academic', this means you will be searching for academic papers and articles on the web.
### Example Output ### Example Output
- Begin with a brief introduction summarizing the event or query topic. - Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Follow with detailed sections under clear headings, covering all aspects of the query if possible.

View File

@@ -50,7 +50,7 @@ export const redditSearchResponsePrompt = `
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Reddit', this means you will be searching for information, opinions and discussions on the web using Reddit. - You are set on focus mode 'Reddit', this means you will be searching for information, opinions and discussions on the web using Reddit.
### Example Output ### Example Output
- Begin with a brief introduction summarizing the event or query topic. - Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Follow with detailed sections under clear headings, covering all aspects of the query if possible.

View File

@@ -50,7 +50,7 @@ export const wolframAlphaSearchResponsePrompt = `
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Wolfram Alpha', this means you will be searching for information on the web using Wolfram Alpha. It is a computational knowledge engine that can answer factual queries and perform computations. - You are set on focus mode 'Wolfram Alpha', this means you will be searching for information on the web using Wolfram Alpha. It is a computational knowledge engine that can answer factual queries and perform computations.
### Example Output ### Example Output
- Begin with a brief introduction summarizing the event or query topic. - Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Follow with detailed sections under clear headings, covering all aspects of the query if possible.

View File

@@ -1,5 +1,5 @@
export const writingAssistantPrompt = ` export const writingAssistantPrompt = `
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query. You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode. Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that. You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that.

View File

@@ -50,7 +50,7 @@ export const youtubeSearchResponsePrompt = `
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
- You are set on focus mode 'Youtube', this means you will be searching for videos on the web using Youtube and providing information based on the video's transcrip - You are set on focus mode 'Youtube', this means you will be searching for videos on the web using Youtube and providing information based on the video's transcrip
### Example Output ### Example Output
- Begin with a brief introduction summarizing the event or query topic. - Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible. - Follow with detailed sections under clear headings, covering all aspects of the query if possible.

View File

@@ -85,10 +85,12 @@ router.post('/', async (req, res) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
modelName: body.chatModel?.model || getCustomOpenaiModelName(), modelName: body.chatModel?.model || getCustomOpenaiModelName(),
openAIApiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), openAIApiKey:
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
baseURL: body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(), baseURL:
body.chatModel?.customOpenAIBaseURL || getCustomOpenaiApiUrl(),
}, },
}) as unknown as BaseChatModel; }) as unknown as BaseChatModel;
} else if ( } else if (

View File

@@ -129,10 +129,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
await Promise.all( await Promise.all(
docGroups.map(async (doc) => { docGroups.map(async (doc) => {
const res = await llm.invoke(` const res = await llm.invoke(`
You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query. text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary. If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
- **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague. - **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
- **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query. - **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
- **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format. - **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
@@ -141,8 +141,8 @@ class MetaSearchAgent implements MetaSearchAgentType {
<example> <example>
1. \`<text> 1. \`<text>
Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers. Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.
It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications
by using containers. by using containers.
</text> </text>
@@ -151,8 +151,8 @@ class MetaSearchAgent implements MetaSearchAgentType {
</query> </query>
Response: Response:
Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application
deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in
any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed. any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed.
\` \`
2. \`<text> 2. \`<text>

View File

@@ -223,11 +223,11 @@ const Page = () => {
setChatModels(data.chatModelProviders || {}); setChatModels(data.chatModelProviders || {});
setEmbeddingModels(data.embeddingModelProviders || {}); setEmbeddingModels(data.embeddingModelProviders || {});
const currentProvider = selectedChatModelProvider; const currentChatProvider = selectedChatModelProvider;
const newProviders = Object.keys(data.chatModelProviders || {}); const newChatProviders = Object.keys(data.chatModelProviders || {});
if (!currentProvider && newProviders.length > 0) { if (!currentChatProvider && newChatProviders.length > 0) {
const firstProvider = newProviders[0]; const firstProvider = newChatProviders[0];
const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name; const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name;
if (firstModel) { if (firstModel) {
@@ -237,11 +237,11 @@ const Page = () => {
localStorage.setItem('chatModel', firstModel); localStorage.setItem('chatModel', firstModel);
} }
} else if ( } else if (
currentProvider && currentChatProvider &&
(!data.chatModelProviders || (!data.chatModelProviders ||
!data.chatModelProviders[currentProvider] || !data.chatModelProviders[currentChatProvider] ||
!Array.isArray(data.chatModelProviders[currentProvider]) || !Array.isArray(data.chatModelProviders[currentChatProvider]) ||
data.chatModelProviders[currentProvider].length === 0) data.chatModelProviders[currentChatProvider].length === 0)
) { ) {
const firstValidProvider = Object.entries( const firstValidProvider = Object.entries(
data.chatModelProviders || {}, data.chatModelProviders || {},
@@ -267,6 +267,55 @@ const Page = () => {
} }
} }
const currentEmbeddingProvider = selectedEmbeddingModelProvider;
const newEmbeddingProviders = Object.keys(
data.embeddingModelProviders || {},
);
if (!currentEmbeddingProvider && newEmbeddingProviders.length > 0) {
const firstProvider = newEmbeddingProviders[0];
const firstModel =
data.embeddingModelProviders[firstProvider]?.[0]?.name;
if (firstModel) {
setSelectedEmbeddingModelProvider(firstProvider);
setSelectedEmbeddingModel(firstModel);
localStorage.setItem('embeddingModelProvider', firstProvider);
localStorage.setItem('embeddingModel', firstModel);
}
} else if (
currentEmbeddingProvider &&
(!data.embeddingModelProviders ||
!data.embeddingModelProviders[currentEmbeddingProvider] ||
!Array.isArray(
data.embeddingModelProviders[currentEmbeddingProvider],
) ||
data.embeddingModelProviders[currentEmbeddingProvider].length === 0)
) {
const firstValidProvider = Object.entries(
data.embeddingModelProviders || {},
).find(
([_, models]) => Array.isArray(models) && models.length > 0,
)?.[0];
if (firstValidProvider) {
setSelectedEmbeddingModelProvider(firstValidProvider);
setSelectedEmbeddingModel(
data.embeddingModelProviders[firstValidProvider][0].name,
);
localStorage.setItem('embeddingModelProvider', firstValidProvider);
localStorage.setItem(
'embeddingModel',
data.embeddingModelProviders[firstValidProvider][0].name,
);
} else {
setSelectedEmbeddingModelProvider(null);
setSelectedEmbeddingModel(null);
localStorage.removeItem('embeddingModelProvider');
localStorage.removeItem('embeddingModel');
}
}
setConfig(data); setConfig(data);
} }
@@ -278,6 +327,10 @@ const Page = () => {
localStorage.setItem('chatModelProvider', value); localStorage.setItem('chatModelProvider', value);
} else if (key === 'chatModel') { } else if (key === 'chatModel') {
localStorage.setItem('chatModel', value); localStorage.setItem('chatModel', value);
} else if (key === 'embeddingModelProvider') {
localStorage.setItem('embeddingModelProvider', value);
} else if (key === 'embeddingModel') {
localStorage.setItem('embeddingModel', value);
} }
} catch (err) { } catch (err) {
console.error('Failed to save:', err); console.error('Failed to save:', err);
@@ -436,7 +489,6 @@ const Page = () => {
const value = e.target.value; const value = e.target.value;
setSelectedChatModelProvider(value); setSelectedChatModelProvider(value);
saveConfig('chatModelProvider', value); saveConfig('chatModelProvider', value);
// Auto-select first model of new provider
const firstModel = const firstModel =
config.chatModelProviders[value]?.[0]?.name; config.chatModelProviders[value]?.[0]?.name;
if (firstModel) { if (firstModel) {
@@ -511,12 +563,16 @@ const Page = () => {
<Input <Input
type="text" type="text"
placeholder="Model name" placeholder="Model name"
defaultValue={config.customOpenaiModelName} value={config.customOpenaiModelName}
onChange={(e) => isSaving={savingStates['customOpenaiModelName']}
setConfig({ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
...config, setConfig((prev) => ({
...prev!,
customOpenaiModelName: e.target.value, customOpenaiModelName: e.target.value,
}) }));
}}
onSave={(value) =>
saveConfig('customOpenaiModelName', value)
} }
/> />
</div> </div>
@@ -527,12 +583,16 @@ const Page = () => {
<Input <Input
type="text" type="text"
placeholder="Custom OpenAI API Key" placeholder="Custom OpenAI API Key"
defaultValue={config.customOpenaiApiKey} value={config.customOpenaiApiKey}
onChange={(e) => isSaving={savingStates['customOpenaiApiKey']}
setConfig({ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
...config, setConfig((prev) => ({
...prev!,
customOpenaiApiKey: e.target.value, customOpenaiApiKey: e.target.value,
}) }));
}}
onSave={(value) =>
saveConfig('customOpenaiApiKey', value)
} }
/> />
</div> </div>
@@ -543,17 +603,96 @@ const Page = () => {
<Input <Input
type="text" type="text"
placeholder="Custom OpenAI Base URL" placeholder="Custom OpenAI Base URL"
defaultValue={config.customOpenaiApiUrl} value={config.customOpenaiApiUrl}
onChange={(e) => isSaving={savingStates['customOpenaiApiUrl']}
setConfig({ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
...config, setConfig((prev) => ({
...prev!,
customOpenaiApiUrl: e.target.value, customOpenaiApiUrl: e.target.value,
}) }));
}}
onSave={(value) =>
saveConfig('customOpenaiApiUrl', value)
} }
/> />
</div> </div>
</div> </div>
)} )}
{config.embeddingModelProviders && (
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Embedding Model Provider
</p>
<Select
value={selectedEmbeddingModelProvider ?? undefined}
onChange={(e) => {
const value = e.target.value;
setSelectedEmbeddingModelProvider(value);
saveConfig('embeddingModelProvider', value);
const firstModel =
config.embeddingModelProviders[value]?.[0]?.name;
if (firstModel) {
setSelectedEmbeddingModel(firstModel);
saveConfig('embeddingModel', firstModel);
}
}}
options={Object.keys(config.embeddingModelProviders).map(
(provider) => ({
value: provider,
label:
provider.charAt(0).toUpperCase() +
provider.slice(1),
}),
)}
/>
</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
value={selectedEmbeddingModel ?? undefined}
onChange={(e) => {
const value = e.target.value;
setSelectedEmbeddingModel(value);
saveConfig('embeddingModel', value);
}}
options={(() => {
const embeddingModelProvider =
config.embeddingModelProviders[
selectedEmbeddingModelProvider
];
return embeddingModelProvider
? embeddingModelProvider.length > 0
? embeddingModelProvider.map((model) => ({
value: model.name,
label: model.displayName,
}))
: [
{
value: '',
label: 'No models available',
disabled: true,
},
]
: [
{
value: '',
label:
'Invalid provider, please check backend logs',
disabled: true,
},
];
})()}
/>
</div>
)}
</div>
)}
</SettingsSection> </SettingsSection>
<SettingsSection title="API Keys"> <SettingsSection title="API Keys">

View File

@@ -368,7 +368,7 @@ const loadMessages = async (
const ChatWindow = ({ id }: { id?: string }) => { const ChatWindow = ({ id }: { id?: string }) => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const initialMessage = searchParams.get('q'); const initialMessage = searchParams?.get('q');
const [chatId, setChatId] = useState<string | undefined>(id); const [chatId, setChatId] = useState<string | undefined>(id);
const [newChatCreated, setNewChatCreated] = useState(false); const [newChatCreated, setNewChatCreated] = useState(false);
@@ -378,7 +378,9 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [isWSReady, setIsWSReady] = useState(false); const [isWSReady, setIsWSReady] = useState(false);
const ws = useSocket( const ws = useSocket(
process.env.NEXT_PUBLIC_WS_URL!, process.env.NEXT_PUBLIC_WS_URL === 'auto'
? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
: process.env.NEXT_PUBLIC_WS_URL!,
setIsWSReady, setIsWSReady,
setHasError, setHasError,
); );

View File

@@ -68,7 +68,7 @@ const MessageBox = ({
return ( return (
<div> <div>
{message.role === 'user' && ( {message.role === 'user' && (
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}> <div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8', 'break-words')}>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{message.content} {message.content}
</h2> </h2>

View File

@@ -110,7 +110,7 @@ const Attach = ({
<button <button
type="button" type="button"
onClick={() => fileInputRef.current.click()} onClick={() => fileInputRef.current.click()}
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200" className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
> >
<input <input
type="file" type="file"
@@ -128,7 +128,7 @@ const Attach = ({
setFiles([]); setFiles([]);
setFileIds([]); setFileIds([]);
}} }}
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200" className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
> >
<Trash size={14} /> <Trash size={14} />
<p className="text-xs">Clear</p> <p className="text-xs">Clear</p>
@@ -145,7 +145,7 @@ const Attach = ({
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> <div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
<File size={16} className="text-white/70" /> <File size={16} className="text-white/70" />
</div> </div>
<p className="text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
{file.fileName.length > 25 {file.fileName.length > 25
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + ? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
'...' + '...' +

View File

@@ -82,7 +82,7 @@ const AttachSmall = ({
<button <button
type="button" type="button"
onClick={() => fileInputRef.current.click()} onClick={() => fileInputRef.current.click()}
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200" className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
> >
<input <input
type="file" type="file"
@@ -100,7 +100,7 @@ const AttachSmall = ({
setFiles([]); setFiles([]);
setFileIds([]); setFileIds([]);
}} }}
className="flex flex-row items-center space-x-1 text-white/70 hover:text-white transition duration-200" className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
> >
<Trash size={14} /> <Trash size={14} />
<p className="text-xs">Clear</p> <p className="text-xs">Clear</p>
@@ -117,7 +117,7 @@ const AttachSmall = ({
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md"> <div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
<File size={16} className="text-white/70" /> <File size={16} className="text-white/70" />
</div> </div>
<p className="text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
{file.fileName.length > 25 {file.fileName.length > 25
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + ? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
'...' + '...' +