diff --git a/.assets/manifest.json b/.assets/manifest.json new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 0620afb..29f7987 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -8,15 +8,12 @@ on: types: [published] jobs: - build-and-push: + build-amd64: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 with: @@ -33,28 +30,109 @@ jobs: id: version run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - name: Build and push Docker image + - name: Build and push AMD64 Docker image if: github.ref == 'refs/heads/master' && github.event_name == 'push' run: | - docker buildx create --use - DOCKERFILE=app.dockerfile; \ - IMAGE_NAME=perplexica; \ - docker buildx build --platform linux/amd64,linux/arm64 \ - --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:main \ + DOCKERFILE=app.dockerfile + IMAGE_NAME=perplexica + docker buildx build --platform linux/amd64 \ + --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:amd64 \ --cache-to=type=inline \ + --provenance false \ -f $DOCKERFILE \ - -t itzcrazykns1337/${IMAGE_NAME}:main \ + -t itzcrazykns1337/${IMAGE_NAME}:amd64 \ --push . - - name: Build and push release Docker image + - name: Build and push AMD64 release Docker image if: github.event_name == 'release' run: | - docker buildx create --use - DOCKERFILE=app.dockerfile; \ - IMAGE_NAME=perplexica; \ - docker buildx build --platform linux/amd64,linux/arm64 \ - --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \ + DOCKERFILE=app.dockerfile + IMAGE_NAME=perplexica + docker buildx build --platform linux/amd64 \ + --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-amd64 \ --cache-to=type=inline \ + --provenance false \ -f $DOCKERFILE \ - -t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \ + -t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-amd64 \ --push . + + build-arm64: + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + install: true + + - name: Log in to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract version from release tag + if: github.event_name == 'release' + id: version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Build and push ARM64 Docker image + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + run: | + DOCKERFILE=app.dockerfile + IMAGE_NAME=perplexica + docker buildx build --platform linux/arm64 \ + --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:arm64 \ + --cache-to=type=inline \ + --provenance false \ + -f $DOCKERFILE \ + -t itzcrazykns1337/${IMAGE_NAME}:arm64 \ + --push . + + - name: Build and push ARM64 release Docker image + if: github.event_name == 'release' + run: | + DOCKERFILE=app.dockerfile + IMAGE_NAME=perplexica + docker buildx build --platform linux/arm64 \ + --cache-from=type=registry,ref=itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-arm64 \ + --cache-to=type=inline \ + --provenance false \ + -f $DOCKERFILE \ + -t itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-arm64 \ + --push . + + manifest: + needs: [build-amd64, build-arm64] + runs-on: ubuntu-latest + steps: + - name: Log in to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract version from release tag + if: github.event_name == 'release' + id: version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Create and push multi-arch manifest for main + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + run: | + IMAGE_NAME=perplexica + docker manifest create itzcrazykns1337/${IMAGE_NAME}:main \ + --amend itzcrazykns1337/${IMAGE_NAME}:amd64 \ + --amend itzcrazykns1337/${IMAGE_NAME}:arm64 + docker manifest push itzcrazykns1337/${IMAGE_NAME}:main + + - name: Create and push multi-arch manifest for releases + if: github.event_name == 'release' + run: | + IMAGE_NAME=perplexica + docker manifest create itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} \ + --amend itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-amd64 \ + --amend itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }}-arm64 + docker manifest push itzcrazykns1337/${IMAGE_NAME}:${{ env.RELEASE_VERSION }} diff --git a/.gitignore b/.gitignore index c95173d..9fb5e4c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ Thumbs.db # Db db.sqlite /searxng + +certificates \ No newline at end of file diff --git a/README.md b/README.md index 6540c73..5eb0713 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@
-[![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT) +[![Discord](https://dcbadge.limes.pink/api/server/26aArMy8tT?style=flat)](https://discord.gg/26aArMy8tT) ![preview](.assets/perplexica-screenshot.png?) @@ -90,6 +90,9 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. - `OLLAMA`: Your Ollama API URL. You should enter it as `http://host.docker.internal:PORT_NUMBER`. If you installed Ollama on port 11434, use `http://host.docker.internal:11434`. For other ports, adjust accordingly. **You need to fill this if you wish to use Ollama's models instead of OpenAI's**. - `GROQ`: Your Groq API key. **You only need to fill this if you wish to use Groq's hosted models**. - `ANTHROPIC`: Your Anthropic API key. **You only need to fill this if you wish to use Anthropic models**. + - `Gemini`: Your Gemini API key. **You only need to fill this if you wish to use Google's models**. + - `DEEPSEEK`: Your Deepseek API key. **Only needed if you want Deepseek models.** + - `AIMLAPI`: Your AI/ML API key. **Only needed if you want to use AI/ML API models and embeddings.** **Note**: You can change these after starting Perplexica from the settings dialog. @@ -111,7 +114,7 @@ There are mainly 2 ways of installing Perplexica - With Docker, Without Docker. 2. Clone the repository and rename the `sample.config.toml` file to `config.toml` in the root directory. Ensure you complete all required fields in this file. 3. After populating the configuration run `npm i`. 4. Install the dependencies and then execute `npm run build`. -5. Finally, start the app by running `npm rum start` +5. Finally, start the app by running `npm run start` **Note**: Using Docker is recommended as it simplifies the setup process, especially for managing environment variables and dependencies. @@ -132,7 +135,7 @@ If you're encountering an Ollama connection error, it is likely due to the backe 3. **Linux Users - Expose Ollama to Network:** - - Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0"`. Then restart Ollama by `systemctl restart ollama`. For more information see [Ollama docs](https://github.com/ollama/ollama/blob/main/docs/faq.md#setting-environment-variables-on-linux) + - Inside `/etc/systemd/system/ollama.service`, you need to add `Environment="OLLAMA_HOST=0.0.0.0:11434"`. (Change the port number if you are using a different one.) Then reload the systemd manager configuration with `systemctl daemon-reload`, and restart Ollama by `systemctl restart ollama`. For more information see [Ollama docs](https://github.com/ollama/ollama/blob/main/docs/faq.md#setting-environment-variables-on-linux) - Ensure that the port (default is 11434) is not blocked by your firewall. @@ -153,12 +156,13 @@ For more details, check out the full documentation [here](https://github.com/Itz ## Expose Perplexica to network -You can access Perplexica over your home network by following our networking guide [here](https://github.com/ItzCrazyKns/Perplexica/blob/master/docs/installation/NETWORKING.md). +Perplexica runs on Next.js and handles all API requests. It works right away on the same network and stays accessible even with port forwarding. ## 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) +[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?referralCode=U11MRQ8U9RM4&openapp=system-fastdeploy%3FtemplateName%3Dperplexica) ## Upcoming Features diff --git a/app.dockerfile b/app.dockerfile index 57a270e..c3c0fd0 100644 --- a/app.dockerfile +++ b/app.dockerfile @@ -1,4 +1,4 @@ -FROM node:20.18.0-alpine AS builder +FROM node:20.18.0-slim AS builder WORKDIR /home/perplexica @@ -12,7 +12,10 @@ COPY public ./public RUN mkdir -p /home/perplexica/data RUN yarn build -FROM node:20.18.0-alpine +RUN yarn add --dev @vercel/ncc +RUN yarn ncc build ./src/lib/db/migrate.ts -o migrator + +FROM node:20.18.0-slim WORKDIR /home/perplexica @@ -21,7 +24,12 @@ COPY --from=builder /home/perplexica/.next/static ./public/_next/static COPY --from=builder /home/perplexica/.next/standalone ./ COPY --from=builder /home/perplexica/data ./data +COPY drizzle ./drizzle +COPY --from=builder /home/perplexica/migrator/build ./build +COPY --from=builder /home/perplexica/migrator/index.js ./migrate.js RUN mkdir /home/perplexica/uploads -CMD ["node", "server.js"] \ No newline at end of file +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh +CMD ["./entrypoint.sh"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index b702b4e..b32e0a9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,6 +16,7 @@ services: dockerfile: app.dockerfile environment: - SEARXNG_API_URL=http://searxng:8080 + - DATA_DIR=/home/perplexica ports: - 3000:3000 networks: diff --git a/docs/API/SEARCH.md b/docs/API/SEARCH.md index 3007901..b67b62b 100644 --- a/docs/API/SEARCH.md +++ b/docs/API/SEARCH.md @@ -32,7 +32,9 @@ The API accepts a JSON object in the request body, where you define the focus mo "history": [ ["human", "Hi, how are you?"], ["assistant", "I am doing well, how can I help you today?"] - ] + ], + "systemInstructions": "Focus on providing technical details about Perplexica's architecture.", + "stream": false } ``` @@ -62,6 +64,8 @@ The API accepts a JSON object in the request body, where you define the focus mo - **`query`** (string, required): The search query or question. +- **`systemInstructions`** (string, optional): Custom instructions provided by the user to guide the AI's response. These instructions are treated as user preferences and have lower priority than the system's core instructions. For example, you can specify a particular writing style, format, or focus area. + - **`history`** (array, optional): An array of message pairs representing the conversation history. Each pair consists of a role (either 'human' or 'assistant') and the message content. This allows the system to use the context of the conversation to refine results. Example: ```json @@ -71,11 +75,13 @@ The API accepts a JSON object in the request body, where you define the focus mo ] ``` +- **`stream`** (boolean, optional): When set to `true`, enables streaming responses. Default is `false`. + ### Response The response from the API includes both the final message and the sources used to generate that message. -#### Example Response +#### Standard Response (stream: false) ```json { @@ -100,6 +106,28 @@ The response from the API includes both the final message and the sources used t } ``` +#### Streaming Response (stream: true) + +When streaming is enabled, the API returns a stream of newline-delimited JSON objects. Each line contains a complete, valid JSON object. The response has Content-Type: application/json. + +Example of streamed response objects: + +``` +{"type":"init","data":"Stream connected"} +{"type":"sources","data":[{"pageContent":"...","metadata":{"title":"...","url":"..."}},...]} +{"type":"response","data":"Perplexica is an "} +{"type":"response","data":"innovative, open-source "} +{"type":"response","data":"AI-powered search engine..."} +{"type":"done"} +``` + +Clients should process each line as a separate JSON object. The different message types include: + +- **`init`**: Initial connection message +- **`sources`**: All sources used for the response +- **`response`**: Chunks of the generated answer text +- **`done`**: Indicates the stream is complete + ### Fields in the Response - **`message`** (string): The search result, generated based on the query and focus mode. diff --git a/docs/installation/UPDATING.md b/docs/installation/UPDATING.md index 972142f..66edf5c 100644 --- a/docs/installation/UPDATING.md +++ b/docs/installation/UPDATING.md @@ -41,6 +41,6 @@ To update Perplexica to the latest version, follow these steps: 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. After populating the configuration run `npm i`. 5. Install the dependencies and then execute `npm run build`. -6. Finally, start the app by running `npm rum start` +6. Finally, start the app by running `npm run start` --- diff --git a/drizzle.config.ts b/drizzle.config.ts index 58de9e0..a029112 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,11 @@ import { defineConfig } from 'drizzle-kit'; +import path from 'path'; export default defineConfig({ dialect: 'sqlite', schema: './src/lib/db/schema.ts', out: './drizzle', dbCredentials: { - url: './data/db.sqlite', + url: path.join(process.cwd(), 'data', 'db.sqlite'), }, }); diff --git a/drizzle/0000_fuzzy_randall.sql b/drizzle/0000_fuzzy_randall.sql new file mode 100644 index 0000000..0a2ff07 --- /dev/null +++ b/drizzle/0000_fuzzy_randall.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `chats` ( + `id` text PRIMARY KEY NOT NULL, + `title` text NOT NULL, + `createdAt` text NOT NULL, + `focusMode` text NOT NULL, + `files` text DEFAULT '[]' +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `messages` ( + `id` integer PRIMARY KEY NOT NULL, + `content` text NOT NULL, + `chatId` text NOT NULL, + `messageId` text NOT NULL, + `type` text, + `metadata` text +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..850bcd3 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,116 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ef3a044b-0f34-40b5-babb-2bb3a909ba27", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "focusMode": { + "name": "focusMode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "files": { + "name": "files", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageId": { + "name": "messageId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..5db59d1 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1748405503809, + "tag": "0000_fuzzy_randall", + "breakpoints": true + } + ] +} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..9f9448a --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +node migrate.js + +exec node server.js \ No newline at end of file diff --git a/package.json b/package.json index e2cf944..5715c2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "perplexica-frontend", - "version": "1.10.0", + "version": "1.11.0-rc2", "license": "MIT", "author": "ItzCrazyKns", "scripts": { @@ -15,9 +15,13 @@ "@headlessui/react": "^2.2.0", "@iarna/toml": "^2.2.5", "@icons-pack/react-simple-icons": "^12.3.0", - "@langchain/community": "^0.3.36", - "@langchain/core": "^0.3.42", - "@langchain/openai": "^0.0.25", + "@langchain/anthropic": "^0.3.24", + "@langchain/community": "^0.3.49", + "@langchain/core": "^0.3.66", + "@langchain/google-genai": "^0.2.15", + "@langchain/groq": "^0.2.3", + "@langchain/ollama": "^0.2.3", + "@langchain/openai": "^0.6.2", "@langchain/textsplitters": "^0.1.0", "@tailwindcss/typography": "^0.5.12", "@xenova/transformers": "^2.17.2", @@ -28,8 +32,10 @@ "compute-dot": "^1.1.0", "drizzle-orm": "^0.40.1", "html-to-text": "^9.0.5", - "langchain": "^0.1.30", + "jspdf": "^3.0.1", + "langchain": "^0.3.30", "lucide-react": "^0.363.0", + "mammoth": "^1.9.1", "markdown-to-jsx": "^7.7.2", "next": "^15.2.2", "next-themes": "^0.3.0", @@ -47,6 +53,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/html-to-text": "^9.0.4", + "@types/jspdf": "^2.0.0", "@types/node": "^20", "@types/pdf-parse": "^1.1.4", "@types/react": "^18", diff --git a/public/icon-100.png b/public/icon-100.png new file mode 100644 index 0000000..98fa242 Binary files /dev/null and b/public/icon-100.png differ diff --git a/public/icon-50.png b/public/icon-50.png new file mode 100644 index 0000000..9bb7a0e Binary files /dev/null and b/public/icon-50.png differ diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..f6fe3c7 Binary files /dev/null and b/public/icon.png differ diff --git a/public/screenshots/p1.png b/public/screenshots/p1.png new file mode 100644 index 0000000..02f01e5 Binary files /dev/null and b/public/screenshots/p1.png differ diff --git a/public/screenshots/p1_small.png b/public/screenshots/p1_small.png new file mode 100644 index 0000000..13d9a42 Binary files /dev/null and b/public/screenshots/p1_small.png differ diff --git a/public/screenshots/p2.png b/public/screenshots/p2.png new file mode 100644 index 0000000..1171675 Binary files /dev/null and b/public/screenshots/p2.png differ diff --git a/public/screenshots/p2_small.png b/public/screenshots/p2_small.png new file mode 100644 index 0000000..bd8d673 Binary files /dev/null and b/public/screenshots/p2_small.png differ diff --git a/public/weather-ico/clear-day.svg b/public/weather-ico/clear-day.svg new file mode 100644 index 0000000..d97d28b --- /dev/null +++ b/public/weather-ico/clear-day.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/clear-night.svg b/public/weather-ico/clear-night.svg new file mode 100644 index 0000000..005ac63 --- /dev/null +++ b/public/weather-ico/clear-night.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/cloudy-1-day.svg b/public/weather-ico/cloudy-1-day.svg new file mode 100644 index 0000000..823fea1 --- /dev/null +++ b/public/weather-ico/cloudy-1-day.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/cloudy-1-night.svg b/public/weather-ico/cloudy-1-night.svg new file mode 100644 index 0000000..3fe1541 --- /dev/null +++ b/public/weather-ico/cloudy-1-night.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/fog-day.svg b/public/weather-ico/fog-day.svg new file mode 100644 index 0000000..ed834cf --- /dev/null +++ b/public/weather-ico/fog-day.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + F + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/fog-night.svg b/public/weather-ico/fog-night.svg new file mode 100644 index 0000000..d59f98f --- /dev/null +++ b/public/weather-ico/fog-night.svg @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/frost-day.svg b/public/weather-ico/frost-day.svg new file mode 100644 index 0000000..16d591c --- /dev/null +++ b/public/weather-ico/frost-day.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + F + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/frost-night.svg b/public/weather-ico/frost-night.svg new file mode 100644 index 0000000..ff2c8dc --- /dev/null +++ b/public/weather-ico/frost-night.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rain-and-sleet-mix.svg b/public/weather-ico/rain-and-sleet-mix.svg new file mode 100644 index 0000000..172010d --- /dev/null +++ b/public/weather-ico/rain-and-sleet-mix.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rainy-1-day.svg b/public/weather-ico/rainy-1-day.svg new file mode 100644 index 0000000..2faf06e --- /dev/null +++ b/public/weather-ico/rainy-1-day.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rainy-1-night.svg b/public/weather-ico/rainy-1-night.svg new file mode 100644 index 0000000..ee8ffd8 --- /dev/null +++ b/public/weather-ico/rainy-1-night.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rainy-2-day.svg b/public/weather-ico/rainy-2-day.svg new file mode 100644 index 0000000..affdfff --- /dev/null +++ b/public/weather-ico/rainy-2-day.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rainy-2-night.svg b/public/weather-ico/rainy-2-night.svg new file mode 100644 index 0000000..9c3ae20 --- /dev/null +++ b/public/weather-ico/rainy-2-night.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rainy-3-day.svg b/public/weather-ico/rainy-3-day.svg new file mode 100644 index 0000000..b0b5754 --- /dev/null +++ b/public/weather-ico/rainy-3-day.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/rainy-3-night.svg b/public/weather-ico/rainy-3-night.svg new file mode 100644 index 0000000..4078e7d --- /dev/null +++ b/public/weather-ico/rainy-3-night.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/scattered-thunderstorms-day.svg b/public/weather-ico/scattered-thunderstorms-day.svg new file mode 100644 index 0000000..0cfbccc --- /dev/null +++ b/public/weather-ico/scattered-thunderstorms-day.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/scattered-thunderstorms-night.svg b/public/weather-ico/scattered-thunderstorms-night.svg new file mode 100644 index 0000000..72cf7a6 --- /dev/null +++ b/public/weather-ico/scattered-thunderstorms-night.svg @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/severe-thunderstorm.svg b/public/weather-ico/severe-thunderstorm.svg new file mode 100644 index 0000000..223198b --- /dev/null +++ b/public/weather-ico/severe-thunderstorm.svg @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/snowy-1-day.svg b/public/weather-ico/snowy-1-day.svg new file mode 100644 index 0000000..fb73943 --- /dev/null +++ b/public/weather-ico/snowy-1-day.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/snowy-1-night.svg b/public/weather-ico/snowy-1-night.svg new file mode 100644 index 0000000..039ea2e --- /dev/null +++ b/public/weather-ico/snowy-1-night.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/snowy-2-day.svg b/public/weather-ico/snowy-2-day.svg new file mode 100644 index 0000000..323a616 --- /dev/null +++ b/public/weather-ico/snowy-2-day.svg @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/snowy-2-night.svg b/public/weather-ico/snowy-2-night.svg new file mode 100644 index 0000000..10dcbfa --- /dev/null +++ b/public/weather-ico/snowy-2-night.svg @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/snowy-3-day.svg b/public/weather-ico/snowy-3-day.svg new file mode 100644 index 0000000..846c17a --- /dev/null +++ b/public/weather-ico/snowy-3-day.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/weather-ico/snowy-3-night.svg b/public/weather-ico/snowy-3-night.svg new file mode 100644 index 0000000..b3c8c24 --- /dev/null +++ b/public/weather-ico/snowy-3-night.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample.config.toml b/sample.config.toml index 691b964..ba3e98e 100644 --- a/sample.config.toml +++ b/sample.config.toml @@ -22,5 +22,14 @@ MODEL_NAME = "" [MODELS.OLLAMA] API_URL = "" # Ollama API URL - http://host.docker.internal:11434 +[MODELS.DEEPSEEK] +API_KEY = "" + +[MODELS.AIMLAPI] +API_KEY = "" # Required to use AI/ML API chat and embedding models + +[MODELS.LM_STUDIO] +API_URL = "" # LM Studio API URL - http://host.docker.internal:1234 + [API_ENDPOINTS] -SEARXNG = "" # SearxNG API URL - http://localhost:32768 \ No newline at end of file +SEARXNG = "" # SearxNG API URL - http://localhost:32768 diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index d9f9c6b..ba88da6 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,11 +1,7 @@ -import prompts from '@/lib/prompts'; -import MetaSearchAgent from '@/lib/search/metaSearchAgent'; import crypto from 'crypto'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { EventEmitter } from 'stream'; import { - chatModelProviders, - embeddingModelProviders, getAvailableChatModelProviders, getAvailableEmbeddingModelProviders, } from '@/lib/providers'; @@ -49,6 +45,7 @@ type Body = { files: Array; chatModel: ChatModel; embeddingModel: EmbeddingModel; + systemInstructions: string; }; const handleEmitterEvents = async ( @@ -137,6 +134,8 @@ const handleHistorySave = async ( where: eq(chats.id, message.chatId), }); + const fileData = files.map(getFileDetails); + if (!chat) { await db .insert(chats) @@ -145,9 +144,15 @@ const handleHistorySave = async ( title: message.content, createdAt: new Date().toString(), focusMode: focusMode, - files: files.map(getFileDetails), + files: fileData, }) .execute(); + } else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) { + db.update(chats) + .set({ + files: files.map(getFileDetails), + }) + .where(eq(chats.id, message.chatId)); } const messageExists = await db.query.messages.findFirst({ @@ -222,7 +227,7 @@ export const POST = async (req: Request) => { if (body.chatModel?.provider === 'custom_openai') { llm = new ChatOpenAI({ - openAIApiKey: getCustomOpenaiApiKey(), + apiKey: getCustomOpenaiApiKey(), modelName: getCustomOpenaiModelName(), temperature: 0.7, configuration: { @@ -278,6 +283,7 @@ export const POST = async (req: Request) => { embedding, body.optimizationMode, body.files, + body.systemInstructions, ); const responseStream = new TransformStream(); @@ -295,9 +301,9 @@ export const POST = async (req: Request) => { }, }); } catch (err) { - console.error('An error ocurred while processing chat request:', err); + console.error('An error occurred while processing chat request:', err); return Response.json( - { message: 'An error ocurred while processing chat request' }, + { message: 'An error occurred while processing chat request' }, { status: 500 }, ); } diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index 46c71f5..f117cce 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -7,7 +7,11 @@ import { getGroqApiKey, getOllamaApiEndpoint, getOpenaiApiKey, + getDeepseekApiKey, + getAimlApiKey, + getLMStudioApiEndpoint, updateConfig, + getOllamaApiKey, } from '@/lib/config'; import { getAvailableChatModelProviders, @@ -50,18 +54,22 @@ export const GET = async (req: Request) => { config['openaiApiKey'] = getOpenaiApiKey(); config['ollamaApiUrl'] = getOllamaApiEndpoint(); + config['ollamaApiKey'] = getOllamaApiKey(); + config['lmStudioApiUrl'] = getLMStudioApiEndpoint(); config['anthropicApiKey'] = getAnthropicApiKey(); config['groqApiKey'] = getGroqApiKey(); config['geminiApiKey'] = getGeminiApiKey(); + config['deepseekApiKey'] = getDeepseekApiKey(); + config['aimlApiKey'] = getAimlApiKey(); config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl(); config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); config['customOpenaiModelName'] = getCustomOpenaiModelName(); return Response.json({ ...config }, { status: 200 }); } catch (err) { - console.error('An error ocurred while getting config:', err); + console.error('An error occurred while getting config:', err); return Response.json( - { message: 'An error ocurred while getting config' }, + { message: 'An error occurred while getting config' }, { status: 500 }, ); } @@ -87,6 +95,16 @@ export const POST = async (req: Request) => { }, OLLAMA: { API_URL: config.ollamaApiUrl, + API_KEY: config.ollamaApiKey, + }, + DEEPSEEK: { + API_KEY: config.deepseekApiKey, + }, + AIMLAPI: { + API_KEY: config.aimlApiKey, + }, + LM_STUDIO: { + API_URL: config.lmStudioApiUrl, }, CUSTOM_OPENAI: { API_URL: config.customOpenaiApiUrl, @@ -100,9 +118,9 @@ export const POST = async (req: Request) => { return Response.json({ message: 'Config updated' }, { status: 200 }); } catch (err) { - console.error('An error ocurred while updating config:', err); + console.error('An error occurred while updating config:', err); return Response.json( - { message: 'An error ocurred while updating config' }, + { message: 'An error occurred while updating config' }, { status: 500 }, ); } diff --git a/src/app/api/discover/route.ts b/src/app/api/discover/route.ts index 0c95498..415aee8 100644 --- a/src/app/api/discover/route.ts +++ b/src/app/api/discover/route.ts @@ -1,43 +1,80 @@ import { searchSearxng } from '@/lib/searxng'; -const articleWebsites = [ - 'yahoo.com', - 'www.exchangewire.com', - 'businessinsider.com', - /* 'wired.com', - 'mashable.com', - 'theverge.com', - 'gizmodo.com', - 'cnet.com', - 'venturebeat.com', */ -]; +const websitesForTopic = { + tech: { + query: ['technology news', 'latest tech', 'AI', 'science and innovation'], + links: ['techcrunch.com', 'wired.com', 'theverge.com'], + }, + finance: { + query: ['finance news', 'economy', 'stock market', 'investing'], + links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'], + }, + art: { + query: ['art news', 'culture', 'modern art', 'cultural events'], + links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'], + }, + sports: { + query: ['sports news', 'latest sports', 'cricket football tennis'], + links: ['espn.com', 'bbc.com/sport', 'skysports.com'], + }, + entertainment: { + query: ['entertainment news', 'movies', 'TV shows', 'celebrities'], + links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'], + }, +}; -const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */ +type Topic = keyof typeof websitesForTopic; export const GET = async (req: Request) => { try { - const data = ( - await Promise.all([ - ...new Array(articleWebsites.length * topics.length) - .fill(0) - .map(async (_, i) => { - return ( - await searchSearxng( - `site:${articleWebsites[i % articleWebsites.length]} ${ - topics[i % topics.length] - }`, - { + const params = new URL(req.url).searchParams; + + const mode: 'normal' | 'preview' = + (params.get('mode') as 'normal' | 'preview') || 'normal'; + const topic: Topic = (params.get('topic') as Topic) || 'tech'; + + const selectedTopic = websitesForTopic[topic]; + + let data = []; + + if (mode === 'normal') { + const seenUrls = new Set(); + + data = ( + await Promise.all( + selectedTopic.links.flatMap((link) => + selectedTopic.query.map(async (query) => { + return ( + await searchSearxng(`site:${link} ${query}`, { engines: ['bing news'], pageno: 1, - }, - ) - ).results; - }), - ]) - ) - .map((result) => result) - .flat() - .sort(() => Math.random() - 0.5); + language: 'en', + }) + ).results; + }), + ), + ) + ) + .flat() + .filter((item) => { + const url = item.url?.toLowerCase().trim(); + if (seenUrls.has(url)) return false; + seenUrls.add(url); + return true; + }) + .sort(() => Math.random() - 0.5); + } else { + data = ( + await searchSearxng( + `site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`, + { + engines: ['bing news'], + pageno: 1, + language: 'en', + }, + ) + ).results; + } return Response.json( { @@ -48,7 +85,7 @@ export const GET = async (req: Request) => { }, ); } catch (err) { - console.error(`An error ocurred in discover route: ${err}`); + console.error(`An error occurred in discover route: ${err}`); return Response.json( { message: 'An error has occurred', diff --git a/src/app/api/images/route.ts b/src/app/api/images/route.ts index f0a6773..e02854d 100644 --- a/src/app/api/images/route.ts +++ b/src/app/api/images/route.ts @@ -49,7 +49,7 @@ export const POST = async (req: Request) => { if (body.chatModel?.provider === 'custom_openai') { llm = new ChatOpenAI({ - openAIApiKey: getCustomOpenaiApiKey(), + apiKey: getCustomOpenaiApiKey(), modelName: getCustomOpenaiModelName(), temperature: 0.7, configuration: { @@ -74,9 +74,9 @@ export const POST = async (req: Request) => { return Response.json({ images }, { status: 200 }); } catch (err) { - console.error(`An error ocurred while searching images: ${err}`); + console.error(`An error occurred while searching images: ${err}`); return Response.json( - { message: 'An error ocurred while searching images' }, + { message: 'An error occurred while searching images' }, { status: 500 }, ); } diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index a5e5b43..04a6949 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -34,7 +34,7 @@ export const GET = async (req: Request) => { }, ); } catch (err) { - console.error('An error ocurred while fetching models', err); + console.error('An error occurred while fetching models', err); return Response.json( { message: 'An error has occurred.', diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index b980623..5f752ec 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -33,6 +33,8 @@ interface ChatRequestBody { embeddingModel?: embeddingModel; query: string; history: Array<[string, string]>; + stream?: boolean; + systemInstructions?: string; } export const POST = async (req: Request) => { @@ -48,6 +50,7 @@ export const POST = async (req: Request) => { body.history = body.history || []; body.optimizationMode = body.optimizationMode || 'balanced'; + body.stream = body.stream || false; const history: BaseMessage[] = body.history.map((msg) => { return msg[0] === 'human' @@ -78,8 +81,7 @@ export const POST = async (req: Request) => { if (body.chatModel?.provider === 'custom_openai') { llm = new ChatOpenAI({ modelName: body.chatModel?.name || getCustomOpenaiModelName(), - openAIApiKey: - body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), + apiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), temperature: 0.7, configuration: { baseURL: @@ -123,42 +125,140 @@ export const POST = async (req: Request) => { embeddings, body.optimizationMode, [], + body.systemInstructions || '', ); - return new Promise( - ( - resolve: (value: Response) => void, - reject: (value: Response) => void, - ) => { - let message = ''; + if (!body.stream) { + return new Promise( + ( + resolve: (value: Response) => void, + reject: (value: Response) => void, + ) => { + let message = ''; + let sources: any[] = []; + + emitter.on('data', (data: string) => { + try { + const parsedData = JSON.parse(data); + if (parsedData.type === 'response') { + message += parsedData.data; + } else if (parsedData.type === 'sources') { + sources = parsedData.data; + } + } catch (error) { + reject( + Response.json( + { message: 'Error parsing data' }, + { status: 500 }, + ), + ); + } + }); + + emitter.on('end', () => { + resolve(Response.json({ message, sources }, { status: 200 })); + }); + + emitter.on('error', (error: any) => { + reject( + Response.json( + { message: 'Search error', error }, + { status: 500 }, + ), + ); + }); + }, + ); + } + + const encoder = new TextEncoder(); + + const abortController = new AbortController(); + const { signal } = abortController; + + const stream = new ReadableStream({ + start(controller) { let sources: any[] = []; - emitter.on('data', (data) => { + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'init', + data: 'Stream connected', + }) + '\n', + ), + ); + + signal.addEventListener('abort', () => { + emitter.removeAllListeners(); + + try { + controller.close(); + } catch (error) {} + }); + + emitter.on('data', (data: string) => { + if (signal.aborted) return; + try { const parsedData = JSON.parse(data); + if (parsedData.type === 'response') { - message += parsedData.data; + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'response', + data: parsedData.data, + }) + '\n', + ), + ); } else if (parsedData.type === 'sources') { sources = parsedData.data; + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'sources', + data: sources, + }) + '\n', + ), + ); } } catch (error) { - reject( - Response.json({ message: 'Error parsing data' }, { status: 500 }), - ); + controller.error(error); } }); emitter.on('end', () => { - resolve(Response.json({ message, sources }, { status: 200 })); + if (signal.aborted) return; + + controller.enqueue( + encoder.encode( + JSON.stringify({ + type: 'done', + }) + '\n', + ), + ); + controller.close(); }); - emitter.on('error', (error) => { - reject( - Response.json({ message: 'Search error', error }, { status: 500 }), - ); + emitter.on('error', (error: any) => { + if (signal.aborted) return; + + controller.error(error); }); }, - ); + cancel() { + abortController.abort(); + }, + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive', + }, + }); } catch (err: any) { console.error(`Error in getting search results: ${err.message}`); return Response.json( diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts index 4a931df..99179d2 100644 --- a/src/app/api/suggestions/route.ts +++ b/src/app/api/suggestions/route.ts @@ -48,7 +48,7 @@ export const POST = async (req: Request) => { if (body.chatModel?.provider === 'custom_openai') { llm = new ChatOpenAI({ - openAIApiKey: getCustomOpenaiApiKey(), + apiKey: getCustomOpenaiApiKey(), modelName: getCustomOpenaiModelName(), temperature: 0.7, configuration: { @@ -72,9 +72,9 @@ export const POST = async (req: Request) => { return Response.json({ suggestions }, { status: 200 }); } catch (err) { - console.error(`An error ocurred while generating suggestions: ${err}`); + console.error(`An error occurred while generating suggestions: ${err}`); return Response.json( - { message: 'An error ocurred while generating suggestions' }, + { message: 'An error occurred while generating suggestions' }, { status: 500 }, ); } diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts index 6153490..7e8288b 100644 --- a/src/app/api/videos/route.ts +++ b/src/app/api/videos/route.ts @@ -49,7 +49,7 @@ export const POST = async (req: Request) => { if (body.chatModel?.provider === 'custom_openai') { llm = new ChatOpenAI({ - openAIApiKey: getCustomOpenaiApiKey(), + apiKey: getCustomOpenaiApiKey(), modelName: getCustomOpenaiModelName(), temperature: 0.7, configuration: { @@ -74,9 +74,9 @@ export const POST = async (req: Request) => { return Response.json({ videos }, { status: 200 }); } catch (err) { - console.error(`An error ocurred while searching videos: ${err}`); + console.error(`An error occurred while searching videos: ${err}`); return Response.json( - { message: 'An error ocurred while searching videos' }, + { message: 'An error occurred while searching videos' }, { status: 500 }, ); } diff --git a/src/app/api/weather/route.ts b/src/app/api/weather/route.ts new file mode 100644 index 0000000..afaf8a6 --- /dev/null +++ b/src/app/api/weather/route.ts @@ -0,0 +1,174 @@ +export const POST = async (req: Request) => { + try { + const body: { + lat: number; + lng: number; + measureUnit: 'Imperial' | 'Metric'; + } = await req.json(); + + if (!body.lat || !body.lng) { + return Response.json( + { + message: 'Invalid request.', + }, + { status: 400 }, + ); + } + + const res = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${ + body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit' + }${body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph'}`, + ); + + const data = await res.json(); + + if (data.error) { + console.error(`Error fetching weather data: ${data.reason}`); + return Response.json( + { + message: 'An error has occurred.', + }, + { status: 500 }, + ); + } + + const weather: { + temperature: number; + condition: string; + humidity: number; + windSpeed: number; + icon: string; + temperatureUnit: 'C' | 'F'; + windSpeedUnit: 'm/s' | 'mph'; + } = { + temperature: data.current.temperature_2m, + condition: '', + humidity: data.current.relative_humidity_2m, + windSpeed: data.current.wind_speed_10m, + icon: '', + temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F', + windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph', + }; + + const code = data.current.weather_code; + const isDay = data.current.is_day === 1; + const dayOrNight = isDay ? 'day' : 'night'; + + switch (code) { + case 0: + weather.icon = `clear-${dayOrNight}`; + weather.condition = 'Clear'; + break; + + case 1: + weather.condition = 'Mainly Clear'; + case 2: + weather.condition = 'Partly Cloudy'; + case 3: + weather.icon = `cloudy-1-${dayOrNight}`; + weather.condition = 'Cloudy'; + break; + + case 45: + weather.condition = 'Fog'; + case 48: + weather.icon = `fog-${dayOrNight}`; + weather.condition = 'Fog'; + break; + + case 51: + weather.condition = 'Light Drizzle'; + case 53: + weather.condition = 'Moderate Drizzle'; + case 55: + weather.icon = `rainy-1-${dayOrNight}`; + weather.condition = 'Dense Drizzle'; + break; + + case 56: + weather.condition = 'Light Freezing Drizzle'; + case 57: + weather.icon = `frost-${dayOrNight}`; + weather.condition = 'Dense Freezing Drizzle'; + break; + + case 61: + weather.condition = 'Slight Rain'; + case 63: + weather.condition = 'Moderate Rain'; + case 65: + weather.condition = 'Heavy Rain'; + weather.icon = `rainy-2-${dayOrNight}`; + break; + + case 66: + weather.condition = 'Light Freezing Rain'; + case 67: + weather.condition = 'Heavy Freezing Rain'; + weather.icon = 'rain-and-sleet-mix'; + break; + + case 71: + weather.condition = 'Slight Snow Fall'; + case 73: + weather.condition = 'Moderate Snow Fall'; + case 75: + weather.condition = 'Heavy Snow Fall'; + weather.icon = `snowy-2-${dayOrNight}`; + break; + + case 77: + weather.condition = 'Snow'; + weather.icon = `snowy-1-${dayOrNight}`; + break; + + case 80: + weather.condition = 'Slight Rain Showers'; + case 81: + weather.condition = 'Moderate Rain Showers'; + case 82: + weather.condition = 'Heavy Rain Showers'; + weather.icon = `rainy-3-${dayOrNight}`; + break; + + case 85: + weather.condition = 'Slight Snow Showers'; + case 86: + weather.condition = 'Moderate Snow Showers'; + case 87: + weather.condition = 'Heavy Snow Showers'; + weather.icon = `snowy-3-${dayOrNight}`; + break; + + case 95: + weather.condition = 'Thunderstorm'; + weather.icon = `scattered-thunderstorms-${dayOrNight}`; + break; + + case 96: + weather.condition = 'Thunderstorm with Slight Hail'; + case 99: + weather.condition = 'Thunderstorm with Heavy Hail'; + weather.icon = 'severe-thunderstorm'; + break; + + default: + weather.icon = `clear-${dayOrNight}`; + weather.condition = 'Clear'; + break; + } + + return Response.json(weather); + } catch (err) { + console.error('An error occurred while getting home widgets', err); + return Response.json( + { + message: 'An error has occurred.', + }, + { + status: 500, + }, + ); + } +}; diff --git a/src/app/c/[chatId]/page.tsx b/src/app/c/[chatId]/page.tsx index aac125a..672107a 100644 --- a/src/app/c/[chatId]/page.tsx +++ b/src/app/c/[chatId]/page.tsx @@ -1,9 +1,17 @@ -import ChatWindow from '@/components/ChatWindow'; -import React from 'react'; +'use client'; -const Page = ({ params }: { params: Promise<{ chatId: string }> }) => { - const { chatId } = React.use(params); - return ; +import ChatWindow from '@/components/ChatWindow'; +import { useParams } from 'next/navigation'; +import React from 'react'; +import { ChatProvider } from '@/lib/hooks/useChat'; + +const Page = () => { + const { chatId }: { chatId: string } = useParams(); + return ( + + + + ); }; export default Page; diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index eb7de7f..8e20e50 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -4,6 +4,7 @@ import { Search } from 'lucide-react'; import { useEffect, useState } from 'react'; import Link from 'next/link'; import { toast } from 'sonner'; +import { cn } from '@/lib/utils'; interface Discover { title: string; @@ -12,60 +13,66 @@ interface Discover { thumbnail: string; } +const topics: { key: string; display: string }[] = [ + { + display: 'Tech & Science', + key: 'tech', + }, + { + display: 'Finance', + key: 'finance', + }, + { + display: 'Art & Culture', + key: 'art', + }, + { + display: 'Sports', + key: 'sports', + }, + { + display: 'Entertainment', + key: 'entertainment', + }, +]; + const Page = () => { const [discover, setDiscover] = useState(null); const [loading, setLoading] = useState(true); + const [activeTopic, setActiveTopic] = useState(topics[0].key); + + const fetchArticles = async (topic: string) => { + setLoading(true); + try { + const res = await fetch(`/api/discover?topic=${topic}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message); + } + + data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); + + setDiscover(data.blogs); + } catch (err: any) { + console.error('Error fetching data:', err.message); + toast.error('Error fetching data'); + } finally { + setLoading(false); + } + }; useEffect(() => { - const fetchData = async () => { - try { - const res = await fetch(`/api/discover`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + fetchArticles(activeTopic); + }, [activeTopic]); - const data = await res.json(); - - if (!res.ok) { - throw new Error(data.message); - } - - data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); - - setDiscover(data.blogs); - } catch (err: any) { - console.error('Error fetching data:', err.message); - toast.error('Error fetching data'); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - return loading ? ( -
- -
- ) : ( + return ( <>
@@ -76,35 +83,73 @@ const Page = () => {
-
- {discover && - discover?.map((item, i) => ( - - {item.title} -
-
- {item.title.slice(0, 100)}... -
-

- {item.content.slice(0, 100)}... -

-
- - ))} +
+ {topics.map((t, i) => ( +
setActiveTopic(t.key)} + > + {t.display} +
+ ))}
+ + {loading ? ( +
+ +
+ ) : ( +
+ {discover && + discover?.map((item, i) => ( + + {item.title} +
+
+ {item.title.slice(0, 100)}... +
+

+ {item.content.slice(0, 100)}... +

+
+ + ))} +
+ )}
); diff --git a/src/app/globals.css b/src/app/globals.css index f75daca..6bdc1a8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -11,3 +11,11 @@ display: none; } } + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + select, + textarea, + input { + font-size: 16px !important; + } +} diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..792e752 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,54 @@ +import type { MetadataRoute } from 'next'; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: 'Perplexica - Chat with the internet', + short_name: 'Perplexica', + description: + 'Perplexica is an AI powered chatbot that is connected to the internet.', + start_url: '/', + display: 'standalone', + background_color: '#0a0a0a', + theme_color: '#0a0a0a', + screenshots: [ + { + src: '/screenshots/p1.png', + form_factor: 'wide', + sizes: '2560x1600', + }, + { + src: '/screenshots/p2.png', + form_factor: 'wide', + sizes: '2560x1600', + }, + { + src: '/screenshots/p1_small.png', + form_factor: 'narrow', + sizes: '828x1792', + }, + { + src: '/screenshots/p2_small.png', + form_factor: 'narrow', + sizes: '828x1792', + }, + ], + icons: [ + { + src: '/icon-50.png', + sizes: '50x50', + type: 'image/png' as const, + }, + { + src: '/icon-100.png', + sizes: '100x100', + type: 'image/png', + }, + { + src: '/icon.png', + sizes: '440x440', + type: 'image/png', + purpose: 'any', + }, + ], + }; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e18aca9..25981b5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,5 @@ import ChatWindow from '@/components/ChatWindow'; +import { ChatProvider } from '@/lib/hooks/useChat'; import { Metadata } from 'next'; import { Suspense } from 'react'; @@ -11,7 +12,9 @@ const Home = () => { return (
- + + +
); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index efe54d5..6fb8255 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -7,6 +7,7 @@ import { Switch } from '@headlessui/react'; import ThemeSwitcher from '@/components/theme/Switcher'; import { ImagesIcon, VideoIcon } from 'lucide-react'; import Link from 'next/link'; +import { PROVIDER_METADATA } from '@/lib/providers'; interface SettingsType { chatModelProviders: { @@ -20,6 +21,10 @@ interface SettingsType { anthropicApiKey: string; geminiApiKey: string; ollamaApiUrl: string; + ollamaApiKey: string; + lmStudioApiUrl: string; + deepseekApiKey: string; + aimlApiKey: string; customOpenaiApiKey: string; customOpenaiApiUrl: string; customOpenaiModelName: string; @@ -54,6 +59,38 @@ const Input = ({ className, isSaving, onSave, ...restProps }: InputProps) => { ); }; +interface TextareaProps extends React.InputHTMLAttributes { + isSaving?: boolean; + onSave?: (value: string) => void; +} + +const Textarea = ({ + className, + isSaving, + onSave, + ...restProps +}: TextareaProps) => { + return ( +
+