mirror of
https://github.com/khoj-ai/khoj.git
synced 2026-05-13 21:41:41 +00:00
Compare commits
330 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9258f57dce | ||
|
|
171ac5d243 | ||
|
|
fdd5fd8f74 | ||
|
|
8965db7087 | ||
|
|
8b8504edb8 | ||
|
|
7475a781bc | ||
|
|
a19e7acd5a | ||
|
|
b8797e00fa | ||
|
|
f356386f3a | ||
|
|
d4df9a73ec | ||
|
|
f7bce48934 | ||
|
|
b8f82b27f5 | ||
|
|
a9749c7184 | ||
|
|
51a56af7ca | ||
|
|
7264ebf533 | ||
|
|
0e169159f8 | ||
|
|
530443a4f6 | ||
|
|
e863126140 | ||
|
|
678549c6b0 | ||
|
|
6735d33af2 | ||
|
|
0e9878c070 | ||
|
|
2c82967807 | ||
|
|
17be2d4800 | ||
|
|
aeea140099 | ||
|
|
b864cb1f30 | ||
|
|
0b8cf5112f | ||
|
|
94bae4789a | ||
|
|
5a51f17a71 | ||
|
|
b0cd8dc8fd | ||
|
|
0ba0f3d0f2 | ||
|
|
60a2f6d4da | ||
|
|
9dfef3f40b | ||
|
|
44b9240253 | ||
|
|
21c51b9ace | ||
|
|
9cbf620e45 | ||
|
|
1fd6e16cff | ||
|
|
d6c2d1fa49 | ||
|
|
d55a00288b | ||
|
|
ff4b9f3502 | ||
|
|
19900e42ef | ||
|
|
a58ae3dd84 | ||
|
|
b607a6187e | ||
|
|
f413ce7354 | ||
|
|
9b9cdc756f | ||
|
|
c5650f166a | ||
|
|
1b7ccd141d | ||
|
|
b8eeefa0b1 | ||
|
|
9801ffd2de | ||
|
|
5e65754a8b | ||
|
|
f65f6ae848 | ||
|
|
446a23524c | ||
|
|
cdcbdf8459 | ||
|
|
054ed79fdf | ||
|
|
f4c519a9d0 | ||
|
|
6480e99266 | ||
|
|
181332dcb8 | ||
|
|
9c03af2735 | ||
|
|
4654ac4962 | ||
|
|
f1337c3b07 | ||
|
|
bdf9afa726 | ||
|
|
5b6dab1627 | ||
|
|
3941159bd6 | ||
|
|
e2340c709f | ||
|
|
856864147b | ||
|
|
c41e37d734 | ||
|
|
731700ac43 | ||
|
|
99f16df7e2 | ||
|
|
da493be417 | ||
|
|
dd4381c25c | ||
|
|
51b893d51d | ||
|
|
32966646e2 | ||
|
|
043777c1bd | ||
|
|
47a55c20a0 | ||
|
|
6459150870 | ||
|
|
03dad1348a | ||
|
|
57d6ebb1b8 | ||
|
|
a30c5f245d | ||
|
|
ec31df7154 | ||
|
|
895af42039 | ||
|
|
748a4f9941 | ||
|
|
3496189618 | ||
|
|
2ac7359092 | ||
|
|
f1a34f0c2a | ||
|
|
45f4253120 | ||
|
|
e6a5d3dc3d | ||
|
|
0415b31a23 | ||
|
|
61cb2d5b7e | ||
|
|
d57c597245 | ||
|
|
15482c54b5 | ||
|
|
761af5f98c | ||
|
|
1f3c1e1221 | ||
|
|
8490f2826b | ||
|
|
630ce77b5f | ||
|
|
cbeb220f00 | ||
|
|
d7e936678d | ||
|
|
4556773f42 | ||
|
|
2c54a2cd10 | ||
|
|
aab0653025 | ||
|
|
b14e6eb069 | ||
|
|
ce6d75e5a2 | ||
|
|
5760f3b534 | ||
|
|
c022e7d553 | ||
|
|
88a1fc75cc | ||
|
|
a809de8970 | ||
|
|
749bbed23d | ||
|
|
69cceda9ab | ||
|
|
140a3ef943 | ||
|
|
f2e0b62217 | ||
|
|
5ef3a3f027 | ||
|
|
6ac2280e41 | ||
|
|
1179a4c8f8 | ||
|
|
51e5c86fcc | ||
|
|
534ee32664 | ||
|
|
e854c1a5a8 | ||
|
|
2fdb1fcc93 | ||
|
|
3e699e5476 | ||
|
|
0bd4bf182c | ||
|
|
52b1928023 | ||
|
|
703e189979 | ||
|
|
edf9ea6312 | ||
|
|
d53ede604c | ||
|
|
7533e3eecf | ||
|
|
3c1948e9de | ||
|
|
3441783d5b | ||
|
|
3aa6f8ba1f | ||
|
|
0babab580a | ||
|
|
00f0d23224 | ||
|
|
81c651b5b2 | ||
|
|
c0f192b436 | ||
|
|
dd8e805cfe | ||
|
|
0a5a882e54 | ||
|
|
9395c17f34 | ||
|
|
be79b8a633 | ||
|
|
9d7adbcbaa | ||
|
|
2091044db5 | ||
|
|
7a42042488 | ||
|
|
02e220f5f5 | ||
|
|
4976b244a4 | ||
|
|
a99eb841ff | ||
|
|
a52a06ad9d | ||
|
|
e150dc5a91 | ||
|
|
15d1f39d0b | ||
|
|
05dbb6a7c1 | ||
|
|
8a16f5a2af | ||
|
|
1e81b51abc | ||
|
|
5a2cae3756 | ||
|
|
0fb6020f30 | ||
|
|
386a17371d | ||
|
|
ff004d31ef | ||
|
|
892e4d4077 | ||
|
|
00c5aec614 | ||
|
|
b99ccbc4c3 | ||
|
|
29ae476a26 | ||
|
|
c89c5c7b46 | ||
|
|
464c1546b7 | ||
|
|
40488b3b68 | ||
|
|
8aa9c0f534 | ||
|
|
2823c84bb4 | ||
|
|
c53a70c997 | ||
|
|
e2f377c27b | ||
|
|
d8b7e9c8a5 | ||
|
|
3c3205bb06 | ||
|
|
48ed7afab8 | ||
|
|
82dc7b115b | ||
|
|
7645cbea3b | ||
|
|
2e6928c582 | ||
|
|
c5e2373d73 | ||
|
|
d8b2df4107 | ||
|
|
eb2f0ec6bc | ||
|
|
2884853c98 | ||
|
|
9f6aa922a2 | ||
|
|
13d26ae8b8 | ||
|
|
fb0347a388 | ||
|
|
dbc3330610 | ||
|
|
83d725d2d8 | ||
|
|
f483a626b8 | ||
|
|
f5a4d106d1 | ||
|
|
c5a9c81479 | ||
|
|
2c91edbb25 | ||
|
|
452c794e93 | ||
|
|
9a8c707f84 | ||
|
|
e0007a31bb | ||
|
|
222cc19b7f | ||
|
|
ff73d30106 | ||
|
|
34dca8e114 | ||
|
|
8862394c15 | ||
|
|
14b4d4b663 | ||
|
|
e504141c07 | ||
|
|
573c6a32e1 | ||
|
|
4728098cad | ||
|
|
a2a3eb8be6 | ||
|
|
b3015f6837 | ||
|
|
916534226a | ||
|
|
fa143d45b9 | ||
|
|
a494a766a4 | ||
|
|
25e549d683 | ||
|
|
59bfaf9698 | ||
|
|
3eb8cce984 | ||
|
|
4274f58dbd | ||
|
|
caf0b994e8 | ||
|
|
7251b25c66 | ||
|
|
20347e21c2 | ||
|
|
bd82626084 | ||
|
|
cbeefb7f94 | ||
|
|
0a6d87067d | ||
|
|
0186403891 | ||
|
|
41f89cf7f3 | ||
|
|
b2d26088dc | ||
|
|
564adb24a7 | ||
|
|
0e1615acc8 | ||
|
|
a79025ee93 | ||
|
|
a3bb7100b4 | ||
|
|
80cce7b439 | ||
|
|
0a0b97446c | ||
|
|
f2bd07044e | ||
|
|
8ad38dfe11 | ||
|
|
b86430227c | ||
|
|
791ebe3a97 | ||
|
|
c8e07e86e4 | ||
|
|
4a3ed9e5a4 | ||
|
|
8700fb8937 | ||
|
|
d2940de367 | ||
|
|
006b958071 | ||
|
|
e0f363d718 | ||
|
|
0387b86a27 | ||
|
|
c6670e815a | ||
|
|
892d57314e | ||
|
|
d9d24dd638 | ||
|
|
b1f2737c9a | ||
|
|
3f8cc71aca | ||
|
|
9096f628d0 | ||
|
|
a6923fac76 | ||
|
|
2e13c9a007 | ||
|
|
fba4ad27f7 | ||
|
|
b335f8cf79 | ||
|
|
c0db9e4fca | ||
|
|
7ab24d875d | ||
|
|
6290d744ea | ||
|
|
0f953f9ec8 | ||
|
|
bbc14951b4 | ||
|
|
6caa6f4008 | ||
|
|
b82d4fe68f | ||
|
|
655a1b38f2 | ||
|
|
f5d12b7546 | ||
|
|
f8924f2521 | ||
|
|
bd9f091a71 | ||
|
|
624d6227ca | ||
|
|
c401bb9591 | ||
|
|
03c4f614dd | ||
|
|
70cfaf72e9 | ||
|
|
15c6118142 | ||
|
|
fc99f8b37e | ||
|
|
bf9a9c7283 | ||
|
|
48e21d9f0f | ||
|
|
e57acf617a | ||
|
|
76a1b0b686 | ||
|
|
43d7e65a49 | ||
|
|
749160e38d | ||
|
|
69a7d332fc | ||
|
|
76ddf8645c | ||
|
|
de7668daec | ||
|
|
b90e2367d5 | ||
|
|
0ecd5f497d | ||
|
|
7b7b1830b7 | ||
|
|
eaed0c839e | ||
|
|
9f0eff6541 | ||
|
|
38dd85c91f | ||
|
|
99ed796c00 | ||
|
|
0a05a5709e | ||
|
|
238bd66c42 | ||
|
|
76ed97d066 | ||
|
|
0a06f5b41a | ||
|
|
d42176fa7e | ||
|
|
d27aac7f13 | ||
|
|
05176cd62b | ||
|
|
b2952236c4 | ||
|
|
25db59e49c | ||
|
|
c8ec29551f | ||
|
|
f1a3ddf2ca | ||
|
|
7b637d3432 | ||
|
|
c28e90f388 | ||
|
|
b763dbfb2b | ||
|
|
1988a8d023 | ||
|
|
69336565b1 | ||
|
|
cc6da4c440 | ||
|
|
3141035f48 | ||
|
|
a601cca79b | ||
|
|
f2b86aa7c8 | ||
|
|
0f0cfba624 | ||
|
|
f0513cbbb1 | ||
|
|
c144aa9c90 | ||
|
|
fad6a638bd | ||
|
|
8d9e75f580 | ||
|
|
254207b010 | ||
|
|
8fb38d9e1e | ||
|
|
9a215141f0 | ||
|
|
da9a78e79b | ||
|
|
bc6bbb4c96 | ||
|
|
4c33d1a526 | ||
|
|
9dc146bb08 | ||
|
|
b27ba1d24b | ||
|
|
8cd2a1a961 | ||
|
|
6bda8dc20b | ||
|
|
531ae80212 | ||
|
|
58f44ad43b | ||
|
|
2daf396cbb | ||
|
|
afa810e552 | ||
|
|
2ec39d295d | ||
|
|
5010623a0a | ||
|
|
dcfa4288c4 | ||
|
|
e90ab5341a | ||
|
|
820b4523fd | ||
|
|
5c4d41d300 | ||
|
|
870d9d851a | ||
|
|
fe44cd3c59 | ||
|
|
f343a92b1d | ||
|
|
aa081913bf | ||
|
|
786b06bb3f | ||
|
|
30878a2fed | ||
|
|
c2ab75efef | ||
|
|
7cd496ac19 | ||
|
|
4e67ba4d6c | ||
|
|
d81fb08366 | ||
|
|
9c38326608 | ||
|
|
59f5648dbd | ||
|
|
2f9f608cff | ||
|
|
721c55a37b | ||
|
|
490f0a435d | ||
|
|
80522e370e | ||
|
|
b888d5e65e |
@@ -1,40 +1,49 @@
|
||||
ARG PYTHON_VERSION=3.10
|
||||
ARG PYTHON_VERSION=3.12
|
||||
FROM mcr.microsoft.com/devcontainers/python:${PYTHON_VERSION}
|
||||
|
||||
# Install Node.js and Yarn
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
# Install UV and Bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash && mv /root/.bun/bin/bun /usr/local/bin/bun
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
RUN uv python pin $PYTHON_VERSION
|
||||
# create python virtual environment
|
||||
RUN uv venv /opt/venv --python $PYTHON_VERSION --seed
|
||||
# Add venv to PATH for subsequent RUN commands and for the container environment
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
# Tell pip, uv to use this virtual environment
|
||||
ENV VIRTUAL_ENV="/opt/venv"
|
||||
ENV UV_PROJECT_ENVIRONMENT="/opt/venv"
|
||||
|
||||
# Setup working directory
|
||||
WORKDIR /workspace
|
||||
WORKDIR /workspaces/khoj
|
||||
|
||||
# --- Python Server App Dependencies ---
|
||||
# Create Python virtual environment
|
||||
RUN python3 -m venv /opt/venv
|
||||
# Add venv to PATH for subsequent RUN commands and for the container environment
|
||||
ENV PATH="/opt/venv/bin:${PATH}"
|
||||
|
||||
# Copy files required for Python dependency installation.
|
||||
COPY pyproject.toml README.md ./
|
||||
|
||||
# Setup python environment
|
||||
# Use the pre-built llama-cpp-python, torch cpu wheel
|
||||
ENV PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/cpu https://abetlen.github.io/llama-cpp-python/whl/cpu" \
|
||||
# Use the pre-built torch cpu wheel
|
||||
ENV UV_INDEX="https://download.pytorch.org/whl/cpu" \
|
||||
UV_INDEX_STRATEGY="unsafe-best-match" \
|
||||
# Avoid downloading unused cuda specific python packages
|
||||
CUDA_VISIBLE_DEVICES="" \
|
||||
# Use static version to build app without git dependency
|
||||
VERSION=0.0.0
|
||||
VERSION=0.0.0 \
|
||||
# Use embedded db
|
||||
USE_EMBEDDED_DB="True" \
|
||||
PGSERVER_DATA_DIR="/opt/khoj_db"
|
||||
# Install Python dependencies from pyproject.toml in editable mode
|
||||
RUN sed -i "s/dynamic = \\[\"version\"\\]/version = \"$VERSION\"/" pyproject.toml && \
|
||||
pip install --no-cache-dir ".[dev]"
|
||||
uv sync --all-extras && \
|
||||
# Save the lock file generated with correct Linux platform wheels
|
||||
cp uv.lock /opt/uv.lock.linux && \
|
||||
chown -R vscode:vscode /opt/venv
|
||||
|
||||
# --- Web App Dependencies ---
|
||||
# Copy web app manifest files
|
||||
COPY src/interface/web/package.json src/interface/web/yarn.lock /tmp/web/
|
||||
COPY src/interface/web/package.json src/interface/web/bun.lock /opt/khoj_web/
|
||||
|
||||
# Install web app dependencies
|
||||
# note: yarn will be available from the "features" in devcontainer.json
|
||||
RUN yarn install --cwd /tmp/web --cache-folder /opt/yarn-cache
|
||||
RUN cd /opt/khoj_web && bun install && chown -R vscode:vscode .
|
||||
|
||||
# The .venv and node_modules are now populated in the image.
|
||||
# The rest of the source code will be mounted by VS Code from your local checkout,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..", // Build context is the project root
|
||||
"args": {
|
||||
"PYTHON_VERSION": "3.10"
|
||||
"PYTHON_VERSION": "3.12"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [
|
||||
@@ -53,11 +53,6 @@
|
||||
"postCreateCommand": "scripts/dev_setup.sh --devcontainer",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "lts",
|
||||
"installYarnUsingApt": false,
|
||||
"nodeGypDependencies": true
|
||||
}
|
||||
},
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
|
||||
2
.github/workflows/build_khoj_el.yml
vendored
2
.github/workflows/build_khoj_el.yml
vendored
@@ -6,12 +6,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/interface/emacs/*.el
|
||||
- .github/workflows/build_khoj_el.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/interface/emacs/*.el
|
||||
- .github/workflows/build_khoj_el.yml
|
||||
|
||||
1
.github/workflows/desktop.yml
vendored
1
.github/workflows/desktop.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- "*"
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/interface/desktop/**
|
||||
- .github/workflows/desktop.yml
|
||||
|
||||
23
.github/workflows/dockerize.yml
vendored
23
.github/workflows/dockerize.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- "*"
|
||||
branches:
|
||||
- master
|
||||
- release/1.x
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- src/interface/web/**
|
||||
@@ -37,8 +38,8 @@ env:
|
||||
# Tag Image with tag name on release
|
||||
# else with user specified tag (default 'dev') if triggered via workflow
|
||||
# else with run_id if triggered via a pull request
|
||||
# else with 'pre' (if push to master)
|
||||
DOCKER_IMAGE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || github.event_name == 'workflow_dispatch' && github.event.inputs.tag || 'pre' }}
|
||||
# else with 'pre' (if push to master) or 'pre-1x' (if push to release/1.x)
|
||||
DOCKER_IMAGE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name == 'release/1.x' && 'pre-1x' || 'pre' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -153,18 +154,32 @@ jobs:
|
||||
- name: Create and Push Local Manifest
|
||||
if: github.event.inputs.khoj == 'true' || github.event_name == 'push'
|
||||
run: |
|
||||
# Only put "latest.*" tag on stable releases (i.e 1.x, 2.x+)
|
||||
if [[ "${{ env.DOCKER_IMAGE_TAG }}" =~ ^[1-9]\.[0-9]+\.[0-9]+$ ]]; then
|
||||
LATEST_TAG="latest"
|
||||
else
|
||||
LATEST_TAG="${{ env.DOCKER_IMAGE_TAG }}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools create \
|
||||
-t ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }} \
|
||||
-t ghcr.io/${{ github.repository }}:${{ github.ref_type == 'tag' && 'latest' || env.DOCKER_IMAGE_TAG }} \
|
||||
-t ghcr.io/${{ github.repository }}:${LATEST_TAG} \
|
||||
ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }}-amd64 \
|
||||
ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }}-arm64
|
||||
|
||||
- name: Create and Push Cloud Manifest
|
||||
if: github.event.inputs.khoj-cloud == 'true' || github.event_name == 'push'
|
||||
run: |
|
||||
# Only put "latest.*" tag on stable releases (i.e 1.x, 2.x+)
|
||||
if [[ "${{ env.DOCKER_IMAGE_TAG }}" =~ ^[1-9]\.[0-9]+\.[0-9]+$ ]]; then
|
||||
LATEST_TAG="latest"
|
||||
else
|
||||
LATEST_TAG="${{ env.DOCKER_IMAGE_TAG }}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools create \
|
||||
-t ghcr.io/${{ github.repository }}-cloud:${{ env.DOCKER_IMAGE_TAG }} \
|
||||
-t ghcr.io/${{ github.repository }}-cloud:${{ github.ref_type == 'tag' && 'latest' || env.DOCKER_IMAGE_TAG }} \
|
||||
-t ghcr.io/${{ github.repository }}-cloud:${LATEST_TAG} \
|
||||
ghcr.io/${{ github.repository }}-cloud:${{ env.DOCKER_IMAGE_TAG }}-amd64 \
|
||||
ghcr.io/${{ github.repository }}-cloud:${{ env.DOCKER_IMAGE_TAG }}-arm64
|
||||
|
||||
|
||||
4
.github/workflows/github_pages_deploy.yml
vendored
4
.github/workflows/github_pages_deploy.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
# 👇 Build steps
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: lts/*
|
||||
cache: yarn
|
||||
cache-dependency-path: documentation/yarn.lock
|
||||
- name: Install dependencies
|
||||
|
||||
22
.github/workflows/pre-commit.yml
vendored
22
.github/workflows/pre-commit.yml
vendored
@@ -2,6 +2,9 @@ name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- release/1.x
|
||||
paths:
|
||||
- src/**
|
||||
- tests/**
|
||||
@@ -12,6 +15,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release/1.x
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- tests/**
|
||||
@@ -31,18 +35,24 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python 3.11
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: ⏬️ Install Dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y libegl1
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
- name: ⬇️ Install Application
|
||||
run: pip install --no-cache-dir --upgrade .[dev]
|
||||
env:
|
||||
UV_INDEX: "https://download.pytorch.org/whl/cpu"
|
||||
UV_INDEX_STRATEGY: "unsafe-best-match"
|
||||
CUDA_VISIBLE_DEVICES: ""
|
||||
run: uv sync --all-extras
|
||||
|
||||
- name: 🌡️ Validate Application
|
||||
run: pre-commit run --hook-stage manual --all
|
||||
run: uv run pre-commit run --hook-stage manual --all
|
||||
|
||||
35
.github/workflows/pypi.yml
vendored
35
.github/workflows/pypi.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- "*"
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- src/interface/web/**
|
||||
@@ -14,6 +15,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- src/interface/web/**
|
||||
@@ -32,25 +34,28 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
python-version: '3.11.12'
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python 3.11
|
||||
run: uv python install 3.11.12
|
||||
|
||||
- name: ⬇️ Install Server
|
||||
run: python -m pip install --upgrade pip && pip install --upgrade .
|
||||
run: uv sync --all-extras
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: ⬇️ Install Web Client
|
||||
run: |
|
||||
yarn install
|
||||
yarn pypiciexport
|
||||
bun install
|
||||
bun pypiciexport
|
||||
working-directory: src/interface/web
|
||||
|
||||
- name: 📂 Copy Generated Files
|
||||
run: |
|
||||
mkdir -p src/khoj/interface/compiled
|
||||
cp -r /opt/hostedtoolcache/Python/3.11.12/x64/lib/python3.11/site-packages/khoj/interface/compiled/* src/khoj/interface/compiled/
|
||||
|
||||
- name: ⚙️ Build Python Package
|
||||
run: |
|
||||
# Setup Environment for Reproducible Builds
|
||||
@@ -59,13 +64,13 @@ jobs:
|
||||
rm -rf dist
|
||||
|
||||
# Build PyPI Package
|
||||
pipx run build
|
||||
uv build
|
||||
|
||||
- name: 🌡️ Validate Python Package
|
||||
run: |
|
||||
# Validate PyPi Package
|
||||
pipx run check-wheel-contents dist/*.whl --ignore W004
|
||||
pipx run twine check dist/*
|
||||
uv tool run check-wheel-contents dist/*.whl --ignore W002,W004
|
||||
uv tool run twine check dist/*
|
||||
|
||||
- name: ⏫ Upload Python Package Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -74,7 +79,7 @@ jobs:
|
||||
path: dist/khoj-*.whl
|
||||
|
||||
- name: 📦 Publish Python Package to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/master'
|
||||
if: startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/release/1.x'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.12
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
65
.github/workflows/run_evals.yml
vendored
65
.github/workflows/run_evals.yml
vendored
@@ -43,7 +43,7 @@ on:
|
||||
chat_model:
|
||||
description: 'Chat model to use'
|
||||
required: false
|
||||
default: 'gemini-2.0-flash'
|
||||
default: 'gemini-2.5-flash'
|
||||
type: string
|
||||
max_research_iterations:
|
||||
description: 'Maximum number of iterations in research mode'
|
||||
@@ -60,14 +60,6 @@ on:
|
||||
required: false
|
||||
default: 'https://api.openai.com/v1'
|
||||
type: string
|
||||
auto_read_webpage:
|
||||
description: 'Auto read webpage on online search'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- 'false'
|
||||
- 'true'
|
||||
randomize:
|
||||
description: 'Randomize the sample of questions'
|
||||
required: false
|
||||
@@ -76,6 +68,11 @@ on:
|
||||
options:
|
||||
- 'false'
|
||||
- 'true'
|
||||
dataset_seed:
|
||||
description: 'Seed to deterministically shuffle questions'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
eval:
|
||||
@@ -106,10 +103,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.10
|
||||
|
||||
- name: Get App Version
|
||||
id: hatch
|
||||
@@ -127,16 +127,18 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
# install dependencies
|
||||
sudo apt update && sudo apt install -y git python3-pip libegl1 sqlite3 libsqlite3-dev libsqlite3-0 ffmpeg libsm6 libxext6
|
||||
# upgrade pip
|
||||
python -m ensurepip --upgrade && python -m pip install --upgrade pip
|
||||
sudo apt update && sudo apt install -y git libegl1 sqlite3 libsqlite3-dev libsqlite3-0 ffmpeg libsm6 libxext6
|
||||
# install terrarium for code sandbox
|
||||
git clone https://github.com/khoj-ai/terrarium.git && cd terrarium && npm install --legacy-peer-deps && mkdir pyodide_cache
|
||||
|
||||
- name: ⬇️ Install Application
|
||||
env:
|
||||
UV_INDEX: "https://download.pytorch.org/whl/cpu"
|
||||
UV_INDEX_STRATEGY: "unsafe-best-match"
|
||||
CUDA_VISIBLE_DEVICES: ""
|
||||
run: |
|
||||
sed -i 's/dynamic = \["version"\]/version = "${{ steps.hatch.outputs.version }}"/' pyproject.toml
|
||||
pip install --upgrade .[dev]
|
||||
uv sync --all-extras
|
||||
|
||||
- name: 📝 Run Eval
|
||||
env:
|
||||
@@ -144,9 +146,10 @@ jobs:
|
||||
SAMPLE_SIZE: ${{ github.event_name == 'workflow_dispatch' && inputs.sample_size || 200 }}
|
||||
BATCH_SIZE: "20"
|
||||
RANDOMIZE: ${{ github.event_name == 'workflow_dispatch' && inputs.randomize || 'true' }}
|
||||
KHOJ_URL: "http://localhost:42110"
|
||||
DATASET_SEED: ${{ github.event_name == 'workflow_dispatch' && inputs.dataset_seed || github.run_id }}
|
||||
KHOJ_LLM_SEED: "42"
|
||||
KHOJ_DEFAULT_CHAT_MODEL: ${{ github.event_name == 'workflow_dispatch' && inputs.chat_model || 'gemini-2.0-flash' }}
|
||||
KHOJ_URL: "http://localhost:42110"
|
||||
KHOJ_DEFAULT_CHAT_MODEL: ${{ github.event_name == 'workflow_dispatch' && inputs.chat_model || 'gemini-2.5-flash' }}
|
||||
KHOJ_RESEARCH_ITERATIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.max_research_iterations || 10 }}
|
||||
KHOJ_AUTO_READ_WEBPAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.auto_read_webpage || 'false' }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
@@ -167,8 +170,13 @@ jobs:
|
||||
USE_EMBEDDED_DB: "true"
|
||||
KHOJ_TELEMETRY_DISABLE: "True" # To disable telemetry for tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Start Khoj server in background
|
||||
khoj --anonymous-mode --non-interactive &
|
||||
# Capture stdout/stderr to a log for debugging if startup fails
|
||||
uv run khoj --anonymous-mode --non-interactive -vv > khoj_server.log 2>&1 &
|
||||
KHOJ_PID=$!
|
||||
echo "Started Khoj (PID=$KHOJ_PID)"
|
||||
|
||||
# Start code sandbox
|
||||
npm install -g pm2
|
||||
@@ -177,17 +185,25 @@ jobs:
|
||||
# Wait for server to be ready
|
||||
timeout=120
|
||||
while ! curl -s http://localhost:42110/api/health > /dev/null; do
|
||||
if [ $timeout -le 0 ]; then
|
||||
echo "Timed out waiting for Khoj server"
|
||||
# If process died, surface logs and fail fast
|
||||
if ! kill -0 "$KHOJ_PID" 2>/dev/null; then
|
||||
echo "Khoj process exited before becoming healthy. Logs:" >&2
|
||||
sed -n '1,200p' khoj_server.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for Khoj server..."
|
||||
if [ $timeout -le 0 ]; then
|
||||
echo "Timed out waiting for Khoj server. Partial logs:" >&2
|
||||
sed -n '1,200p' khoj_server.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for Khoj server... ($timeout s left)"
|
||||
sleep 2
|
||||
timeout=$((timeout-2))
|
||||
done
|
||||
echo "Khoj server is healthy"
|
||||
|
||||
# Run evals
|
||||
python tests/evals/eval.py -d ${{ matrix.dataset }}
|
||||
uv run python tests/evals/eval.py -d ${{ matrix.dataset }}
|
||||
|
||||
- name: Upload Results
|
||||
if: always() # Upload results even if tests fail
|
||||
@@ -197,6 +213,7 @@ jobs:
|
||||
path: |
|
||||
*_evaluation_results_*.csv
|
||||
*_evaluation_summary_*.txt
|
||||
khoj_server.log
|
||||
|
||||
- name: Display Results
|
||||
if: always()
|
||||
@@ -205,7 +222,7 @@ jobs:
|
||||
echo "## Evaluation Summary of Khoj on ${{ matrix.dataset }} in ${{ matrix.khoj_mode }} mode" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**$(head -n 1 *_evaluation_summary_*.txt)**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Khoj Version: ${{ steps.hatch.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Chat Model: ${{ inputs.chat_model || 'gemini-2.0-flash' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Chat Model: ${{ inputs.chat_model || 'gemini-2.5-flash' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Code Sandbox: ${{ inputs.sandbox || 'terrarium' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
tail -n +2 *_evaluation_summary_*.txt >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
29
.github/workflows/test.yml
vendored
29
.github/workflows/test.yml
vendored
@@ -2,6 +2,9 @@ name: test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- release/1.x
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- tests/**
|
||||
@@ -13,6 +16,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release/1.x
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- tests/**
|
||||
@@ -50,18 +54,19 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python ${{ matrix.python_version }}
|
||||
run: uv python install ${{ matrix.python_version }}
|
||||
|
||||
- name: ⏬️ Install Dependencies
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
apt update && apt install -y git libegl1 sqlite3 libsqlite3-dev libsqlite3-0 ffmpeg libsm6 libxext6
|
||||
# required by llama-cpp-python prebuilt wheels
|
||||
apt install -y musl-dev && ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1
|
||||
|
||||
- name: ⬇️ Install Postgres
|
||||
env:
|
||||
@@ -69,17 +74,12 @@ jobs:
|
||||
run : |
|
||||
apt install -y postgresql postgresql-client && apt install -y postgresql-server-dev-16
|
||||
|
||||
- name: ⬇️ Install pip
|
||||
run: |
|
||||
apt install -y python3-pip
|
||||
python3 -m ensurepip --upgrade
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: ⬇️ Install Application
|
||||
env:
|
||||
PIP_EXTRA_INDEX_URL: "https://download.pytorch.org/whl/cpu https://abetlen.github.io/llama-cpp-python/whl/cpu"
|
||||
UV_INDEX: "https://download.pytorch.org/whl/cpu"
|
||||
UV_INDEX_STRATEGY: "unsafe-best-match"
|
||||
CUDA_VISIBLE_DEVICES: ""
|
||||
run: sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && pip install --break-system-packages --upgrade .[dev]
|
||||
run: sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && uv sync --all-extras
|
||||
|
||||
- name: 🧪 Test Application
|
||||
env:
|
||||
@@ -88,5 +88,6 @@ jobs:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
run: pytest
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
run: uv run pytest
|
||||
timeout-minutes: 10
|
||||
|
||||
7
.github/workflows/test_khoj_el.yml
vendored
7
.github/workflows/test_khoj_el.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/interface/emacs/*.el
|
||||
- src/interface/emacs/tests/*.el
|
||||
@@ -11,6 +12,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/1.x'
|
||||
paths:
|
||||
- src/interface/emacs/*.el
|
||||
- src/interface/emacs/tests/*.el
|
||||
@@ -23,10 +25,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
emacs_version:
|
||||
- 27.1
|
||||
- 27.2
|
||||
- 28.1
|
||||
- 28.2
|
||||
- 29.4
|
||||
- 30.2
|
||||
- snapshot
|
||||
steps:
|
||||
- uses: purcell/setup-emacs@master
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.3
|
||||
hooks:
|
||||
- id: black
|
||||
- id: ruff-check
|
||||
args: [ --fix ]
|
||||
files: \.py$
|
||||
- id: ruff-format
|
||||
files: \.py$
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
@@ -12,17 +16,10 @@ repos:
|
||||
# Exclude elisp files to not clear page breaks
|
||||
exclude: \.el$
|
||||
- id: check-json
|
||||
exclude: (devcontainer\.json|launch\.json)$
|
||||
exclude: (devcontainer\.json|launch\.json|settings\.json)$
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black", "--filter-files"]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -3,5 +3,9 @@
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.include": [
|
||||
"src/khoj/**/*"
|
||||
],
|
||||
"python.analysis.aiHoverSummaries": false,
|
||||
}
|
||||
|
||||
@@ -35,17 +35,17 @@ RUN sed -i "s/dynamic = \\[\"version\"\\]/version = \"$VERSION\"/" pyproject.tom
|
||||
pip install --no-cache-dir .
|
||||
|
||||
# Build Web App
|
||||
FROM node:23-alpine AS web-app
|
||||
FROM oven/bun:1-alpine AS web-app
|
||||
# Set build optimization env vars
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
WORKDIR /app/src/interface/web
|
||||
# Install dependencies first (cache layer)
|
||||
COPY src/interface/web/package.json src/interface/web/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY src/interface/web/package.json src/interface/web/bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
# Copy source and build
|
||||
COPY src/interface/web/. ./
|
||||
RUN yarn build
|
||||
RUN bun run build
|
||||
|
||||
# Merge the Server and Web App into a Single Image
|
||||
FROM base
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
***
|
||||
|
||||
### 🎁 New
|
||||
* Start any message with `/research` to try out the experimental research mode with Khoj.
|
||||
* Anyone can now [create custom agents](https://blog.khoj.dev/posts/create-agents-on-khoj/) with tunable personality, tools and knowledge bases.
|
||||
* Meet 🌶️ **[Pipali](https://pipali.ai)** - our [open-source](https://github.com/khoj-ai/pipali) AI coworker that runs on your computer.
|
||||
* [Read](https://blog.khoj.dev/posts/evaluate-khoj-quality/) about Khoj's excellent performance on modern retrieval and reasoning benchmarks.
|
||||
|
||||
***
|
||||
|
||||
@@ -72,39 +72,31 @@ RUN apt update \
|
||||
&& apt remove -y light-locker xfce4-screensaver xfce4-power-manager || true
|
||||
|
||||
# Create Computer User
|
||||
ENV USERNAME=operator
|
||||
ENV USERNAME=khoj
|
||||
ENV HOME=/home/$USERNAME
|
||||
RUN useradd -m -s /bin/bash -d $HOME -g $USERNAME $USERNAME && echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
RUN groupadd $USERNAME && \
|
||||
useradd -m -s /bin/bash -d $HOME -g $USERNAME $USERNAME && \
|
||||
echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
USER $USERNAME
|
||||
WORKDIR $HOME
|
||||
|
||||
# Setup Python
|
||||
RUN git clone https://github.com/pyenv/pyenv.git ~/.pyenv && \
|
||||
cd ~/.pyenv && src/configure && make -C src && cd .. && \
|
||||
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc && \
|
||||
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc && \
|
||||
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
|
||||
ENV PYENV_ROOT="$HOME/.pyenv"
|
||||
ENV PATH="$PYENV_ROOT/bin:$PATH"
|
||||
ENV PYENV_VERSION_MAJOR=3
|
||||
ENV PYENV_VERSION_MINOR=11
|
||||
ENV PYENV_VERSION_PATCH=6
|
||||
ENV PYENV_VERSION=$PYENV_VERSION_MAJOR.$PYENV_VERSION_MINOR.$PYENV_VERSION_PATCH
|
||||
RUN eval "$(pyenv init -)" && \
|
||||
pyenv install $PYENV_VERSION && \
|
||||
pyenv global $PYENV_VERSION && \
|
||||
pyenv rehash
|
||||
ENV PATH="$HOME/.pyenv/shims:$HOME/.pyenv/bin:$PATH"
|
||||
# Install Python using uv and create a virtual environment
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
ENV PYTHON_VERSION=3.11.6
|
||||
RUN uv python pin $PYTHON_VERSION
|
||||
RUN uv venv $HOME/.venv --python $PYTHON_VERSION --seed
|
||||
RUN echo 'export PATH="$HOME/.venv/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
ENV PATH="$HOME/.venv/bin:$PATH"
|
||||
|
||||
# Install Python Packages
|
||||
RUN python3 -m pip install --no-cache-dir \
|
||||
RUN uv pip install --no-cache-dir \
|
||||
pyautogui \
|
||||
Pillow \
|
||||
pyperclip \
|
||||
pygetwindow
|
||||
|
||||
# Setup VNC
|
||||
RUN x11vnc -storepasswd secret /home/operator/.vncpass
|
||||
RUN x11vnc -storepasswd secret /home/khoj/.vncpass
|
||||
|
||||
ARG WIDTH=1024
|
||||
ARG HEIGHT=768
|
||||
@@ -115,13 +107,22 @@ ENV DISPLAY_NUM=$DISPLAY_NUM
|
||||
ENV DISPLAY=":$DISPLAY_NUM"
|
||||
|
||||
# Expose VNC on port 5900
|
||||
# run Xvfb, x11vnc, Xfce (no login manager)
|
||||
EXPOSE 5900
|
||||
CMD ["/bin/sh", "-c", " export XDG_RUNTIME_DIR=/run/user/$(id -u); \
|
||||
mkdir -p $XDG_RUNTIME_DIR && chown $USERNAME:$USERNAME $XDG_RUNTIME_DIR && chmod 0700 $XDG_RUNTIME_DIR; \
|
||||
|
||||
# Start Virtual Display (Xvfb), Desktop Manager (XFCE) and Remote Viewer (X11 VNC)
|
||||
CMD ["/bin/sh", "-c", " \
|
||||
# Create and permission XDG_RUNTIME_DIR with sudo \n\
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u); \
|
||||
sudo mkdir -p $XDG_RUNTIME_DIR && \
|
||||
sudo chown $(id -u):$(id -g) $XDG_RUNTIME_DIR && \
|
||||
sudo chmod 0700 $XDG_RUNTIME_DIR; \
|
||||
\
|
||||
# Start Virtual Display \n\
|
||||
Xvfb $DISPLAY -screen 0 ${WIDTH}x${HEIGHT}x24 -dpi 96 -auth /home/$USERNAME/.Xauthority >/dev/null 2>&1 & \
|
||||
sleep 1; \
|
||||
xauth add $DISPLAY . $(mcookie); \
|
||||
\
|
||||
# Start VNC Server \n\
|
||||
x11vnc -display $DISPLAY -forever -rfbauth /home/$USERNAME/.vncpass -listen 0.0.0.0 -rfbport 5900 >/dev/null 2>&1 & \
|
||||
eval $(dbus-launch --sh-syntax) && \
|
||||
startxfce4 & \
|
||||
|
||||
@@ -95,14 +95,14 @@ services:
|
||||
# Uncomment appropriate lines below to enable web results with Khoj
|
||||
# Ensure you set your provider specific API keys.
|
||||
# ---
|
||||
# Free, Slower API. Does both web search and webpage read. Get API key from https://jina.ai/
|
||||
# - JINA_API_KEY=your_jina_api_key
|
||||
# Paid, Fast API. Only does web search. Get API key from https://serper.dev/
|
||||
# - SERPER_DEV_API_KEY=your_serper_dev_api_key
|
||||
# Paid, Fast, Open API. Only does webpage read. Get API key from https://firecrawl.dev/
|
||||
# - FIRECRAWL_API_KEY=your_firecrawl_api_key
|
||||
# Paid, Fast, Higher Read Success API. Only does webpage read. Get API key from https://olostep.com/
|
||||
# Paid, Higher Read Success API. Only does webpage read. Get API key from https://olostep.com/
|
||||
# - OLOSTEP_API_KEY=your_olostep_api_key
|
||||
# Paid, Open API. Does both web search and webpage read. Get API key from https://firecrawl.dev/
|
||||
# - FIRECRAWL_API_KEY=your_firecrawl_api_key
|
||||
# Paid, Fast API. Does both web search and webpage read. Get API key from https://exa.ai/
|
||||
# - EXA_API_KEY=your_exa_api_key
|
||||
#
|
||||
# Uncomment the necessary lines below to make your instance publicly accessible.
|
||||
# Proceed with caution, especially if you are using anonymous mode.
|
||||
|
||||
@@ -20,7 +20,7 @@ Add all the agents you want to use for your different use-cases like Writer, Res
|
||||
### Chat Model Options
|
||||
Add all the chat models you want to try, use and switch between for your different use-cases. For each chat model you add:
|
||||
- `Chat model`: The name of an [OpenAI](https://platform.openai.com/docs/models), [Anthropic](https://docs.anthropic.com/en/docs/about-claude/models#model-names), [Gemini](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) or [Offline](https://huggingface.co/models?pipeline_tag=text-generation&library=gguf) chat model.
|
||||
- `Model type`: The chat model provider like `OpenAI`, `Offline`.
|
||||
- `Model type`: The chat model provider like `OpenAI`, `Google`.
|
||||
- `Vision enabled`: Set to `true` if your model supports vision. This is currently only supported for vision capable OpenAI models like `gpt-4o`
|
||||
- `Max prompt size`, `Subscribed max prompt size`: These are optional fields. They are used to truncate the context to the maximum context size that can be passed to the model. This can help with accuracy and cost-saving.<br />
|
||||
- `Tokenizer`: This is an optional field. It is used to accurately count tokens and truncate context passed to the chat model to stay within the models max prompt size.
|
||||
@@ -52,8 +52,8 @@ Search models are used to generate vector embeddings of your documents for natur
|
||||
<img src="/img/example_search_model_admin_settings.png" alt="Example Search Model Settings" style={{width: 500}} />
|
||||
|
||||
### Text to Image Model Options
|
||||
Add text to image generation models with these settings. Khoj currently supports text to image models available via OpenAI, Stability or Replicate API
|
||||
- `api-key`: Set to your OpenAI, Stability or Replicate API key
|
||||
Add text to image generation models with these settings. Khoj currently supports text to image models available via OpenAI, Google or Replicate API
|
||||
- `api-key`: Set to your OpenAI, Google AI or Replicate API key
|
||||
- `model`: Set the model name available over the selected model provider
|
||||
- `model-type`: Set to the appropriate model provider
|
||||
- `openai-config`: For image generation models available via OpenAI (compatible) API you can set the appropriate OpenAI Processor Conversation Settings instead of specifying the `api-key` field above
|
||||
|
||||
@@ -24,7 +24,7 @@ It's still possible to use the magic links feature without Resend, but you'll ne
|
||||
|
||||
## Manually sending magic links
|
||||
|
||||
1. The user will have to enter their email address in the login page at http://localhost:42110/login.
|
||||
1. The user will have to enter their email address in the login popup shown at http://localhost:42110/?v=app.
|
||||
|
||||
They'll click `Get Login Link`. Without the Resend API key, this will just create an unverified account for them in the backend
|
||||
<img src="/img/magic_link.png" alt="Magic link login form" width="400"/>
|
||||
|
||||
@@ -30,7 +30,7 @@ git clone https://github.com/khoj-ai/khoj && cd khoj
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
|
||||
# For MacOS or zsh users run this
|
||||
pip install -e '.[dev]'
|
||||
uv sync --all-extras
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="windows" label="Windows">
|
||||
@@ -42,7 +42,7 @@ git clone https://github.com/khoj-ai/khoj && cd khoj
|
||||
python3 -m venv .venv && .venv\Scripts\activate
|
||||
|
||||
# Install Khoj for Development
|
||||
pip install -e '.[dev]'
|
||||
uv sync --all-extras
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="linux" label="Linux">
|
||||
@@ -54,7 +54,7 @@ git clone https://github.com/khoj-ai/khoj && cd khoj
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
|
||||
# Install Khoj for Development
|
||||
pip install -e '.[dev]'
|
||||
uv sync --all-extras
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -106,13 +106,13 @@ sudo -u postgres createdb khoj --password
|
||||
|
||||
```shell
|
||||
cd src/interface/web/
|
||||
yarn install
|
||||
yarn export
|
||||
bun install
|
||||
bun export
|
||||
```
|
||||
|
||||
You can optionally use `yarn dev` to start a development server for the front-end which will be available at http://localhost:3000. This is especially useful if you're making changes to the front-end code, but not necessary for running Khoj. Note that streaming does not work on the dev server due to how it is handled with SSR in Next.js.
|
||||
You can optionally use `bun dev` to start a development server for the front-end which will be available at http://localhost:3000. This is especially useful if you're making changes to the front-end code, but not necessary for running Khoj. Note that streaming does not work on the dev server due to how it is handled with SSR in Next.js.
|
||||
|
||||
Always run `yarn export` to test your front-end changes on http://localhost:42110 before creating a PR.
|
||||
Always run `bun export` to test your front-end changes on http://localhost:42110 before creating a PR.
|
||||
|
||||
#### 4. Run
|
||||
1. Start Khoj
|
||||
@@ -129,7 +129,7 @@ Always run `yarn export` to test your front-end changes on http://localhost:4211
|
||||
- Try reactivating the virtual environment and rerunning the `khoj` command.
|
||||
- If it still doesn't work repeat the installation process.
|
||||
2. Python Package Missing
|
||||
- Use `pip install xxx` and try running the `khoj` command.
|
||||
- Use `uv add xxx` and try running the `khoj` command.
|
||||
3. Command `createdb` Not Recognized
|
||||
- make sure path to postgres binaries is included in environment variables. It usually looks something like
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ Khoj supports a variety of features, including search and chat with a wide range
|
||||
- **Works online or offline**: Chat using online or offline AI chat models
|
||||
|
||||
#### General
|
||||
- **Cloud or Self-Host**: Use [cloud](https://app.khoj.dev/login) to use Khoj anytime from anywhere or [self-host](/get-started/setup) for privacy
|
||||
- **Cloud or Self-Host**: Use [cloud](https://app.khoj.dev) to use Khoj anytime from anywhere or [self-host](/get-started/setup) for privacy
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Pluggable**: Modular architecture makes it easy to plug in new data sources, frontends and ML models
|
||||
- **Multiple Sources**: Index your Org-mode, Markdown, PDF, plaintext files, Github repos and Notion pages
|
||||
|
||||
@@ -19,13 +19,14 @@ Try it out yourself! https://app.khoj.dev
|
||||
Online search can work even with self-hosting! You have a few options:
|
||||
|
||||
- If you're using Docker, online search should work out of the box with [searxng](https://github.com/searxng/searxng) using our standard `docker-compose.yml`.
|
||||
- For a non-local, free solution, you can use [JinaAI's reader API](https://jina.ai/reader/) to search online and read webpages. You can get a free API key via https://jina.ai/reader. Set the `JINA_API_KEY` environment variable to your Jina AI reader API key to enable online search.
|
||||
- To get production-grade, fast online search, set the `SERPER_DEV_API_KEY` environment variable to your [Serper.dev](https://serper.dev/) API key. These search results include additional context like answer box, knowledge graph etc.
|
||||
- To use open, self-hostable search provider, set the `FIRECRAWL_API_KEY` environment variable to your [Firecrawl](https://firecrawl.dev) API key. These search results do not scrape social media results.
|
||||
- To use Exa search provider, set the `EXA_API_KEY` environment variable to your [Exa](https://exa.ai) API key.
|
||||
|
||||
### Webpage Reading
|
||||
|
||||
Out of the box, you **don't have to do anything to enable webpage reading**. Khoj will automatically read webpages by using the `requests` library. To get more distributed and scalable webpage reading, you can use the following options:
|
||||
Out of the box, you **don't have to do anything to enable webpage reading**. Khoj will automatically read webpages by using the `requests` library. To get faster, more readable webpages for Khoj, you can use the following options:
|
||||
|
||||
- If you're using Jina AI's reader API for search, it should work automatically for webpage reading as well.
|
||||
- For scalable webpage scraping, you can use [Firecrawl](https://www.firecrawl.dev/). Create a new [Webscraper](http://localhost:42110/server/admin/database/webscraper/add/). Set your Firecrawl API key to the Api Key field, and set the type to Firecrawl.
|
||||
- For advanced webpage reading, you can use [Olostep](https://www.olostep.com/). This has a higher success rate at reading webpages than the default webpage readers. Create a new [Webscraper](http://localhost:42110/server/admin/database/webscraper/add/). Set your Olostep API key to the Api Key field, and set the type to Olostep.
|
||||
- For open, self-hostable webpage reader, you can use [Firecrawl](https://www.firecrawl.dev/). Create a new [Webscraper](http://localhost:42110/server/admin/database/webscraper/add/). Set your Firecrawl API key to the Api Key field, and set the type to Firecrawl.
|
||||
- For advanced webpage reading, you can use [Olostep](https://www.olostep.com/). This can read a wider variety of webpages. Create a new [Webscraper](http://localhost:42110/server/admin/database/webscraper/add/). Set your Olostep API key to the Api Key field, and set the type to Olostep.
|
||||
- For fast webpage reading, you can use [Exa](https://exa.ai). Create a new [Webscraper](http://localhost:42110/server/admin/database/webscraper/add/). Set your Exa API key to the Api Key field, and set the type to Exa.
|
||||
|
||||
@@ -28,7 +28,7 @@ Welcome to the Khoj Docs! This is the best place to get setup and explore Khoj's
|
||||
- Quickly [find](/features/search) relevant notes and documents using natural language
|
||||
- It understands pdf, plaintext, markdown, org-mode files, and [notion pages](/data-sources/notion_integration).
|
||||
- Access it from your [Emacs](/clients/emacs), [Obsidian](/clients/obsidian), the [Khoj desktop app](/clients/desktop), or [any web browser](/clients/web)
|
||||
- Use our [cloud](https://app.khoj.dev/login) instance to access your Khoj anytime from anywhere, [self-host](/get-started/setup) on consumer hardware for privacy
|
||||
- Use our [cloud](https://app.khoj.dev) instance to access your Khoj anytime from anywhere, [self-host](/get-started/setup) on consumer hardware for privacy
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Here's what to consider if you're using Khoj, whether self-hosted or on our clou
|
||||
- If you're self-hosting, you can opt out of telemetry by following [these instructions](/miscellaneous/telemetry).
|
||||
|
||||
|
||||
Self-hosting isn't for everyone, so we've still taken steps to make Khoj privacy-friendly, even if you choose to use our [cloud offering](https://app.khoj.dev/login). Here's what to consider when using Khoj Cloud:
|
||||
Self-hosting isn't for everyone, so we've still taken steps to make Khoj privacy-friendly, even if you choose to use our [cloud offering](https://app.khoj.dev). Here's what to consider when using Khoj Cloud:
|
||||
1. Your embeddings are generated by an open source model within our own dedicated endpoint [hosted on AWS with Huggingface](https://huggingface.co/inference-endpoints/dedicated). There's zero persistent memory to the Huggingface Inference endpoints (it's stateless).
|
||||
1. Your embeddings and the associated raw text are stored in a secure Postgres DB in our private AWS cloud. Your data is sharded on a unique user ID. We store the raw text in your files to improve file syncing and provide context when you chat with Khoj.
|
||||
1. When you use the single-sign-on option with Google, we only receive your name, a link to your profile photo, and your email address.
|
||||
|
||||
@@ -18,10 +18,6 @@ import TabItem from '@theme/TabItem';
|
||||
These are the general setup instructions for self-hosted Khoj.
|
||||
You can install the Khoj server using either [Docker](?server=docker) or [Pip](?server=pip).
|
||||
|
||||
:::info[Offline Model + GPU]
|
||||
To use the offline chat model with your GPU, we recommend using the Docker setup with Ollama . You can also use the local Khoj setup via the Python package directly.
|
||||
:::
|
||||
|
||||
:::info[First Run]
|
||||
Restart your Khoj server after the first run to ensure all settings are applied correctly.
|
||||
:::
|
||||
@@ -225,10 +221,6 @@ To start Khoj automatically in the background use [Task scheduler](https://www.w
|
||||
You can now open the web app at http://localhost:42110 and start interacting!<br />
|
||||
Nothing else is necessary, but you can customize your setup further by following the steps below.
|
||||
|
||||
:::info[First Message to Offline Chat Model]
|
||||
The offline chat model gets downloaded when you first send a message to it. The download can take a few minutes! Subsequent messages should be faster.
|
||||
:::
|
||||
|
||||
### Add Chat Models
|
||||
<h4>Login to the Khoj Admin Panel</h4>
|
||||
Go to http://localhost:42110/server/admin and login with the admin credentials you setup during installation.
|
||||
@@ -287,7 +279,7 @@ Using Ollama? See the [Ollama Integration](/advanced/ollama) section for more cu
|
||||
- Add your [Gemini API key](https://aistudio.google.com/app/apikey)
|
||||
- Give the configuration a friendly name like `Gemini`. Do not configure the API base url.
|
||||
2. Create a new [chat model](http://localhost:42110/server/admin/database/chatmodel/add)
|
||||
- Set the `chat-model` field to a [Google Gemini chat model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models). Example: `gemini-2.0-flash`.
|
||||
- Set the `chat-model` field to a [Google Gemini chat model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models). Example: `gemini-2.5-flash`.
|
||||
- Set the `model-type` field to `Google`.
|
||||
- Set the `ai model api` field to the Gemini AI Model API you created in step 1.
|
||||
|
||||
@@ -301,13 +293,14 @@ Offline chat stays completely private and can work without internet using any op
|
||||
- A Nvidia, AMD GPU or a Mac M1+ machine would significantly speed up chat responses
|
||||
:::
|
||||
|
||||
1. Get the name of your preferred chat model from [HuggingFace](https://huggingface.co/models?pipeline_tag=text-generation&library=gguf). *Most GGUF format chat models are supported*.
|
||||
2. Open the [create chat model page](http://localhost:42110/server/admin/database/chatmodel/add/) on the admin panel
|
||||
3. Set the `chat-model` field to the name of your preferred chat model
|
||||
- Make sure the `model-type` is set to `Offline`
|
||||
4. Set the newly added chat model as your preferred model in your [User chat settings](http://localhost:42110/settings) and [Server chat settings](http://localhost:42110/server/admin/database/serverchatsettings/).
|
||||
5. Restart the Khoj server and [start chatting](http://localhost:42110) with your new offline model!
|
||||
</TabItem>
|
||||
1. Install any Openai API compatible local ai model server like [llama-cpp-server](https://github.com/ggml-org/llama.cpp/tree/master/tools/server), Ollama, vLLM etc.
|
||||
2. Add an [ai model api](http://localhost:42110/server/admin/database/aimodelapi/add/) on the admin panel
|
||||
- Set the `api url` field to the url of your local ai model provider like `http://localhost:11434/v1/` for Ollama
|
||||
3. Restart the Khoj server to load models available on your local ai model provider
|
||||
- If that doesn't work, you'll need to manually add available [chat model](http://localhost:42110/server/admin/database/chatmodel/add) in the admin panel.
|
||||
4. Set the newly added chat model as your preferred model in your [User chat settings](http://localhost:42110/settings)
|
||||
5. [Start chatting](http://localhost:42110) with your local AI!
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::tip[Multiple Chat Models]
|
||||
|
||||
@@ -27,7 +27,11 @@ const config = {
|
||||
projectName: 'khoj', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
markdown: {
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
@@ -99,7 +103,7 @@ const config = {
|
||||
'aria-label': 'GitHub repository',
|
||||
},
|
||||
{
|
||||
href: 'https://app.khoj.dev/login',
|
||||
href: 'https://app.khoj.dev',
|
||||
position: 'right',
|
||||
className: 'header-cloud-link',
|
||||
title: 'Khoj Cloud',
|
||||
@@ -187,14 +191,14 @@ const config = {
|
||||
},
|
||||
{
|
||||
label: 'Khoj Cloud',
|
||||
href: 'https://app.khoj.dev/login',
|
||||
href: 'https://app.khoj.dev',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/khoj-ai/khoj',
|
||||
},
|
||||
{
|
||||
label: 'Website',
|
||||
label: 'Khoj Inc.',
|
||||
href: 'https://khoj.dev',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,18 +14,19 @@
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.2.1",
|
||||
"@docusaurus/plugin-sitemap": "^3.2.1",
|
||||
"@docusaurus/preset-classic": "^3.7.0",
|
||||
"@docusaurus/core": "^3.9.2",
|
||||
"@docusaurus/plugin-sitemap": "^3.9.2",
|
||||
"@docusaurus/preset-classic": "^3.9.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"webpack-dev-server": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.2.1",
|
||||
"@docusaurus/types": "^3.2.1"
|
||||
"@docusaurus/module-type-aliases": "^3.9.2",
|
||||
"@docusaurus/types": "^3.9.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -43,6 +44,8 @@
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"webpack-dev-server": "^5.2.1"
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"picomatch": ">=2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "1.42.8",
|
||||
"version": "2.0.0-beta.28",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Khoj Inc.",
|
||||
|
||||
@@ -34,17 +34,17 @@ RUN sed -i "s/dynamic = \\[\"version\"\\]/version = \"$VERSION\"/" pyproject.tom
|
||||
pip install --no-cache-dir -e .[prod]
|
||||
|
||||
# Build Web App
|
||||
FROM node:20-alpine AS web-app
|
||||
FROM oven/bun:1-alpine AS web-app
|
||||
# Set build optimization env vars
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
WORKDIR /app/src/interface/web
|
||||
# Install dependencies first (cache layer)
|
||||
COPY src/interface/web/package.json src/interface/web/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY src/interface/web/package.json src/interface/web/bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
# Copy source and build
|
||||
COPY src/interface/web/. ./
|
||||
RUN yarn build
|
||||
RUN bun run build
|
||||
|
||||
# Merge the Server and Web App into a Single Image
|
||||
FROM base
|
||||
|
||||
@@ -40,43 +40,43 @@ dependencies = [
|
||||
"dateparser >= 1.1.1",
|
||||
"defusedxml == 0.7.1",
|
||||
"fastapi >= 0.110.0",
|
||||
"python-multipart >= 0.0.7",
|
||||
"python-multipart >= 0.0.22",
|
||||
"jinja2 == 3.1.6",
|
||||
"openai >= 1.86.0",
|
||||
"openai >= 2.0.0, < 3.0.0",
|
||||
"tiktoken >= 0.3.2",
|
||||
"tenacity >= 9.0.0",
|
||||
"magika ~= 0.5.1",
|
||||
"pillow ~= 10.0.0",
|
||||
"pillow >= 12.1.1",
|
||||
"pydantic[email] >= 2.0.0",
|
||||
"pyyaml ~= 6.0",
|
||||
"rich >= 13.3.1",
|
||||
"click < 8.2.2",
|
||||
"schedule == 1.1.0",
|
||||
"sentence-transformers == 3.4.1",
|
||||
"einops == 0.8.0",
|
||||
"transformers >= 4.51.0",
|
||||
"transformers >= 4.53.0",
|
||||
"torch == 2.6.0",
|
||||
"uvicorn == 0.30.6",
|
||||
"aiohttp ~= 3.9.0",
|
||||
"langchain-text-splitters == 0.3.1",
|
||||
"langchain-community == 0.3.3",
|
||||
"requests >= 2.26.0",
|
||||
"uvicorn >= 0.31.1",
|
||||
"aiohttp ~= 3.13.0",
|
||||
"langchain-text-splitters == 0.3.11",
|
||||
"langchain-community == 0.3.31",
|
||||
"requests >= 2.33.0",
|
||||
"anyio ~= 4.8.0",
|
||||
"pymupdf == 1.24.11",
|
||||
"django == 5.1.10",
|
||||
"django == 5.1.15",
|
||||
"django-unfold == 0.42.0",
|
||||
"authlib == 1.2.1",
|
||||
"llama-cpp-python == 0.2.88",
|
||||
"authlib == 1.6.9",
|
||||
"itsdangerous == 2.1.2",
|
||||
"httpx == 0.28.1",
|
||||
"pgvector == 0.2.4",
|
||||
"psycopg2-binary == 2.9.9",
|
||||
"lxml == 4.9.3",
|
||||
"tzdata == 2023.3",
|
||||
"rapidocr-onnxruntime == 1.3.24",
|
||||
"rapidocr-onnxruntime == 1.4.4",
|
||||
"openai-whisper >= 20231117",
|
||||
"django-phonenumber-field == 7.3.0",
|
||||
"phonenumbers == 8.13.27",
|
||||
"markdownify ~= 0.11.6",
|
||||
"markdownify ~= 0.14.1",
|
||||
"markdown-it-py ~= 3.0.0",
|
||||
"websockets == 13.0",
|
||||
"psutil >= 5.8.0",
|
||||
@@ -85,14 +85,16 @@ dependencies = [
|
||||
"pytz ~= 2024.1",
|
||||
"cron-descriptor == 1.4.3",
|
||||
"django_apscheduler == 0.7.0",
|
||||
"anthropic == 0.52.0",
|
||||
"anthropic == 0.75.0",
|
||||
"docx2txt == 0.8",
|
||||
"google-genai == 1.11.0",
|
||||
"google-genai == 1.52.0",
|
||||
"google-auth ~= 2.23.3",
|
||||
"pyjson5 == 1.6.7",
|
||||
"resend == 1.0.1",
|
||||
"resend == 1.2.0",
|
||||
"email-validator == 2.2.0",
|
||||
"e2b-code-interpreter ~= 1.0.0",
|
||||
"mcp >= 1.23.0",
|
||||
"pyasn1>=0.6.3",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
@@ -123,7 +125,7 @@ dev = [
|
||||
"freezegun >= 1.2.0",
|
||||
"factory-boy >= 3.2.1",
|
||||
"mypy >= 1.0.1",
|
||||
"black >= 23.1.0",
|
||||
"ruff >= 0.12.0",
|
||||
"pre-commit >= 3.0.4",
|
||||
"gitpython ~= 3.1.43",
|
||||
"datasets",
|
||||
@@ -150,11 +152,34 @@ non_interactive = true
|
||||
show_error_codes = true
|
||||
warn_unused_ignores = false
|
||||
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"] # Enable error, warning, and import checks
|
||||
ignore = [
|
||||
"E501", # Ignore line length
|
||||
"F405", # Ignore name not defined (e.g., from imports)
|
||||
"E402", # Ignore module level import not at top of file
|
||||
]
|
||||
unfixable = ["F841"] # Don't auto-remove unused variables
|
||||
exclude = [ "tests/*.py" ]
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"src/khoj/main.py" = [
|
||||
"I001", # Ignore Import order
|
||||
"I002", # Ignore Import not at top of file
|
||||
"E402", # Ignore module level import not at top of file
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["khoj"]
|
||||
|
||||
[tool.uv]
|
||||
build-constraint-dependencies = ["setuptools>=61.2,<82"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--strict-markers"
|
||||
|
||||
@@ -2,9 +2,80 @@
|
||||
|
||||
project_root=$PWD
|
||||
|
||||
while getopts 'nc:t:' opt;
|
||||
while getopts 'nc:t:p:' opt;
|
||||
do
|
||||
case "${opt}" in
|
||||
p)
|
||||
# Create pre-release SemVer version. Options: alpha, beta, rc
|
||||
prerelease_type=$OPTARG
|
||||
|
||||
# Get the current version from web package.json to determine base version
|
||||
cd $project_root/src/interface/web
|
||||
current_base_version=$(grep '"version":' package.json | awk -F '"' '{print $4}')
|
||||
|
||||
# Extract base version (remove any existing pre-release suffix)
|
||||
base_version=$(echo $current_base_version | sed 's/-.*$//')
|
||||
|
||||
# If current version is already 2.x.x, increment the pre-release number
|
||||
if [[ $current_base_version == *"-$prerelease_type"* ]]; then
|
||||
# Extract current pre-release number and increment
|
||||
current_num=$(echo $current_base_version | sed "s/.*-$prerelease_type\.//" | sed 's/[^0-9]*$//')
|
||||
next_num=$((current_num + 1))
|
||||
current_version="$base_version-$prerelease_type.$next_num"
|
||||
else
|
||||
# If base version is 1.x.x, bump to 2.0.0-prerelease.1
|
||||
if [[ $base_version == 1.* ]]; then
|
||||
current_version="2.0.0-$prerelease_type.1"
|
||||
else
|
||||
# Otherwise add pre-release to current base version
|
||||
current_version="$base_version-$prerelease_type.1"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Bump Web app to pre-release version
|
||||
cd $project_root/src/interface/web
|
||||
yarn version --new-version $current_version --no-git-tag-version
|
||||
|
||||
# Bump Desktop app to pre-release version
|
||||
cd $project_root/src/interface/desktop
|
||||
yarn version --new-version $current_version --no-git-tag-version
|
||||
|
||||
# Bump Obsidian plugin to pre-release version
|
||||
cd $project_root/src/interface/obsidian
|
||||
yarn build # verify build before bumping version
|
||||
yarn version --new-version $current_version --no-git-tag-version
|
||||
# append current version, min Obsidian app version from manifest to versions json
|
||||
cp $project_root/versions.json .
|
||||
yarn run version # run Obsidian version script
|
||||
|
||||
# Bump Emacs package to pre-release version
|
||||
cd ../emacs
|
||||
sed -E -i.bak "s/^;; Version: (.*)/;; Version: $current_version/" khoj.el
|
||||
git add khoj.el
|
||||
rm *.bak
|
||||
|
||||
# Copy current obsidian versioned files to project root
|
||||
cd $project_root
|
||||
cp src/interface/obsidian/versions.json .
|
||||
cp src/interface/obsidian/manifest.json .
|
||||
|
||||
# Run pre-commit validation to fix jsons
|
||||
pre-commit run --hook-stage manual --all
|
||||
|
||||
# Commit changes and tag commit for pre-release
|
||||
git add \
|
||||
$project_root/src/interface/web/package.json \
|
||||
$project_root/src/interface/desktop/package.json \
|
||||
$project_root/src/interface/obsidian/package.json \
|
||||
$project_root/src/interface/obsidian/yarn.lock \
|
||||
$project_root/src/interface/obsidian/manifest.json \
|
||||
$project_root/src/interface/obsidian/versions.json \
|
||||
$project_root/src/interface/emacs/khoj.el \
|
||||
$project_root/manifest.json \
|
||||
$project_root/versions.json
|
||||
git commit -m "Release Khoj version $current_version"
|
||||
git tag $current_version
|
||||
;;
|
||||
t)
|
||||
# Get version type to bump. Options: major, minor, patch
|
||||
version_type=$OPTARG
|
||||
@@ -54,7 +125,7 @@ do
|
||||
$project_root/manifest.json \
|
||||
$project_root/versions.json
|
||||
git commit -m "Release Khoj version $current_version"
|
||||
git tag $current_version master
|
||||
git tag $current_version
|
||||
;;
|
||||
c)
|
||||
# Get current project version
|
||||
@@ -101,7 +172,7 @@ do
|
||||
$project_root/manifest.json \
|
||||
$project_root/versions.json
|
||||
git commit -m "Release Khoj version $current_version"
|
||||
git tag $current_version master
|
||||
git tag $current_version
|
||||
;;
|
||||
n)
|
||||
# Induce hatch to compute next version number
|
||||
@@ -144,7 +215,11 @@ do
|
||||
git commit -m "Bump Khoj to pre-release version $next_version"
|
||||
;;
|
||||
?)
|
||||
echo -e "Invalid command option.\nUsage: $(basename $0) [-t] [-c] [-n]"
|
||||
echo -e "Invalid command option.\nUsage: $(basename $0) [-t type] [-c version] [-p prerelease] [-n]"
|
||||
echo -e " -t: Bump version by type (major, minor, patch)"
|
||||
echo -e " -c: Set specific version"
|
||||
echo -e " -p: Create pre-release version (alpha, beta, rc)"
|
||||
echo -e " -n: Compute and set next version using hatch"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -7,11 +7,11 @@ INSTALL_FULL=false
|
||||
DEVCONTAINER=false
|
||||
for arg in "$@"
|
||||
do
|
||||
if [ "$arg" == "--full" ]
|
||||
if [ "$arg" = "--full" ]
|
||||
then
|
||||
INSTALL_FULL=true
|
||||
fi
|
||||
if [ "$arg" == "--devcontainer" ]
|
||||
if [ "$arg" = "--devcontainer" ]
|
||||
then
|
||||
DEVCONTAINER=true
|
||||
fi
|
||||
@@ -24,26 +24,40 @@ if [ "$DEVCONTAINER" = true ]; then
|
||||
# Use devcontainer launch.json
|
||||
mkdir -p .vscode && cp .devcontainer/launch.json .vscode/launch.json
|
||||
|
||||
# Activate the pre-installed venv (no need to create new one)
|
||||
echo "Using Python environment at /opt/venv"
|
||||
# PATH should already include /opt/venv/bin from Dockerfile
|
||||
|
||||
# Install khoj in editable mode (dependencies already installed)
|
||||
python3 -m pip install -e '.[dev]'
|
||||
# Install Server App using pre-installed dependencies
|
||||
echo "Setup Server App with UV. Use pre-installed dependencies in $UV_PROJECT_ENVIRONMENT."
|
||||
sed -i "s/dynamic = \\[\"version\"\\]/version = \"$VERSION\"/" pyproject.toml
|
||||
cp /opt/uv.lock.linux uv.lock
|
||||
uv sync --all-extras
|
||||
|
||||
# Install Web App using cached dependencies
|
||||
echo "Installing Web App using cached dependencies..."
|
||||
echo "Setup Web App with Bun. Use pre-installed dependencies in /opt/khoj_web."
|
||||
cd "$PROJECT_ROOT/src/interface/web"
|
||||
yarn install --cache-folder /opt/yarn-cache && yarn export
|
||||
ln -sf /opt/khoj_web/node_modules node_modules
|
||||
bun install && bun run ciexport
|
||||
else
|
||||
# Standard setup
|
||||
echo "Installing Server App..."
|
||||
cd "$PROJECT_ROOT"
|
||||
python3 -m venv .venv && . .venv/bin/activate && python3 -m pip install -e '.[dev]'
|
||||
if command -v uv &> /dev/null
|
||||
then
|
||||
uv venv
|
||||
uv sync --all-extras
|
||||
else
|
||||
python3 -m venv .venv && . .venv/bin/activate
|
||||
python3 -m pip install -e '.[dev]'
|
||||
fi
|
||||
|
||||
echo "Installing Web App..."
|
||||
cd "$PROJECT_ROOT/src/interface/web"
|
||||
yarn install && yarn export
|
||||
if command -v bun &> /dev/null
|
||||
then
|
||||
echo "using Bun."
|
||||
bun install && bun run export
|
||||
else
|
||||
echo "using Yarn."
|
||||
yarn install && yarn export
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Obsidian App
|
||||
|
||||
@@ -17,7 +17,6 @@ package dev.khoj.app;
|
||||
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
|
||||
@@ -35,11 +34,7 @@ public class LauncherActivity
|
||||
// Oreo and below. We only set the orientation on Oreo and above. This only affects the
|
||||
// splash screen and Chrome will still respect the orientation.
|
||||
// See https://github.com/GoogleChromeLabs/bubblewrap/issues/496 for details.
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
} else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function copyParentText(event, message=null) { //same
|
||||
function copyParentText(event, message=null) {
|
||||
const button = event.currentTarget;
|
||||
const textContent = message ?? button.parentNode.textContent.trim();
|
||||
navigator.clipboard.writeText(textContent).then(() => {
|
||||
@@ -17,19 +17,19 @@ function copyParentText(event, message=null) { //same
|
||||
});
|
||||
}
|
||||
|
||||
function createCopyParentText(message) { //same
|
||||
function createCopyParentText(message) {
|
||||
return function(event) {
|
||||
copyParentText(event, message);
|
||||
}
|
||||
}
|
||||
function formatDate(date) { //same
|
||||
function formatDate(date) {
|
||||
// Format date in HH:MM, DD MMM YYYY format
|
||||
let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit'}).replaceAll('-', ' ');
|
||||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
function generateReference(referenceJson, index) { //same
|
||||
function generateReference(referenceJson, index) {
|
||||
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
|
||||
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
|
||||
|
||||
@@ -62,7 +62,7 @@ function generateReference(referenceJson, index) { //same
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function generateOnlineReference(reference, index) { //same
|
||||
function generateOnlineReference(reference, index) {
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
let title = reference.title || reference.link;
|
||||
@@ -107,7 +107,7 @@ function generateOnlineReference(reference, index) { //same
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") { //same
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") {
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
let formattedMessage = formatHTMLMessage(message, raw);
|
||||
@@ -145,7 +145,7 @@ function renderMessage(message, by, dt=null, annotations=null, raw=false, render
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
}
|
||||
|
||||
function processOnlineReferences(referenceSection, onlineContext) { //same
|
||||
function processOnlineReferences(referenceSection, onlineContext) {
|
||||
let numOnlineReferences = 0;
|
||||
for (let subquery in onlineContext) {
|
||||
let onlineReference = onlineContext[subquery];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.42.8",
|
||||
"version": "2.0.0-beta.28",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Khoj Inc. <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
@@ -10,15 +10,19 @@
|
||||
"main": "main.js",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"electron": "28.3.2"
|
||||
"electron": "35.7.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn electron ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@todesktop/runtime": "^2.0.0",
|
||||
"axios": "^1.8.2",
|
||||
"axios": "^1.13.5",
|
||||
"cron": "^2.4.3",
|
||||
"electron-store": "^8.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"ajv": "^8.18.0",
|
||||
"picomatch": ">=2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
"@types/responselike" "^1.0.0"
|
||||
|
||||
"@types/http-cache-semantics@*":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
|
||||
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#f6a7788f438cbfde15f29acad46512b4c01913b3"
|
||||
integrity sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==
|
||||
|
||||
"@types/keyv@^3.1.4":
|
||||
version "3.1.4"
|
||||
@@ -90,18 +90,18 @@
|
||||
integrity sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==
|
||||
|
||||
"@types/node@*":
|
||||
version "24.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.10.tgz#f65a169779bf0d70203183a1890be7bee8ca2ddb"
|
||||
integrity sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==
|
||||
version "25.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.5.0.tgz#5c99f37c443d9ccc4985866913f1ed364217da31"
|
||||
integrity sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==
|
||||
dependencies:
|
||||
undici-types "~7.8.0"
|
||||
undici-types "~7.18.0"
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.19.115"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.115.tgz#cd94caf14472021b4443c99bcd7aac6bb5c4f672"
|
||||
integrity sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==
|
||||
"@types/node@^22.7.7":
|
||||
version "22.19.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.15.tgz#6091d99fdf7c08cb57dc8b1345d407ba9a1df576"
|
||||
integrity sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
undici-types "~6.21.0"
|
||||
|
||||
"@types/responselike@^1.0.0":
|
||||
version "1.0.3"
|
||||
@@ -132,10 +132,10 @@ ajv-formats@^2.1.1:
|
||||
dependencies:
|
||||
ajv "^8.0.0"
|
||||
|
||||
ajv@^8.0.0, ajv@^8.6.3:
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
|
||||
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
|
||||
ajv@^8.0.0, ajv@^8.18.0, ajv@^8.6.3:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc"
|
||||
integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
fast-uri "^3.0.1"
|
||||
@@ -162,13 +162,13 @@ atomically@^1.7.0:
|
||||
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
|
||||
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
|
||||
|
||||
axios@^1.8.2:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54"
|
||||
integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==
|
||||
axios@^1.13.5:
|
||||
version "1.13.6"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.6.tgz#c3f92da917dc209a15dd29936d20d5089b6b6c98"
|
||||
integrity sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
follow-redirects "^1.15.11"
|
||||
form-data "^4.0.5"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
@@ -201,10 +201,10 @@ buffer-crc32@~0.2.3:
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||
|
||||
builder-util-runtime@9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz#0daedde0f6d381f2a00a50a407b166fe7dca1a67"
|
||||
integrity sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==
|
||||
builder-util-runtime@9.5.1:
|
||||
version "9.5.1"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz#74125fb374d1ecbf472ae1787485485ff7619702"
|
||||
integrity sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
@@ -300,9 +300,9 @@ debounce-fn@^4.0.0:
|
||||
mimic-fn "^3.0.0"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.4:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
@@ -392,26 +392,26 @@ electron-store@^8.1.0:
|
||||
type-fest "^2.17.0"
|
||||
|
||||
electron-updater@^6.3.9:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.2.tgz#3e65e044f1a99b00d61e200e24de8e709c69ce99"
|
||||
integrity sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==
|
||||
version "6.8.3"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.8.3.tgz#bb0c8ef6509e5c67663f6481a729244d1bce21fb"
|
||||
integrity sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==
|
||||
dependencies:
|
||||
builder-util-runtime "9.3.1"
|
||||
builder-util-runtime "9.5.1"
|
||||
fs-extra "^10.1.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isequal "^4.5.0"
|
||||
semver "^7.6.3"
|
||||
semver "~7.7.3"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@28.3.2:
|
||||
version "28.3.2"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-28.3.2.tgz#5bf674fe9a440e5d8e51627c66fc8d4bce4c409f"
|
||||
integrity sha512-bmrQpdncbYNTArlg4n+qsASoXy3eeCELxeRmwUS52RNgvio1gGx5FLCwf8d4R+TsxwfkDWOaWbW0taIKheivKA==
|
||||
electron@35.7.5:
|
||||
version "35.7.5"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-35.7.5.tgz#294a4aebb2ad2a884de730c410f2358d061e8d53"
|
||||
integrity sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^18.11.18"
|
||||
"@types/node" "^22.7.7"
|
||||
extract-zip "^2.0.1"
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
@@ -511,14 +511,14 @@ fast-glob@^3.2.9:
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748"
|
||||
integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
|
||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
|
||||
integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675"
|
||||
integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -543,15 +543,15 @@ find-up@^3.0.0:
|
||||
dependencies:
|
||||
locate-path "^3.0.0"
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
follow-redirects@^1.15.11:
|
||||
version "1.15.11"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
|
||||
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.3.tgz#608b1b3f3e28be0fccf5901fc85fb3641e5cf0ae"
|
||||
integrity sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==
|
||||
form-data@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
|
||||
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
@@ -811,9 +811,9 @@ isexe@^2.0.0:
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -845,9 +845,9 @@ jsonfile@^4.0.0:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62"
|
||||
integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
@@ -961,9 +961,9 @@ mimic-response@^3.1.0:
|
||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
minimatch@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
|
||||
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
@@ -1059,10 +1059,10 @@ pend@~1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
picomatch@>=2.3.2, picomatch@^2.3.1:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
pkg-up@^3.1.0:
|
||||
version "3.1.0"
|
||||
@@ -1082,9 +1082,9 @@ proxy-from-env@^1.1.0:
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d"
|
||||
integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.4.tgz#1f313430527fa8b905622ebd22fe1444e757ab3c"
|
||||
integrity sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
@@ -1148,9 +1148,9 @@ run-parallel@^1.1.9:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
sax@^1.2.4:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
|
||||
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.0.tgz#da59637629307b97e7c4cb28e080a7bc38560d5b"
|
||||
integrity sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -1162,10 +1162,10 @@ semver@^6.2.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.2, semver@^7.3.5, semver@^7.6.3:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
semver@^7.3.2, semver@^7.3.5, semver@^7.6.3, semver@~7.7.3:
|
||||
version "7.7.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
|
||||
|
||||
serialize-error@^7.0.1:
|
||||
version "7.0.1"
|
||||
@@ -1235,15 +1235,15 @@ type-fest@^2.17.0:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
|
||||
undici-types@~7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
|
||||
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
|
||||
undici-types@~7.18.0:
|
||||
version "7.18.2"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9"
|
||||
integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
;; Saba Imran <saba@khoj.dev>
|
||||
;; Description: Your Second Brain
|
||||
;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image
|
||||
;; Version: 1.42.8
|
||||
;; Version: 2.0.0-beta.28
|
||||
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
|
||||
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "1.42.8",
|
||||
"version": "2.0.0-beta.28",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Khoj Inc.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.42.8",
|
||||
"version": "2.0.0-beta.28",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
@@ -29,7 +29,10 @@
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff": "^8.0.2",
|
||||
"diff": "^8.0.3",
|
||||
"isomorphic-dompurify": "^2.25.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"picomatch": ">=2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
30
src/interface/obsidian/src/api.ts
Normal file
30
src/interface/obsidian/src/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export async function deleteContentByType(khojUrl: string, khojApiKey: string, contentType: string): Promise<void> {
|
||||
// Deletes all content of a given type on Khoj server for Obsidian client
|
||||
const response = await fetch(`${khojUrl}/api/content/type/${contentType}?client=obsidian`, {
|
||||
method: 'DELETE',
|
||||
headers: khojApiKey ? { 'Authorization': `Bearer ${khojApiKey}` } : {},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new Error(`Failed to delete content type ${contentType}: ${response.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadContentBatch(khojUrl: string, khojApiKey: string, files: { blob: Blob, path: string }[]): Promise<string> {
|
||||
// Uploads a batch of files to Khoj content endpoint
|
||||
const formData = new FormData();
|
||||
files.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path); });
|
||||
|
||||
const response = await fetch(`${khojUrl}/api/content?client=obsidian`, {
|
||||
method: 'PATCH',
|
||||
headers: khojApiKey ? { 'Authorization': `Bearer ${khojApiKey}` } : {},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new Error(`Failed to upload batch: ${response.status} ${text}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ItemView, MarkdownRenderer, Scope, WorkspaceLeaf, request, requestUrl, setIcon, Platform, TFile } from 'obsidian';
|
||||
import * as DOMPurify from 'isomorphic-dompurify';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { KhojPaneView } from 'src/pane_view';
|
||||
import { KhojView, createCopyParentText, getLinkToEntry, pasteTextAtCursor } from 'src/utils';
|
||||
import { KhojSearchModal } from 'src/search_modal';
|
||||
import Khoj from 'src/main';
|
||||
import { FileInteractions, EditBlock } from 'src/interact_with_files';
|
||||
|
||||
export interface ChatJsonResult {
|
||||
@@ -67,12 +67,12 @@ interface Agent {
|
||||
|
||||
export class KhojChatView extends KhojPaneView {
|
||||
result: string;
|
||||
setting: KhojSetting;
|
||||
waitingForLocation: boolean;
|
||||
location: Location = { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone };
|
||||
keyPressTimeout: NodeJS.Timeout | null = null;
|
||||
userMessages: string[] = []; // Store user sent messages for input history cycling
|
||||
currentMessageIndex: number = -1; // Track current message index in userMessages array
|
||||
voiceChatActive: boolean = false; // Flag to track if voice chat is active
|
||||
private currentUserInput: string = ""; // Stores the current user input that is being typed in chat
|
||||
private startingMessage: string = this.getLearningMoment();
|
||||
chatMessageState: ChatMessageState;
|
||||
@@ -101,10 +101,13 @@ export class KhojChatView extends KhojPaneView {
|
||||
// 2. Higher invalid edit blocks than tolerable
|
||||
private maxEditRetries: number = 1; // Maximum retries for edit blocks
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf, setting);
|
||||
constructor(leaf: WorkspaceLeaf, plugin: Khoj) {
|
||||
super(leaf, plugin);
|
||||
this.fileInteractions = new FileInteractions(this.app);
|
||||
|
||||
// Initialize file access mode from persisted settings
|
||||
this.fileAccessMode = this.setting.fileAccessMode ?? 'read';
|
||||
|
||||
this.waitingForLocation = true;
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
@@ -127,9 +130,9 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
// Register chat view keybindings
|
||||
this.scope = new Scope(this.app.scope);
|
||||
this.scope.register(["Ctrl", "Alt"], 'n', (_) => this.createNewConversation());
|
||||
this.scope.register(["Ctrl", "Alt"], 'n', (_) => this.createNewConversation(this.currentAgent));
|
||||
this.scope.register(["Ctrl", "Alt"], 'o', async (_) => await this.toggleChatSessions());
|
||||
this.scope.register(["Ctrl", "Alt"], 'v', (_) => this.speechToText(new KeyboardEvent('keydown')));
|
||||
this.scope.register(["Ctrl", "Alt"], 'v', (_) => this.speechToText(this.voiceChatActive ? new KeyboardEvent('keyup') : new KeyboardEvent('keydown')));
|
||||
this.scope.register(["Ctrl"], 'f', (_) => new KhojSearchModal(this.app, this.setting).open());
|
||||
this.scope.register(["Ctrl"], 'r', (_) => { this.activateView(KhojView.SIMILAR); });
|
||||
}
|
||||
@@ -220,7 +223,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
await this.fetchAgents();
|
||||
|
||||
// Populate the agent selector in the header
|
||||
const headerAgentSelect = this.contentEl.querySelector('#khoj-header-agent-select') as HTMLSelectElement;
|
||||
const headerAgentSelect = this.contentEl.querySelector('.khoj-header-agent-select') as HTMLSelectElement;
|
||||
if (headerAgentSelect && this.agents.length > 0) {
|
||||
// Clear existing options
|
||||
headerAgentSelect.innerHTML = '';
|
||||
@@ -272,29 +275,48 @@ export class KhojChatView extends KhojPaneView {
|
||||
text: "File Access",
|
||||
attr: {
|
||||
class: "khoj-input-row-button clickable-icon",
|
||||
title: "Toggle file access mode (Read Only)",
|
||||
title: "Toggle open file access",
|
||||
},
|
||||
});
|
||||
setIcon(fileAccessButton, "file-search");
|
||||
fileAccessButton.addEventListener('click', () => {
|
||||
// Set initial icon based on persisted setting
|
||||
switch (this.fileAccessMode) {
|
||||
case 'none':
|
||||
setIcon(fileAccessButton, "file-x");
|
||||
fileAccessButton.title = "Toggle open file access (No Access)";
|
||||
break;
|
||||
case 'write':
|
||||
setIcon(fileAccessButton, "file-edit");
|
||||
fileAccessButton.title = "Toggle open file access (Read & Write)";
|
||||
break;
|
||||
case 'read':
|
||||
default:
|
||||
setIcon(fileAccessButton, "file-search");
|
||||
fileAccessButton.title = "Toggle open file access (Read Only)";
|
||||
break;
|
||||
}
|
||||
fileAccessButton.addEventListener('click', async () => {
|
||||
// Cycle through modes: none -> read -> write -> none
|
||||
switch (this.fileAccessMode) {
|
||||
case 'none':
|
||||
this.fileAccessMode = 'read';
|
||||
setIcon(fileAccessButton, "file-search");
|
||||
fileAccessButton.title = "Toggle file access mode (Read Only)";
|
||||
fileAccessButton.title = "Toggle open file access (Read Only)";
|
||||
break;
|
||||
case 'read':
|
||||
this.fileAccessMode = 'write';
|
||||
setIcon(fileAccessButton, "file-edit");
|
||||
fileAccessButton.title = "Toggle file access mode (Read & Write)";
|
||||
fileAccessButton.title = "Toggle open file access (Read & Write)";
|
||||
break;
|
||||
case 'write':
|
||||
this.fileAccessMode = 'none';
|
||||
setIcon(fileAccessButton, "file-x");
|
||||
fileAccessButton.title = "Toggle file access mode (No Access)";
|
||||
fileAccessButton.title = "Toggle open file access (No Access)";
|
||||
break;
|
||||
}
|
||||
|
||||
// Persist the updated mode to settings
|
||||
this.setting.fileAccessMode = this.fileAccessMode;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
let chatInput = inputRow.createEl("textarea", {
|
||||
@@ -319,7 +341,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
attr: {
|
||||
id: "khoj-transcribe",
|
||||
class: "khoj-transcribe khoj-input-row-button clickable-icon ",
|
||||
title: "Start Voice Chat (Ctrl+Alt+V)",
|
||||
title: "Hold to Voice Chat (Ctrl+Alt+V)",
|
||||
},
|
||||
})
|
||||
transcribe.addEventListener('mousedown', (event) => { this.startSpeechToText(event) });
|
||||
@@ -943,7 +965,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
return learningMoments[Math.floor(Math.random() * learningMoments.length)];
|
||||
}
|
||||
|
||||
async createNewConversation(agentSlug?: string) {
|
||||
async createNewConversation(agentSlug?: string | null) {
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
chatBodyEl.innerHTML = "";
|
||||
chatBodyEl.dataset.conversationId = "";
|
||||
@@ -960,6 +982,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
try {
|
||||
// Create a new conversation with or without an agent
|
||||
let endpoint = `${this.setting.khojUrl}/api/chat/sessions`;
|
||||
agentSlug = agentSlug || this.currentAgent;
|
||||
if (agentSlug) {
|
||||
endpoint += `?agent_slug=${encodeURIComponent(agentSlug)}`;
|
||||
}
|
||||
@@ -979,7 +1002,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
this.currentAgent = agentSlug || null;
|
||||
|
||||
// Update agent selector to reflect current agent
|
||||
const agentSelect = this.contentEl.querySelector('.khoj-agent-select') as HTMLSelectElement;
|
||||
const agentSelect = this.contentEl.querySelector('.khoj-header-agent-select') as HTMLSelectElement;
|
||||
if (agentSelect) {
|
||||
agentSelect.value = this.currentAgent || '';
|
||||
}
|
||||
@@ -1009,7 +1032,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
const newConversationButtonEl = newConversationEl.createEl("button");
|
||||
newConversationButtonEl.classList.add("new-conversation-button");
|
||||
newConversationButtonEl.classList.add("side-panel-button");
|
||||
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation());
|
||||
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation(this.currentAgent));
|
||||
setIcon(newConversationButtonEl, "plus");
|
||||
newConversationButtonEl.innerHTML += "New";
|
||||
newConversationButtonEl.title = "New Conversation (Ctrl+Alt+N)";
|
||||
@@ -1036,7 +1059,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
if (incomingConversationId == conversationId) {
|
||||
conversationSessionEl.classList.add("selected-conversation");
|
||||
}
|
||||
const conversationTitle = conversation["slug"] || `New conversation 🌱`;
|
||||
const conversationTitle = conversation["slug"].split("<SYSTEM>")[0].trim() || `New conversation 🌱`;
|
||||
const conversationSessionTitleEl = conversationSessionEl.createDiv("conversation-session-title");
|
||||
conversationSessionTitleEl.textContent = conversationTitle;
|
||||
conversationSessionTitleEl.addEventListener('click', () => {
|
||||
@@ -1189,7 +1212,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
chatUrl += `&conversation_id=${chatBodyEl.dataset.conversationId}`;
|
||||
}
|
||||
|
||||
console.log("Fetching chat history from:", chatUrl);
|
||||
console.debug("Fetching chat history from:", chatUrl);
|
||||
|
||||
try {
|
||||
let response = await fetch(chatUrl, {
|
||||
@@ -1198,7 +1221,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
|
||||
let responseJson: any = await response.json();
|
||||
console.log("Chat history response:", responseJson);
|
||||
console.debug("Chat history response:", responseJson);
|
||||
|
||||
chatBodyEl.dataset.conversationId = responseJson.conversation_id;
|
||||
|
||||
@@ -1220,10 +1243,10 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
// Update current agent from conversation history
|
||||
if (responseJson.response.agent?.slug) {
|
||||
console.log("Found agent in conversation history:", responseJson.response.agent);
|
||||
console.debug("Found agent in conversation history:", responseJson.response.agent);
|
||||
this.currentAgent = responseJson.response.agent.slug;
|
||||
// Update the agent selector if it exists
|
||||
const agentSelect = this.contentEl.querySelector('.khoj-agent-select') as HTMLSelectElement;
|
||||
const agentSelect = this.contentEl.querySelector('.khoj-header-agent-select') as HTMLSelectElement;
|
||||
if (agentSelect && this.currentAgent) {
|
||||
agentSelect.value = this.currentAgent;
|
||||
console.log("Updated agent selector to:", this.currentAgent);
|
||||
@@ -1351,18 +1374,25 @@ export class KhojChatView extends KhojPaneView {
|
||||
if (this.fileAccessMode === 'write') {
|
||||
const editBlocks = this.parseEditBlocks(this.chatMessageState.rawResponse);
|
||||
|
||||
// Check for errors and retry if needed
|
||||
if (editBlocks.length > 0 && editBlocks[0].hasError && this.editRetryCount < this.maxEditRetries) {
|
||||
await this.handleEditRetry(editBlocks[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset retry count on success
|
||||
this.editRetryCount = 0;
|
||||
|
||||
// Apply edits if there are any
|
||||
if (editBlocks.length > 0) {
|
||||
await this.applyEditBlocks(editBlocks);
|
||||
const firstBlock = editBlocks[0];
|
||||
if (firstBlock.hasError) {
|
||||
// Only retry if we have remaining attempts; do NOT reset counter on failure
|
||||
if (this.editRetryCount < this.maxEditRetries) {
|
||||
await this.handleEditRetry(firstBlock);
|
||||
return; // Wait for retry response
|
||||
} else {
|
||||
// Exhausted retries; surface error and do not attempt further automatic retries
|
||||
console.warn('[Khoj] Max edit retries reached. Aborting further retries.');
|
||||
}
|
||||
} else {
|
||||
// Successful parse => reset counter and apply edits
|
||||
this.editRetryCount = 0;
|
||||
await this.applyEditBlocks(editBlocks);
|
||||
}
|
||||
} else {
|
||||
// No edit blocks => reset counter just in case
|
||||
this.editRetryCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1719,6 +1749,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
// Toggle recording
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart' || event.type === 'mousedown' || event.type === 'keydown') {
|
||||
this.voiceChatActive = true;
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
?.then(handleRecording)
|
||||
@@ -1726,6 +1757,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
this.flashStatusInChatInput("⛔️ Failed to access microphone");
|
||||
});
|
||||
} else if (this.mediaRecorder?.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel' || event.type === 'mouseup' || event.type === 'keyup') {
|
||||
this.voiceChatActive = false;
|
||||
this.mediaRecorder.stop();
|
||||
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
this.mediaRecorder = undefined;
|
||||
@@ -2402,7 +2434,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
// Add retry count
|
||||
retryBadge.createSpan({
|
||||
cls: "retry-count",
|
||||
text: `Attempt ${this.editRetryCount}/3`
|
||||
text: `Attempt ${this.editRetryCount}/${this.maxEditRetries}`
|
||||
});
|
||||
|
||||
// Add error details as a tooltip
|
||||
@@ -2417,7 +2449,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
retryBadge.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
// Create a retry prompt for the LLM
|
||||
const retryPrompt = `/general I noticed some issues with the edit block. Please fix the following and provide a corrected version (retry ${this.editRetryCount}/3):\n\n${errorDetails}\n\nPlease provide a new edit block that fixes these issues. Make sure to follow the exact format required.`;
|
||||
const retryPrompt = `/general I noticed some issues with the edit block. Please fix the following and provide a corrected version (retry ${this.editRetryCount}/${this.maxEditRetries}):\n\n${errorDetails}\n\nPlease provide a new edit block that fixes these issues. Make sure to follow the exact format required.`;
|
||||
|
||||
// Send retry request without displaying the user message
|
||||
await this.getChatResponse(retryPrompt, "", false, false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { App, MarkdownView, TFile } from 'obsidian';
|
||||
import { diffWords } from 'diff';
|
||||
|
||||
/**
|
||||
@@ -55,6 +55,7 @@ export class FileInteractions {
|
||||
private app: App;
|
||||
private readonly EDIT_BLOCK_START = '<khoj_edit>';
|
||||
private readonly EDIT_BLOCK_END = '</khoj_edit>';
|
||||
private readonly CONTEXT_FILES_LIMIT = 3;
|
||||
|
||||
/**
|
||||
* Constructor for FileInteractions
|
||||
@@ -65,6 +66,26 @@ export class FileInteractions {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get N open, recently viewed markdown files.
|
||||
*/
|
||||
private getRecentActiveMarkdownFiles(N: number): TFile[] {
|
||||
const seen = new Set<string>();
|
||||
const recentActiveFiles = this.app.workspace.getLeavesOfType('markdown')
|
||||
.sort((a, b) => (b as any).activeTime - (a as any).activeTime) // Sort by leaf activeTime (note: undocumented prop)
|
||||
.map(leaf => (leaf.view as MarkdownView)?.file)
|
||||
// Dedupe by file path
|
||||
.filter((file): file is TFile => {
|
||||
if (!file || seen.has(file.path)) return false;
|
||||
seen.add(file.path);
|
||||
return true;
|
||||
})
|
||||
.slice(0, N);
|
||||
|
||||
console.log(`Using ${recentActiveFiles.length} recently viewed md files for context: ${recentActiveFiles.map(file => file.path).join(', ')}`);
|
||||
return recentActiveFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content of all open files
|
||||
*
|
||||
@@ -75,9 +96,9 @@ export class FileInteractions {
|
||||
// Only proceed if we have read or write access
|
||||
if (fileAccessMode === 'none') return '';
|
||||
|
||||
// Get all open markdown leaves
|
||||
const leaves = this.app.workspace.getLeavesOfType('markdown');
|
||||
if (leaves.length === 0) return '';
|
||||
// Get recently viewed markdown files
|
||||
const recentFiles = this.getRecentActiveMarkdownFiles(this.CONTEXT_FILES_LIMIT);
|
||||
if (recentFiles.length === 0) return '';
|
||||
|
||||
// Instructions in write access mode
|
||||
let editInstructions: string = '';
|
||||
@@ -274,11 +295,7 @@ For context, the user is currently working on the following files:
|
||||
|
||||
`;
|
||||
|
||||
for (const leaf of leaves) {
|
||||
const view = leaf.view as any;
|
||||
const file = view?.file;
|
||||
if (!file || file.extension !== 'md') continue;
|
||||
|
||||
for (const file of recentFiles) {
|
||||
// Read file content
|
||||
let fileContent: string;
|
||||
try {
|
||||
@@ -415,8 +432,16 @@ For context, the user is currently working on the following files:
|
||||
}
|
||||
|
||||
// Try parse SEARCH/REPLACE format for complete edit blocks
|
||||
// Regex: file_path\n<<<<<<< SEARCH\nsearch_content\n=======\nreplacement_content\n>>>>>>> REPLACE
|
||||
const newFormatRegex = /^([^\n]+)\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE\s*$/;
|
||||
// Supports empty SEARCH (new file / replace whole file) and empty REPLACE (deletion)
|
||||
// Regex structure:
|
||||
// file_path (group 1)
|
||||
// <<<<<<< SEARCH literal marker
|
||||
// search_content (group 2, can be empty)
|
||||
// ======= divider
|
||||
// replacement_content (group 3, can be empty => deletion)
|
||||
// >>>>>>> REPLACE end marker
|
||||
// Note: The trailing newline before the end marker is optional to allow zero-length replacement
|
||||
const newFormatRegex = /^([^\n]+)\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n?>>>>>>> REPLACE\s*$/;
|
||||
const newFormatMatch = newFormatRegex.exec(cleanContent);
|
||||
|
||||
let editData: EditBlock | null = null;
|
||||
@@ -430,32 +455,25 @@ For context, the user is currently working on the following files:
|
||||
|
||||
// Validate required fields
|
||||
let error: { type: 'missing_field' | 'invalid_format' | 'preprocessing' | 'unknown', message: string, details?: string } | null = null;
|
||||
if (!editData) {
|
||||
error = {
|
||||
type: 'invalid_format',
|
||||
message: 'Invalid edit block format',
|
||||
details: 'The edit block does not match the expected format'
|
||||
};
|
||||
}
|
||||
else if (!editData.file) {
|
||||
if (editData && !editData.file) {
|
||||
error = {
|
||||
type: 'missing_field',
|
||||
message: 'Missing "file" field in edit block',
|
||||
details: 'The "file" field is required and should contain the target file name'
|
||||
};
|
||||
}
|
||||
else if (editData.find === undefined || editData.find === null) {
|
||||
else if (editData && (editData.find === undefined || editData.find === null)) {
|
||||
error = {
|
||||
type: 'missing_field',
|
||||
message: 'Missing "find" field markers',
|
||||
details: 'The "find" field is required and should contain the content to find in the file'
|
||||
details: 'The "find" field is required. It should contain the content to find in the file or be empty for new files'
|
||||
};
|
||||
}
|
||||
else if (!editData.replace) {
|
||||
else if (editData && editData.replace === undefined) {
|
||||
error = {
|
||||
type: 'missing_field',
|
||||
message: 'Missing "replace" field in edit block',
|
||||
details: 'The "replace" field is required and should contain the replacement text'
|
||||
details: 'The "replace" field is required. It should contain the content to replace or be empty to indicate deletion'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -507,7 +525,7 @@ For context, the user is currently working on the following files:
|
||||
}
|
||||
|
||||
if (!editData) {
|
||||
console.error("No edit data parsed");
|
||||
console.debug("No edit data parsed");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -684,10 +702,8 @@ For context, the user is currently working on the following files:
|
||||
// Track current content for each file as we apply edits
|
||||
const currentFileContents = new Map<string, string>();
|
||||
|
||||
// Get all open markdown files
|
||||
const files = this.app.workspace.getLeavesOfType('markdown')
|
||||
.map(leaf => (leaf.view as any)?.file)
|
||||
.filter(file => file && file.extension === 'md');
|
||||
// Get recently viewed markdown file(s) to edit
|
||||
const files = this.getRecentActiveMarkdownFiles(this.CONTEXT_FILES_LIMIT);
|
||||
|
||||
// Track success/failure for each edit
|
||||
const editResults: { block: EditBlock, success: boolean, error?: string }[] = [];
|
||||
@@ -883,6 +899,10 @@ For context, the user is currently working on the following files:
|
||||
|
||||
// Parse the block content
|
||||
const { editData, cleanContent, error, inProgress } = this.parseEditBlock(content, isComplete);
|
||||
if (!editData && !error) {
|
||||
// If no edit data and no error, skip this block
|
||||
continue;
|
||||
}
|
||||
|
||||
// Escape content for HTML display
|
||||
const diff = diffWords(editData?.find || '', editData?.replace || '');
|
||||
@@ -898,7 +918,7 @@ For context, the user is currently working on the following files:
|
||||
).join('').trim();
|
||||
|
||||
let htmlRender = '';
|
||||
if (error || !editData) {
|
||||
if (error) {
|
||||
// Error block
|
||||
console.error("Error parsing khoj-edit block:", error);
|
||||
console.error("Content causing error:", content);
|
||||
@@ -913,7 +933,7 @@ For context, the user is currently working on the following files:
|
||||
<pre><code class="language-md error">${diffContent}</code></pre>
|
||||
</div>
|
||||
</details>`;
|
||||
} else if (inProgress) {
|
||||
} else if (editData && inProgress) {
|
||||
// In-progress block
|
||||
htmlRender = `<details class="khoj-edit-accordion in-progress">
|
||||
<summary>📄 ${editData.file} <span class="khoj-edit-status">In Progress</span></summary>
|
||||
@@ -921,7 +941,7 @@ For context, the user is currently working on the following files:
|
||||
<pre><code class="language-md">${diffContent}</code></pre>
|
||||
</div>
|
||||
</details>`;
|
||||
} else {
|
||||
} else if (editData) {
|
||||
// Success block
|
||||
// Find the actual file that will be modified
|
||||
const targetFile = this.findBestMatchingFile(editData.file, files);
|
||||
|
||||
@@ -3,8 +3,8 @@ import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings'
|
||||
import { KhojSearchModal } from 'src/search_modal'
|
||||
import { KhojChatView } from 'src/chat_view'
|
||||
import { KhojSimilarView } from 'src/similar_view'
|
||||
import { updateContentIndex, canConnectToBackend, KhojView, jumpToPreviousView } from './utils';
|
||||
import { KhojPaneView } from './pane_view';
|
||||
import { updateContentIndex, canConnectToBackend, KhojView } from 'src/utils';
|
||||
import { KhojPaneView } from 'src/pane_view';
|
||||
|
||||
|
||||
export default class Khoj extends Plugin {
|
||||
@@ -73,7 +73,7 @@ export default class Khoj extends Plugin {
|
||||
this.activateView(KhojView.CHAT).then(() => {
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
chatView.toggleChatSessions(true);
|
||||
chatView.toggleChatSessions();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -88,8 +88,9 @@ export default class Khoj extends Plugin {
|
||||
this.activateView(KhojView.CHAT).then(() => {
|
||||
const chatView = this.app.workspace.getActiveViewOfType(KhojChatView);
|
||||
if (chatView) {
|
||||
// Trigger speech to text functionality
|
||||
chatView.speechToText(new KeyboardEvent('keydown'));
|
||||
// Toggle speech to text functionality
|
||||
const toggleEvent = chatView.voiceChatActive ? 'keyup' : 'keydown';
|
||||
chatView.speechToText(new KeyboardEvent(toggleEvent));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -136,8 +137,8 @@ export default class Khoj extends Plugin {
|
||||
});
|
||||
|
||||
// Register views
|
||||
this.registerView(KhojView.CHAT, (leaf) => new KhojChatView(leaf, this.settings));
|
||||
this.registerView(KhojView.SIMILAR, (leaf) => new KhojSimilarView(leaf, this.settings));
|
||||
this.registerView(KhojView.CHAT, (leaf) => new KhojChatView(leaf, this));
|
||||
this.registerView(KhojView.SIMILAR, (leaf) => new KhojSimilarView(leaf, this));
|
||||
|
||||
// Create an icon in the left ribbon.
|
||||
this.addRibbonIcon('message-circle', 'Khoj', (_: MouseEvent) => {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { KhojView, populateHeaderPane } from './utils';
|
||||
import Khoj from 'src/main';
|
||||
|
||||
export abstract class KhojPaneView extends ItemView {
|
||||
setting: KhojSetting;
|
||||
plugin: Khoj;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
constructor(leaf: WorkspaceLeaf, plugin: Khoj) {
|
||||
super(leaf);
|
||||
|
||||
this.setting = setting;
|
||||
this.setting = plugin.settings;
|
||||
this.plugin = plugin;
|
||||
|
||||
// Register Modal Keybindings to send user message
|
||||
// this.scope.register([], 'Enter', async () => { await this.chat() });
|
||||
|
||||
@@ -16,6 +16,10 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
currentController: AbortController | null = null; // To cancel requests
|
||||
isLoading: boolean = false;
|
||||
loadingEl: HTMLElement;
|
||||
private isFileFilterMode: boolean = false;
|
||||
private fileSelected: string = "";
|
||||
private allFiles: Array<{path: string, inVault: boolean}> = [];
|
||||
private resultsTitle: HTMLDivElement;
|
||||
|
||||
constructor(app: App, setting: KhojSetting, find_similar_notes: boolean = false) {
|
||||
super(app);
|
||||
@@ -85,6 +89,46 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
|
||||
// Set Placeholder Text for Modal
|
||||
this.setPlaceholder('Search with Khoj...');
|
||||
|
||||
// Initialize allFiles with files in vault
|
||||
this.allFiles = this.app.vault.getFiles().map(file => ({
|
||||
path: file.path,
|
||||
inVault: true
|
||||
}));
|
||||
|
||||
// Update isFileFilterMode when input changes
|
||||
this.inputEl.addEventListener('input', () => {
|
||||
// Match file: at the end of input, with an optional unquoted partial path
|
||||
const fileFilterMatch = this.inputEl.value.match(/file:([^"\s]*)$/);
|
||||
if (fileFilterMatch) {
|
||||
// Enter file filter mode when we see an unquoted file: token
|
||||
this.isFileFilterMode = true;
|
||||
} else {
|
||||
// Exit file filter mode when input no longer ends with an unquoted file: token
|
||||
this.isFileFilterMode = false;
|
||||
this.fileSelected = "";
|
||||
}
|
||||
});
|
||||
|
||||
// Override selectSuggestion to prevent modal close during file filter selection
|
||||
const originalSelectSuggestion = this.selectSuggestion.bind(this);
|
||||
this.selectSuggestion = async (value: SearchResult & { inVault: boolean }, evt: MouseEvent | KeyboardEvent) => {
|
||||
if (this.isFileFilterMode) {
|
||||
// In file filter mode, handle selection without closing the modal
|
||||
await this.onChooseSuggestion(value, evt);
|
||||
} else {
|
||||
// For normal search results, use the original behavior
|
||||
originalSelectSuggestion(value, evt);
|
||||
}
|
||||
};
|
||||
|
||||
// Add title element
|
||||
this.resultsTitle = createDiv();
|
||||
this.resultsTitle.style.padding = "8px";
|
||||
this.resultsTitle.style.fontWeight = "bold";
|
||||
|
||||
// Insert title before results container
|
||||
this.resultContainerEl.parentElement?.insertBefore(this.resultsTitle, this.resultContainerEl);
|
||||
}
|
||||
|
||||
// Check if the file exists in the vault
|
||||
@@ -99,7 +143,30 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
}
|
||||
|
||||
async getSuggestions(query: string): Promise<SearchResult[]> {
|
||||
// Do not show loading if the query is empty
|
||||
// Check if we are in file filter mode and input matches file filter pattern
|
||||
const fileFilterMatch = query.match(/file:([^"\s]*)$/);
|
||||
if (this.isFileFilterMode && fileFilterMatch) {
|
||||
const partialPath = fileFilterMatch[1] || '';
|
||||
// Update title for file filter mode
|
||||
this.resultsTitle.setText("Select a file:");
|
||||
// Return filtered file suggestions
|
||||
return this.allFiles
|
||||
.filter(file => file.path.toLowerCase().includes(partialPath.toLowerCase().trim()))
|
||||
.map(file => ({
|
||||
entry: file.path,
|
||||
file: file.path,
|
||||
inVault: file.inVault
|
||||
}));
|
||||
}
|
||||
|
||||
// Update title for search results
|
||||
if (query.trim()) {
|
||||
this.resultsTitle.setText("Search results:");
|
||||
} else {
|
||||
this.resultsTitle.setText("");
|
||||
}
|
||||
|
||||
// If not in file filter mode, continue with normal search
|
||||
if (!query.trim()) {
|
||||
this.isLoading = false;
|
||||
this.updateLoadingState();
|
||||
@@ -138,22 +205,29 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Parse search results
|
||||
// Parse search results and update allFiles with any new non-vault files
|
||||
let results = data
|
||||
.filter((result: any) =>
|
||||
!this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path)
|
||||
)
|
||||
.map((result: any) => {
|
||||
const isInVault = this.isFileInVault(result.additional.file);
|
||||
|
||||
// Add new non-vault files to allFiles if they don't exist
|
||||
if (!this.allFiles.some(file => file.path === result.additional.file)) {
|
||||
this.allFiles.push({
|
||||
path: result.additional.file,
|
||||
inVault: isInVault
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
entry: result.entry,
|
||||
file: result.additional.file,
|
||||
inVault: this.isFileInVault(result.additional.file)
|
||||
inVault: isInVault
|
||||
} as SearchResult & { inVault: boolean };
|
||||
})
|
||||
.sort((a: SearchResult & { inVault: boolean }, b: SearchResult & { inVault: boolean }) => {
|
||||
if (a.inVault === b.inVault) return 0;
|
||||
return a.inVault ? -1 : 1;
|
||||
});
|
||||
.sort((a: SearchResult & { inVault: boolean }, b: SearchResult & { inVault: boolean }) => Number(b.inVault) - Number(a.inVault));
|
||||
|
||||
this.query = query;
|
||||
|
||||
@@ -203,6 +277,15 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
}
|
||||
|
||||
async renderSuggestion(result: SearchResult & { inVault: boolean }, el: HTMLElement) {
|
||||
if (this.isFileFilterMode) {
|
||||
// Render file suggestions
|
||||
el.createEl("div", {
|
||||
text: result.entry,
|
||||
cls: "khoj-file-suggestion"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Max number of lines to render
|
||||
let lines_to_render = 8;
|
||||
|
||||
@@ -251,6 +334,20 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
}
|
||||
|
||||
async onChooseSuggestion(result: SearchResult & { inVault: boolean }, _: MouseEvent | KeyboardEvent) {
|
||||
if (this.isFileFilterMode) {
|
||||
// When a file suggestion is selected, append it to the current input
|
||||
const currentValue = this.inputEl.value;
|
||||
const beforeFile = currentValue.substring(0, currentValue.lastIndexOf('file:'));
|
||||
this.inputEl.value = `${beforeFile}file:"${result.entry}"`;
|
||||
// Set fileSelected to the selected file
|
||||
this.fileSelected = result.entry;
|
||||
// Reset isFileFilterMode when a file is selected
|
||||
this.isFileFilterMode = false;
|
||||
// Trigger input event to refresh suggestions
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only open files that are in the vault
|
||||
if (!result.inVault) {
|
||||
new Notice("This file is not in your vault");
|
||||
|
||||
@@ -36,8 +36,10 @@ export interface KhojSetting {
|
||||
syncFileType: SyncFileTypes;
|
||||
userInfo: UserInfo | null;
|
||||
syncFolders: string[];
|
||||
excludeFolders: string[];
|
||||
syncInterval: number;
|
||||
autoVoiceResponse: boolean;
|
||||
fileAccessMode: 'none' | 'read' | 'write';
|
||||
selectedChatModelId: string | null; // Mirrors server's selected_chat_model_config
|
||||
availableChatModels: ModelOption[];
|
||||
}
|
||||
@@ -56,8 +58,10 @@ export const DEFAULT_SETTINGS: KhojSetting = {
|
||||
},
|
||||
userInfo: null,
|
||||
syncFolders: [],
|
||||
excludeFolders: [],
|
||||
syncInterval: 60,
|
||||
autoVoiceResponse: true,
|
||||
fileAccessMode: 'read',
|
||||
selectedChatModelId: null, // Will be populated from server
|
||||
availableChatModels: [],
|
||||
}
|
||||
@@ -65,6 +69,8 @@ export const DEFAULT_SETTINGS: KhojSetting = {
|
||||
export class KhojSettingTab extends PluginSettingTab {
|
||||
plugin: Khoj;
|
||||
private chatModelSetting: Setting | null = null;
|
||||
private storageProgressEl: HTMLProgressElement | null = null;
|
||||
private storageProgressText: HTMLSpanElement | null = null;
|
||||
|
||||
constructor(app: App, plugin: Khoj) {
|
||||
super(app, plugin);
|
||||
@@ -225,6 +231,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncFileType.markdown = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.refreshStorageDisplay();
|
||||
}));
|
||||
|
||||
// Add setting to sync images
|
||||
@@ -236,6 +243,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncFileType.images = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.refreshStorageDisplay();
|
||||
}));
|
||||
|
||||
// Add setting to sync PDFs
|
||||
@@ -247,6 +255,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncFileType.pdf = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.refreshStorageDisplay();
|
||||
}));
|
||||
|
||||
// Add setting for sync interval
|
||||
@@ -271,27 +280,56 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
this.plugin.restartSyncTimer();
|
||||
}));
|
||||
|
||||
// Add setting to manage sync folders
|
||||
const syncFoldersContainer = containerEl.createDiv('sync-folders-container');
|
||||
new Setting(syncFoldersContainer)
|
||||
.setName('Sync Folders')
|
||||
.setDesc('Specify folders to sync (leave empty to sync entire vault)')
|
||||
// Add setting to manage include folders
|
||||
const includeFoldersContainer = containerEl.createDiv('include-folders-container');
|
||||
new Setting(includeFoldersContainer)
|
||||
.setName('Include Folders')
|
||||
.setDesc('Folders to sync (leave empty to sync entire vault)')
|
||||
.addButton(button => button
|
||||
.setButtonText('Add Folder')
|
||||
.onClick(() => {
|
||||
const modal = new FolderSuggestModal(this.app, (folder: string) => {
|
||||
const modal = new FolderSuggestModal(this.app, async (folder: string) => {
|
||||
if (!this.plugin.settings.syncFolders.includes(folder)) {
|
||||
this.plugin.settings.syncFolders.push(folder);
|
||||
this.plugin.saveSettings();
|
||||
this.updateFolderList(folderListEl);
|
||||
await this.plugin.saveSettings();
|
||||
this.updateIncludeFolderList(includeFolderListEl);
|
||||
this.refreshStorageDisplay();
|
||||
}
|
||||
});
|
||||
modal.open();
|
||||
}));
|
||||
|
||||
// Create a list to display selected folders
|
||||
const folderListEl = syncFoldersContainer.createDiv('folder-list');
|
||||
this.updateFolderList(folderListEl);
|
||||
// Create a list to display selected include folders
|
||||
const includeFolderListEl = includeFoldersContainer.createDiv('folder-list');
|
||||
this.updateIncludeFolderList(includeFolderListEl);
|
||||
|
||||
// Add setting to manage exclude folders
|
||||
const excludeFoldersContainer = containerEl.createDiv('exclude-folders-container');
|
||||
new Setting(excludeFoldersContainer)
|
||||
.setName('Exclude Folders')
|
||||
.setDesc('Folders to exclude from sync (takes precedence over includes)')
|
||||
.addButton(button => button
|
||||
.setButtonText('Add Folder')
|
||||
.onClick(() => {
|
||||
const modal = new FolderSuggestModal(this.app, async (folder: string) => {
|
||||
// Don't allow excluding root folder
|
||||
if (folder === '') {
|
||||
new Notice('Cannot exclude the root folder');
|
||||
return;
|
||||
}
|
||||
if (!this.plugin.settings.excludeFolders.includes(folder)) {
|
||||
this.plugin.settings.excludeFolders.push(folder);
|
||||
await this.plugin.saveSettings();
|
||||
this.updateExcludeFolderList(excludeFolderListEl);
|
||||
this.refreshStorageDisplay();
|
||||
}
|
||||
});
|
||||
modal.open();
|
||||
}));
|
||||
|
||||
// Create a list to display selected exclude folders
|
||||
const excludeFolderListEl = excludeFoldersContainer.createDiv('folder-list');
|
||||
this.updateExcludeFolderList(excludeFolderListEl);
|
||||
|
||||
let indexVaultSetting = new Setting(containerEl);
|
||||
indexVaultSetting
|
||||
@@ -306,7 +344,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
button.removeCta();
|
||||
indexVaultSetting = indexVaultSetting.setDisabled(true);
|
||||
|
||||
// Show indicator for indexing in progress
|
||||
// Show indicator for indexing in progress (animated text)
|
||||
const progress_indicator = window.setInterval(() => {
|
||||
if (button.buttonEl.innerText === 'Updating 🌑') {
|
||||
button.setButtonText('Updating 🌘');
|
||||
@@ -328,17 +366,79 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
}, 300);
|
||||
this.plugin.registerInterval(progress_indicator);
|
||||
|
||||
this.plugin.settings.lastSync = await updateContentIndex(
|
||||
this.app.vault, this.plugin.settings, this.plugin.settings.lastSync, true, true
|
||||
);
|
||||
// Obtain sync progress elements by id (created below)
|
||||
const syncProgressEl = document.getElementById('khoj-sync-progress') as HTMLProgressElement | null;
|
||||
const syncProgressText = document.getElementById('khoj-sync-progress-text') as HTMLElement | null;
|
||||
|
||||
// Reset button once index is updated
|
||||
window.clearInterval(progress_indicator);
|
||||
button.setButtonText('Update');
|
||||
button.setCta();
|
||||
indexVaultSetting = indexVaultSetting.setDisabled(false);
|
||||
if (syncProgressEl && syncProgressText) {
|
||||
syncProgressEl.style.display = '';
|
||||
syncProgressText.style.display = '';
|
||||
syncProgressText.textContent = 'Preparing files...';
|
||||
syncProgressEl.value = 0;
|
||||
syncProgressEl.max = 1;
|
||||
}
|
||||
|
||||
const onProgress = (progress: { processed: number, total: number }) => {
|
||||
const el = document.getElementById('khoj-sync-progress') as HTMLProgressElement | null;
|
||||
const txt = document.getElementById('khoj-sync-progress-text') as HTMLElement | null;
|
||||
if (!el || !txt) return;
|
||||
el.max = Math.max(progress.total, 1);
|
||||
el.value = Math.min(progress.processed, el.max);
|
||||
txt.textContent = `Syncing... ${progress.processed} / ${progress.total} files`;
|
||||
};
|
||||
|
||||
try {
|
||||
this.plugin.settings.lastSync = await updateContentIndex(
|
||||
this.app.vault, this.plugin.settings, this.plugin.settings.lastSync, true, true, onProgress
|
||||
);
|
||||
} finally {
|
||||
// Cleanup: hide sync progress UI
|
||||
const el = document.getElementById('khoj-sync-progress') as HTMLProgressElement | null;
|
||||
const txt = document.getElementById('khoj-sync-progress-text') as HTMLElement | null;
|
||||
if (el) el.style.display = 'none';
|
||||
if (txt) txt.style.display = 'none';
|
||||
this.refreshStorageDisplay();
|
||||
|
||||
// Reset button state
|
||||
window.clearInterval(progress_indicator);
|
||||
button.setButtonText('Update');
|
||||
button.setCta();
|
||||
indexVaultSetting = indexVaultSetting.setDisabled(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
// Estimated Cloud Storage (client-side)
|
||||
const storageSetting = new Setting(containerEl)
|
||||
.setName('Estimated Cloud Storage')
|
||||
.setDesc('Estimated storage usage based on files configured for sync. This is a client-side estimation.')
|
||||
.then(() => { });
|
||||
|
||||
// Create custom elements: progress and text for storage estimation
|
||||
this.storageProgressEl = document.createElement('progress');
|
||||
this.storageProgressEl.value = 0;
|
||||
this.storageProgressEl.max = 1;
|
||||
this.storageProgressEl.style.width = '100%';
|
||||
this.storageProgressText = document.createElement('span');
|
||||
this.storageProgressText.textContent = 'Calculating...';
|
||||
storageSetting.descEl.appendChild(this.storageProgressEl);
|
||||
storageSetting.descEl.appendChild(this.storageProgressText);
|
||||
|
||||
// Create progress bar for Force Sync operation (hidden by default)
|
||||
const syncProgressEl = document.createElement('progress');
|
||||
syncProgressEl.id = 'khoj-sync-progress';
|
||||
syncProgressEl.value = 0;
|
||||
syncProgressEl.max = 1;
|
||||
syncProgressEl.style.width = '100%';
|
||||
syncProgressEl.style.display = 'none';
|
||||
const syncProgressText = document.createElement('span');
|
||||
syncProgressText.id = 'khoj-sync-progress-text';
|
||||
syncProgressText.textContent = '';
|
||||
syncProgressText.style.display = 'none';
|
||||
storageSetting.descEl.appendChild(syncProgressEl);
|
||||
storageSetting.descEl.appendChild(syncProgressText);
|
||||
|
||||
// Call initial update
|
||||
this.refreshStorageDisplay();
|
||||
}
|
||||
|
||||
private connectStatusIcon() {
|
||||
@@ -350,6 +450,28 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
return '🔴';
|
||||
}
|
||||
|
||||
private async refreshStorageDisplay() {
|
||||
if (!this.storageProgressEl || !this.storageProgressText) return;
|
||||
|
||||
// Show calculating state
|
||||
this.storageProgressEl.removeAttribute('value');
|
||||
this.storageProgressText.textContent = 'Calculating...';
|
||||
try {
|
||||
const { calculateVaultSyncMetrics } = await import('./utils');
|
||||
const metrics = await calculateVaultSyncMetrics(this.app.vault, this.plugin.settings);
|
||||
const usedMB = (metrics.usedBytes / (1024 * 1024));
|
||||
const totalMB = (metrics.totalBytes / (1024 * 1024));
|
||||
const usedStr = `${usedMB.toFixed(1)} MB`;
|
||||
const totalStr = `${totalMB.toFixed(0)} MB`;
|
||||
this.storageProgressEl.value = metrics.usedBytes;
|
||||
this.storageProgressEl.max = metrics.totalBytes;
|
||||
this.storageProgressText.textContent = `${usedStr} / ${totalStr}`;
|
||||
} catch (err) {
|
||||
console.error('Khoj: Failed to update storage display', err);
|
||||
this.storageProgressText.textContent = 'Estimation unavailable';
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshModelsAndServerPreference() {
|
||||
let serverSelectedModelId: string | null = null;
|
||||
if (this.plugin.settings.connectedToBackend) {
|
||||
@@ -439,19 +561,54 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper method to update the folder list display
|
||||
private updateFolderList(containerEl: HTMLElement) {
|
||||
// Helper method to update the include folder list display
|
||||
private updateIncludeFolderList(containerEl: HTMLElement) {
|
||||
this.updateFolderList(
|
||||
containerEl,
|
||||
this.plugin.settings.syncFolders,
|
||||
'Including entire vault',
|
||||
async (folder) => {
|
||||
this.plugin.settings.syncFolders = this.plugin.settings.syncFolders.filter(f => f !== folder);
|
||||
await this.plugin.saveSettings();
|
||||
this.updateIncludeFolderList(containerEl);
|
||||
this.refreshStorageDisplay();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method to update the exclude folder list display
|
||||
private updateExcludeFolderList(containerEl: HTMLElement) {
|
||||
this.updateFolderList(
|
||||
containerEl,
|
||||
this.plugin.settings.excludeFolders,
|
||||
'No folders excluded',
|
||||
async (folder) => {
|
||||
this.plugin.settings.excludeFolders = this.plugin.settings.excludeFolders.filter(f => f !== folder);
|
||||
await this.plugin.saveSettings();
|
||||
this.updateExcludeFolderList(containerEl);
|
||||
this.refreshStorageDisplay();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Shared helper to render a folder list with remove buttons
|
||||
private updateFolderList(
|
||||
containerEl: HTMLElement,
|
||||
folders: string[],
|
||||
emptyText: string,
|
||||
onRemove: (folder: string) => void
|
||||
) {
|
||||
containerEl.empty();
|
||||
if (this.plugin.settings.syncFolders.length === 0) {
|
||||
if (folders.length === 0) {
|
||||
containerEl.createEl('div', {
|
||||
text: 'Syncing entire vault',
|
||||
text: emptyText,
|
||||
cls: 'folder-list-empty'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const list = containerEl.createEl('ul', { cls: 'folder-list' });
|
||||
this.plugin.settings.syncFolders.forEach(folder => {
|
||||
folders.forEach(folder => {
|
||||
const item = list.createEl('li', { cls: 'folder-list-item' });
|
||||
item.createSpan({ text: folder });
|
||||
|
||||
@@ -459,11 +616,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
cls: 'folder-list-remove',
|
||||
text: '×'
|
||||
});
|
||||
removeButton.addEventListener('click', async () => {
|
||||
this.plugin.settings.syncFolders = this.plugin.settings.syncFolders.filter(f => f !== folder);
|
||||
await this.plugin.saveSettings();
|
||||
this.updateFolderList(containerEl);
|
||||
});
|
||||
removeButton.addEventListener('click', () => onRemove(folder));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkspaceLeaf, TFile, MarkdownRenderer, Notice, setIcon } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { KhojPaneView } from 'src/pane_view';
|
||||
import { KhojView, getLinkToEntry, supportedBinaryFileTypes } from 'src/utils';
|
||||
import Khoj from 'src/main';
|
||||
|
||||
export interface SimilarResult {
|
||||
entry: string;
|
||||
@@ -11,7 +11,6 @@ export interface SimilarResult {
|
||||
|
||||
export class KhojSimilarView extends KhojPaneView {
|
||||
static iconName: string = "search";
|
||||
setting: KhojSetting;
|
||||
currentController: AbortController | null = null;
|
||||
isLoading: boolean = false;
|
||||
loadingEl: HTMLElement;
|
||||
@@ -21,9 +20,8 @@ export class KhojSimilarView extends KhojPaneView {
|
||||
fileWatcher: any;
|
||||
component: any;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf, setting);
|
||||
this.setting = setting;
|
||||
constructor(leaf: WorkspaceLeaf, plugin: Khoj) {
|
||||
super(leaf, plugin);
|
||||
this.component = this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FileSystemAdapter, Notice, Vault, Modal, TFile, request, setIcon, Editor, App, WorkspaceLeaf } from 'obsidian';
|
||||
import { FileSystemAdapter, Notice, Vault, Modal, TFile, request, setIcon, Editor, WorkspaceLeaf } from 'obsidian';
|
||||
import { KhojSetting, ModelOption, ServerUserConfig, UserInfo } from 'src/settings'
|
||||
import { deleteContentByType, uploadContentBatch } from './api';
|
||||
import { KhojSearchModal } from './search_modal';
|
||||
|
||||
export function getVaultAbsolutePath(vault: Vault): string {
|
||||
@@ -60,9 +61,7 @@ export const supportedImageFilesTypes = fileTypeToExtension.image;
|
||||
export const supportedBinaryFileTypes = fileTypeToExtension.pdf.concat(supportedImageFilesTypes);
|
||||
export const supportedFileTypes = fileTypeToExtension.markdown.concat(supportedBinaryFileTypes);
|
||||
|
||||
export async function updateContentIndex(vault: Vault, setting: KhojSetting, lastSync: Map<TFile, number>, regenerate: boolean = false, userTriggered: boolean = false): Promise<Map<TFile, number>> {
|
||||
// Get all markdown, pdf files in the vault
|
||||
console.log(`Khoj: Updating Khoj content index...`)
|
||||
export function getFilesToSync(vault: Vault, setting: KhojSetting): TFile[] {
|
||||
const files = vault.getFiles()
|
||||
// Filter supported file types for syncing
|
||||
.filter(file => supportedFileTypes.includes(file.extension))
|
||||
@@ -73,7 +72,7 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
if (fileTypeToExtension.image.includes(file.extension)) return setting.syncFileType.images;
|
||||
return false;
|
||||
})
|
||||
// Filter files based on specified folders
|
||||
// Filter in included folders
|
||||
.filter(file => {
|
||||
// If no folders are specified, sync all files
|
||||
if (setting.syncFolders.length === 0) return true;
|
||||
@@ -81,17 +80,61 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
return setting.syncFolders.some(folder =>
|
||||
file.path.startsWith(folder + '/') || file.path === folder
|
||||
);
|
||||
})
|
||||
// Filter out excluded folders
|
||||
.filter(file => {
|
||||
// If no folders are excluded, include all files
|
||||
if (setting.excludeFolders.length === 0) return true;
|
||||
// Exclude files in any of the excluded folders
|
||||
return !setting.excludeFolders.some(folder =>
|
||||
file.path.startsWith(folder + '/') || file.path === folder
|
||||
);
|
||||
})
|
||||
// Sort files by type: markdown > pdf > image
|
||||
.sort((a, b) => {
|
||||
const typeOrder: (keyof typeof fileTypeToExtension)[] = ['markdown', 'pdf', 'image'];
|
||||
const aType = typeOrder.findIndex(type => fileTypeToExtension[type].includes(a.extension));
|
||||
const bType = typeOrder.findIndex(type => fileTypeToExtension[type].includes(b.extension));
|
||||
return aType - bType;
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function updateContentIndex(
|
||||
vault: Vault,
|
||||
setting: KhojSetting,
|
||||
lastSync: Map<TFile, number>,
|
||||
regenerate: boolean = false,
|
||||
userTriggered: boolean = false,
|
||||
onProgress?: (progress: { processed: number, total: number }) => void
|
||||
): Promise<Map<TFile, number>> {
|
||||
// Get all markdown, pdf files in the vault
|
||||
console.log(`Khoj: Updating Khoj content index...`);
|
||||
const files = getFilesToSync(vault, setting);
|
||||
console.log(`Khoj: Found ${files.length} eligible files in vault`);
|
||||
|
||||
let countOfFilesToIndex = 0;
|
||||
let countOfFilesToDelete = 0;
|
||||
lastSync = lastSync.size > 0 ? lastSync : new Map<TFile, number>();
|
||||
|
||||
// Add all files to index as multipart form data
|
||||
let fileData = [];
|
||||
let currentBatchSize = 0;
|
||||
// Count files that need indexing (modified since last sync or regenerating)
|
||||
const filesToSync = regenerate
|
||||
? files
|
||||
: files.filter(file => file.stat.mtime >= (lastSync.get(file) ?? 0));
|
||||
|
||||
// Show notice with file counts when user triggers sync
|
||||
if (userTriggered) {
|
||||
new Notice(`🔄 Syncing ${filesToSync.length} of ${files.length} files to Khoj...`);
|
||||
}
|
||||
console.log(`Khoj: ${filesToSync.length} files to sync (${files.length} total eligible)`);
|
||||
|
||||
// Add all files to index as multipart form data, batched by size, item count
|
||||
const MAX_BATCH_SIZE = 10 * 1024 * 1024; // 10MB max batch size
|
||||
let currentBatch = [];
|
||||
const MAX_BATCH_ITEMS = 50; // Max 50 items per batch
|
||||
let fileData: { blob: Blob, path: string }[][] = [];
|
||||
let currentBatch: { blob: Blob, path: string }[] = [];
|
||||
let currentBatchSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
// Only push files that have been modified since last sync if not regenerating
|
||||
@@ -105,9 +148,8 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
const fileContent = encoding == 'binary' ? await vault.readBinary(file) : await vault.read(file);
|
||||
const fileItem = { blob: new Blob([fileContent], { type: mimeType }), path: file.path };
|
||||
|
||||
// Check if adding this file would exceed batch size
|
||||
const fileSize = (typeof fileContent === 'string') ? new Blob([fileContent]).size : fileContent.byteLength;
|
||||
if (currentBatchSize + fileSize > MAX_BATCH_SIZE && currentBatch.length > 0) {
|
||||
if ((currentBatchSize + fileSize > MAX_BATCH_SIZE || currentBatch.length >= MAX_BATCH_ITEMS) && currentBatch.length > 0) {
|
||||
fileData.push(currentBatch);
|
||||
currentBatch = [];
|
||||
currentBatchSize = 0;
|
||||
@@ -117,12 +159,12 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
currentBatchSize += fileSize;
|
||||
}
|
||||
|
||||
// Add any previously synced files to be deleted to final batch
|
||||
// Add files to delete (previously synced but no longer in vault) to final batch
|
||||
let filesToDelete: TFile[] = [];
|
||||
for (const lastSyncedFile of lastSync.keys()) {
|
||||
if (!files.includes(lastSyncedFile)) {
|
||||
countOfFilesToDelete++;
|
||||
let fileObj = new Blob([""], { type: filenameToMimeType(lastSyncedFile) });
|
||||
const fileObj = new Blob([""], { type: filenameToMimeType(lastSyncedFile) });
|
||||
currentBatch.push({ blob: fileObj, path: lastSyncedFile.path });
|
||||
filesToDelete.push(lastSyncedFile);
|
||||
}
|
||||
@@ -134,86 +176,51 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
}
|
||||
|
||||
// Delete all files of enabled content types first if regenerating
|
||||
let error_message = null;
|
||||
const contentTypesToDelete = [];
|
||||
let error_message: string | null = null;
|
||||
if (regenerate) {
|
||||
// Mark content types to delete based on user sync file type settings
|
||||
const contentTypesToDelete: string[] = [];
|
||||
if (setting.syncFileType.markdown) contentTypesToDelete.push('markdown');
|
||||
if (setting.syncFileType.pdf) contentTypesToDelete.push('pdf');
|
||||
if (setting.syncFileType.images) contentTypesToDelete.push('image');
|
||||
}
|
||||
for (const contentType of contentTypesToDelete) {
|
||||
const response = await fetch(`${setting.khojUrl}/api/content/type/${contentType}?client=obsidian`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${setting.khojApiKey}`,
|
||||
|
||||
try {
|
||||
for (const contentType of contentTypesToDelete) {
|
||||
await deleteContentByType(setting.khojUrl, setting.khojApiKey, contentType);
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
} catch (err) {
|
||||
console.error('Khoj: Error deleting content types:', err);
|
||||
error_message = "❗️Failed to clear existing content index";
|
||||
fileData = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all indexable files in vault, 10Mb batch at a time
|
||||
// Upload files in batches
|
||||
let responses: string[] = [];
|
||||
let processedFiles = 0;
|
||||
const totalFiles = fileData.reduce((sum, batch) => sum + batch.length, 0);
|
||||
|
||||
// Report initial progress with total count before uploading
|
||||
if (onProgress) {
|
||||
onProgress({ processed: 0, total: totalFiles });
|
||||
}
|
||||
|
||||
for (const batch of fileData) {
|
||||
// Create multipart form data with all files in batch
|
||||
const formData = new FormData();
|
||||
batch.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
|
||||
|
||||
// Call Khoj backend to sync index with updated files in vault
|
||||
const method = regenerate ? "PUT" : "PATCH";
|
||||
const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${setting.khojApiKey}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
let response_text = await response.text();
|
||||
if (response_text.includes("Too much data")) {
|
||||
const errorFragment = document.createDocumentFragment();
|
||||
errorFragment.appendChild(document.createTextNode("❗️Exceeded data sync limits. To resolve this either:"));
|
||||
const bulletList = document.createElement('ul');
|
||||
|
||||
const limitFilesItem = document.createElement('li');
|
||||
const settingsPrefixText = document.createTextNode("Limit files to sync from ");
|
||||
const settingsLink = document.createElement('a');
|
||||
settingsLink.textContent = "Khoj settings";
|
||||
settingsLink.href = "#";
|
||||
settingsLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
openKhojPluginSettings();
|
||||
});
|
||||
limitFilesItem.appendChild(settingsPrefixText);
|
||||
limitFilesItem.appendChild(settingsLink);
|
||||
bulletList.appendChild(limitFilesItem);
|
||||
|
||||
const upgradeItem = document.createElement('li');
|
||||
const upgradeLink = document.createElement('a');
|
||||
upgradeLink.href = `${setting.khojUrl}/settings#subscription`;
|
||||
upgradeLink.textContent = 'Upgrade your subscription';
|
||||
upgradeLink.target = '_blank';
|
||||
upgradeItem.appendChild(upgradeLink);
|
||||
bulletList.appendChild(upgradeItem);
|
||||
errorFragment.appendChild(bulletList);
|
||||
error_message = errorFragment;
|
||||
} else {
|
||||
error_message = `❗️Failed to sync your content with Khoj server. Requests were throttled. Upgrade your subscription or try again later.`;
|
||||
}
|
||||
break;
|
||||
} else if (response.status === 404) {
|
||||
error_message = `❗️Could not connect to Khoj server. Ensure you can connect to it.`;
|
||||
break;
|
||||
} else {
|
||||
error_message = `❗️Failed to sync all your content with Khoj server. Raise issue on Khoj Discord or Github\nError: ${response.statusText}`;
|
||||
try {
|
||||
const resultText = await uploadContentBatch(setting.khojUrl, setting.khojApiKey, batch);
|
||||
responses.push(resultText);
|
||||
processedFiles += batch.length;
|
||||
if (onProgress) {
|
||||
onProgress({ processed: processedFiles, total: totalFiles });
|
||||
}
|
||||
} else {
|
||||
responses.push(await response.text());
|
||||
} catch (err: any) {
|
||||
console.error('Khoj: Failed to upload batch:', err);
|
||||
if (err.message?.includes('429')) {
|
||||
error_message = `❗️Requests were throttled. Upgrade your subscription or try again later.`;
|
||||
} else {
|
||||
error_message = `❗️Failed to sync content with Khoj server. Error: ${err.message ?? String(err)}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +240,9 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
if (error_message) {
|
||||
new Notice(error_message);
|
||||
} else {
|
||||
if (userTriggered) new Notice('✅ Updated Khoj index.');
|
||||
console.log(`✅ Refreshed Khoj content index. Updated: ${countOfFilesToIndex} files, Deleted: ${countOfFilesToDelete} files.`);
|
||||
const summary = `Updated ${countOfFilesToIndex}, deleted ${countOfFilesToDelete} files`;
|
||||
if (userTriggered) new Notice(`✅ ${summary}`);
|
||||
console.log(`✅ Refreshed Khoj content index. ${summary}.`);
|
||||
}
|
||||
|
||||
return lastSync;
|
||||
@@ -606,6 +614,44 @@ export function getLinkToEntry(sourceFiles: TFile[], chosenFile: string, chosenE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate estimated vault sync metrics (used and total bytes).
|
||||
* This is a client-side estimation based on the configured sync file types and folders.
|
||||
* The storage limit is determined from the backend-provided `setting.userInfo?.is_active` flag:
|
||||
* - if true => premium limit (500 MB)
|
||||
* - otherwise => free limit (10 MB)
|
||||
* This avoids client-side heuristics and relies on server-provided user info.
|
||||
*/
|
||||
export async function calculateVaultSyncMetrics(vault: Vault, setting: KhojSetting): Promise<{ usedBytes: number, totalBytes: number }> {
|
||||
try {
|
||||
const files = getFilesToSync(vault, setting);
|
||||
const usedBytes = files.reduce((acc, file) => acc + (file.stat?.size ?? 0), 0);
|
||||
|
||||
// Default to free plan limit
|
||||
const FREE_LIMIT = 10 * 1024 * 1024; // 10 MB
|
||||
const PAID_LIMIT = 500 * 1024 * 1024; // 500 MB
|
||||
let totalBytes = FREE_LIMIT;
|
||||
|
||||
// Determine plan from backend-provided user info. Use FREE_LIMIT as default when info missing.
|
||||
try {
|
||||
if (setting.userInfo && setting.userInfo.is_active === true) {
|
||||
totalBytes = PAID_LIMIT;
|
||||
} else {
|
||||
totalBytes = FREE_LIMIT;
|
||||
}
|
||||
} catch (err) {
|
||||
// Defensive: on any unexpected error, fall back to free limit
|
||||
console.warn('Khoj: Error reading userInfo.is_active, defaulting to free limit', err);
|
||||
totalBytes = FREE_LIMIT;
|
||||
}
|
||||
|
||||
return { usedBytes, totalBytes };
|
||||
} catch (err) {
|
||||
console.error('Khoj: Error calculating vault sync metrics:', err);
|
||||
return { usedBytes: 0, totalBytes: 10 * 1024 * 1024 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChatModels(settings: KhojSetting): Promise<ModelOption[]> {
|
||||
if (!settings.connectedToBackend || !settings.khojUrl) {
|
||||
return [];
|
||||
|
||||
@@ -1300,6 +1300,11 @@ img.copy-icon {
|
||||
}
|
||||
}
|
||||
|
||||
.khoj-file-suggestion {
|
||||
padding: 8px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.khoj-similar-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
@@ -136,5 +136,33 @@
|
||||
"1.42.5": "0.15.0",
|
||||
"1.42.6": "0.15.0",
|
||||
"1.42.7": "0.15.0",
|
||||
"1.42.8": "0.15.0"
|
||||
"1.42.8": "0.15.0",
|
||||
"2.0.0-beta.1": "0.15.0",
|
||||
"2.0.0-beta.2": "0.15.0",
|
||||
"2.0.0-beta.3": "0.15.0",
|
||||
"2.0.0-beta.4": "0.15.0",
|
||||
"2.0.0-beta.5": "0.15.0",
|
||||
"2.0.0-beta.6": "0.15.0",
|
||||
"2.0.0-beta.7": "0.15.0",
|
||||
"2.0.0-beta.8": "0.15.0",
|
||||
"2.0.0-beta.9": "0.15.0",
|
||||
"2.0.0-beta.10": "0.15.0",
|
||||
"2.0.0-beta.11": "0.15.0",
|
||||
"2.0.0-beta.12": "0.15.0",
|
||||
"2.0.0-beta.13": "0.15.0",
|
||||
"2.0.0-beta.14": "0.15.0",
|
||||
"2.0.0-beta.15": "0.15.0",
|
||||
"2.0.0-beta.16": "0.15.0",
|
||||
"2.0.0-beta.17": "0.15.0",
|
||||
"2.0.0-beta.18": "0.15.0",
|
||||
"2.0.0-beta.19": "0.15.0",
|
||||
"2.0.0-beta.20": "0.15.0",
|
||||
"2.0.0-beta.21": "0.15.0",
|
||||
"2.0.0-beta.22": "0.15.0",
|
||||
"2.0.0-beta.23": "0.15.0",
|
||||
"2.0.0-beta.24": "0.15.0",
|
||||
"2.0.0-beta.25": "0.15.0",
|
||||
"2.0.0-beta.26": "0.15.0",
|
||||
"2.0.0-beta.27": "0.15.0",
|
||||
"2.0.0-beta.28": "0.15.0"
|
||||
}
|
||||
|
||||
@@ -2,56 +2,94 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@asamuzakjp/css-color@^3.1.2":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794"
|
||||
integrity sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==
|
||||
"@acemir/cssom@^0.9.31":
|
||||
version "0.9.31"
|
||||
resolved "https://registry.yarnpkg.com/@acemir/cssom/-/cssom-0.9.31.tgz#bd5337d290fb8be2ac18391f37386bc53778b0bc"
|
||||
integrity sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==
|
||||
|
||||
"@asamuzakjp/css-color@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-5.0.1.tgz#3b9462a9b52f3c6680a0945a3d0851881017550f"
|
||||
integrity sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==
|
||||
dependencies:
|
||||
"@csstools/css-calc" "^2.1.3"
|
||||
"@csstools/css-color-parser" "^3.0.9"
|
||||
"@csstools/css-parser-algorithms" "^3.0.4"
|
||||
"@csstools/css-tokenizer" "^3.0.3"
|
||||
lru-cache "^10.4.3"
|
||||
"@csstools/css-calc" "^3.1.1"
|
||||
"@csstools/css-color-parser" "^4.0.2"
|
||||
"@csstools/css-parser-algorithms" "^4.0.0"
|
||||
"@csstools/css-tokenizer" "^4.0.0"
|
||||
lru-cache "^11.2.6"
|
||||
|
||||
"@csstools/color-helpers@^5.0.2":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8"
|
||||
integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==
|
||||
|
||||
"@csstools/css-calc@^2.1.3":
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.3.tgz#6f68affcb569a86b91965e8622d644be35a08423"
|
||||
integrity sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==
|
||||
|
||||
"@csstools/css-color-parser@^3.0.9":
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz#8d81b77d6f211495b5100ec4cad4c8828de49f6b"
|
||||
integrity sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==
|
||||
"@asamuzakjp/dom-selector@^6.8.1":
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz#39b20993672b106f7cd9a3a9a465212e87e0bfd1"
|
||||
integrity sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==
|
||||
dependencies:
|
||||
"@csstools/color-helpers" "^5.0.2"
|
||||
"@csstools/css-calc" "^2.1.3"
|
||||
"@asamuzakjp/nwsapi" "^2.3.9"
|
||||
bidi-js "^1.0.3"
|
||||
css-tree "^3.1.0"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
lru-cache "^11.2.6"
|
||||
|
||||
"@csstools/css-parser-algorithms@^3.0.4":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356"
|
||||
integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==
|
||||
"@asamuzakjp/nwsapi@^2.3.9":
|
||||
version "2.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz#ad5549322dfe9d153d4b4dd6f7ff2ae234b06e24"
|
||||
integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==
|
||||
|
||||
"@csstools/css-tokenizer@^3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2"
|
||||
integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==
|
||||
"@bramus/specificity@^2.4.2":
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@bramus/specificity/-/specificity-2.4.2.tgz#aa8db8eb173fdee7324f82284833106adeecc648"
|
||||
integrity sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==
|
||||
dependencies:
|
||||
css-tree "^3.0.0"
|
||||
|
||||
"@csstools/color-helpers@^6.0.2":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz#82c59fd30649cf0b4d3c82160489748666e6550b"
|
||||
integrity sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==
|
||||
|
||||
"@csstools/css-calc@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-3.1.1.tgz#78b494996dac41a02797dcca18ac3b46d25b3fd7"
|
||||
integrity sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==
|
||||
|
||||
"@csstools/css-color-parser@^4.0.2":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz#c27e03a3770d0352db92d668d6dde427a37859e5"
|
||||
integrity sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==
|
||||
dependencies:
|
||||
"@csstools/color-helpers" "^6.0.2"
|
||||
"@csstools/css-calc" "^3.1.1"
|
||||
|
||||
"@csstools/css-parser-algorithms@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz#e1c65dc09378b42f26a111fca7f7075fc2c26164"
|
||||
integrity sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==
|
||||
|
||||
"@csstools/css-syntax-patches-for-csstree@^1.0.28":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz#ce4c9a0cbe30590491fcd5c03fe6426d22ba89e4"
|
||||
integrity sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==
|
||||
|
||||
"@csstools/css-tokenizer@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz#798a33950d11226a0ebb6acafa60f5594424967f"
|
||||
integrity sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
|
||||
integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.10.0":
|
||||
version "4.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
|
||||
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
|
||||
version "4.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
||||
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
||||
|
||||
"@exodus/bytes@^1.11.0", "@exodus/bytes@^1.6.0":
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@exodus/bytes/-/bytes-1.15.0.tgz#54479e0f406cbad024d6fe1c3190ecca4468df3b"
|
||||
integrity sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -89,9 +127,9 @@
|
||||
dompurify "*"
|
||||
|
||||
"@types/estree@*":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
|
||||
"@types/node@^16.11.6":
|
||||
version "16.18.126"
|
||||
@@ -192,9 +230,9 @@
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
agent-base@^7.1.0, agent-base@^7.1.2:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1"
|
||||
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
|
||||
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
@@ -206,10 +244,17 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
bidi-js@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2"
|
||||
integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==
|
||||
dependencies:
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
brace-expansion@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
|
||||
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
@@ -225,38 +270,48 @@ builtin-modules@3.3.0:
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
|
||||
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
|
||||
|
||||
cssstyle@^4.2.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.3.1.tgz#68a3c9f5a70aa97d5a6ebecc9805e511fc022eb8"
|
||||
integrity sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==
|
||||
css-tree@^3.0.0, css-tree@^3.1.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.2.1.tgz#86cac7011561272b30e6b1e042ba6ce047aa7518"
|
||||
integrity sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==
|
||||
dependencies:
|
||||
"@asamuzakjp/css-color" "^3.1.2"
|
||||
rrweb-cssom "^0.8.0"
|
||||
mdn-data "2.27.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
data-urls@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
|
||||
integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==
|
||||
cssstyle@^6.0.1:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-6.2.0.tgz#c41b59955c19c7a1223352d67ca462750204ad0f"
|
||||
integrity sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==
|
||||
dependencies:
|
||||
whatwg-mimetype "^4.0.0"
|
||||
whatwg-url "^14.0.0"
|
||||
"@asamuzakjp/css-color" "^5.0.1"
|
||||
"@csstools/css-syntax-patches-for-csstree" "^1.0.28"
|
||||
css-tree "^3.1.0"
|
||||
lru-cache "^11.2.6"
|
||||
|
||||
data-urls@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-7.0.0.tgz#6dce8b63226a1ecfdd907ce18a8ccfb1eee506d3"
|
||||
integrity sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==
|
||||
dependencies:
|
||||
whatwg-mimetype "^5.0.0"
|
||||
whatwg-url "^16.0.0"
|
||||
|
||||
debug@4, debug@^4.3.4:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decimal.js@^10.5.0:
|
||||
version "10.5.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22"
|
||||
integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==
|
||||
decimal.js@^10.6.0:
|
||||
version "10.6.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
|
||||
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
|
||||
|
||||
diff@^8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.2.tgz#712156a6dd288e66ebb986864e190c2fc9eddfae"
|
||||
integrity sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==
|
||||
diff@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.3.tgz#c7da3d9e0e8c283bb548681f8d7174653720c2d5"
|
||||
integrity sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
@@ -265,17 +320,17 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
dompurify@*, dompurify@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad"
|
||||
integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
|
||||
dompurify@*, dompurify@^3.3.1:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
|
||||
integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
entities@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.0.tgz#09c9e29cb79b0a6459a9b9db9efb418ac5bb8e51"
|
||||
integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
|
||||
integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
|
||||
|
||||
esbuild-android-64@0.14.47:
|
||||
version "0.14.47"
|
||||
@@ -420,9 +475,9 @@ fast-glob@^3.2.9:
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
|
||||
integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675"
|
||||
integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -457,12 +512,12 @@ graphemer@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
html-encoding-sniffer@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
|
||||
integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==
|
||||
html-encoding-sniffer@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz#f8d9390b3b348b50d4f61c16dd2ef5c05980a882"
|
||||
integrity sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==
|
||||
dependencies:
|
||||
whatwg-encoding "^3.1.1"
|
||||
"@exodus/bytes" "^1.6.0"
|
||||
|
||||
http-proxy-agent@^7.0.2:
|
||||
version "7.0.2"
|
||||
@@ -480,13 +535,6 @@ https-proxy-agent@^7.0.6:
|
||||
agent-base "^7.1.2"
|
||||
debug "4"
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ignore@^5.2.0, ignore@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
@@ -515,43 +563,49 @@ is-potential-custom-element-name@^1.0.1:
|
||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||
|
||||
isomorphic-dompurify@^2.25.0:
|
||||
version "2.25.0"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz#063e3ea7399bc1146783a9527be6c10baa25dc15"
|
||||
integrity sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==
|
||||
version "2.36.0"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.36.0.tgz#91e0e554cc3130cc4b7bfb77264f58e51ebd561e"
|
||||
integrity sha512-E8YkGyPY3a/U5s0WOoc8Ok+3SWL/33yn2IHCoxCFLBUUPVy9WGa++akJZFxQCcJIhI+UvYhbrbnTIFQkHKZbgA==
|
||||
dependencies:
|
||||
dompurify "^3.2.6"
|
||||
jsdom "^26.1.0"
|
||||
dompurify "^3.3.1"
|
||||
jsdom "^28.0.0"
|
||||
|
||||
jsdom@^26.1.0:
|
||||
version "26.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-26.1.0.tgz#ab5f1c1cafc04bd878725490974ea5e8bf0c72b3"
|
||||
integrity sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==
|
||||
jsdom@^28.0.0:
|
||||
version "28.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-28.1.0.tgz#ac4203e58fd24d7b0f34359ab00d6d9caebd4b62"
|
||||
integrity sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==
|
||||
dependencies:
|
||||
cssstyle "^4.2.1"
|
||||
data-urls "^5.0.0"
|
||||
decimal.js "^10.5.0"
|
||||
html-encoding-sniffer "^4.0.0"
|
||||
"@acemir/cssom" "^0.9.31"
|
||||
"@asamuzakjp/dom-selector" "^6.8.1"
|
||||
"@bramus/specificity" "^2.4.2"
|
||||
"@exodus/bytes" "^1.11.0"
|
||||
cssstyle "^6.0.1"
|
||||
data-urls "^7.0.0"
|
||||
decimal.js "^10.6.0"
|
||||
html-encoding-sniffer "^6.0.0"
|
||||
http-proxy-agent "^7.0.2"
|
||||
https-proxy-agent "^7.0.6"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
nwsapi "^2.2.16"
|
||||
parse5 "^7.2.1"
|
||||
rrweb-cssom "^0.8.0"
|
||||
parse5 "^8.0.0"
|
||||
saxes "^6.0.0"
|
||||
symbol-tree "^3.2.4"
|
||||
tough-cookie "^5.1.1"
|
||||
tough-cookie "^6.0.0"
|
||||
undici "^7.21.0"
|
||||
w3c-xmlserializer "^5.0.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
whatwg-encoding "^3.1.1"
|
||||
whatwg-mimetype "^4.0.0"
|
||||
whatwg-url "^14.1.1"
|
||||
ws "^8.18.0"
|
||||
webidl-conversions "^8.0.1"
|
||||
whatwg-mimetype "^5.0.0"
|
||||
whatwg-url "^16.0.0"
|
||||
xml-name-validator "^5.0.0"
|
||||
|
||||
lru-cache@^10.4.3:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
lru-cache@^11.2.6:
|
||||
version "11.2.7"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.7.tgz#9127402617f34cd6767b96daee98c28e74458d35"
|
||||
integrity sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==
|
||||
|
||||
mdn-data@2.27.1:
|
||||
version "2.27.1"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.27.1.tgz#e37b9c50880b75366c4d40ac63d9bbcacdb61f0e"
|
||||
integrity sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==
|
||||
|
||||
merge2@^1.3.0, merge2@^1.4.1:
|
||||
version "1.4.1"
|
||||
@@ -567,11 +621,11 @@ micromatch@^4.0.8:
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
version "9.0.9"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
|
||||
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
brace-expansion "^2.0.2"
|
||||
|
||||
moment@2.29.4:
|
||||
version "2.29.4"
|
||||
@@ -588,23 +642,18 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
nwsapi@^2.2.16:
|
||||
version "2.2.20"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef"
|
||||
integrity sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==
|
||||
|
||||
obsidian@^1.6.6:
|
||||
version "1.8.7"
|
||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.8.7.tgz#601e9ea1724289effa4c9bb3b4e20d327263634f"
|
||||
integrity sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==
|
||||
version "1.12.3"
|
||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.12.3.tgz#5307fe4c36d6b3d554fd0d4e4732f756a7e1d1cd"
|
||||
integrity sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==
|
||||
dependencies:
|
||||
"@types/codemirror" "5.60.8"
|
||||
moment "2.29.4"
|
||||
|
||||
parse5@^7.2.1:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
|
||||
integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
|
||||
parse5@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-8.0.0.tgz#aceb267f6b15f9b6e6ba9e35bfdd481fc2167b12"
|
||||
integrity sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==
|
||||
dependencies:
|
||||
entities "^6.0.0"
|
||||
|
||||
@@ -613,10 +662,10 @@ path-type@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
picomatch@>=2.3.2, picomatch@^2.3.1:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
punycode@^2.3.1:
|
||||
version "2.3.1"
|
||||
@@ -628,16 +677,16 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
|
||||
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
|
||||
|
||||
rrweb-cssom@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2"
|
||||
integrity sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
@@ -645,11 +694,6 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
saxes@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
|
||||
@@ -658,31 +702,36 @@ saxes@^6.0.0:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
semver@^7.6.0:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
version "7.7.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
tldts-core@^6.1.86:
|
||||
version "6.1.86"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8"
|
||||
integrity sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==
|
||||
tldts-core@^7.0.26:
|
||||
version "7.0.26"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.26.tgz#070f14bc7a4deabf115c6501bc5c0bae4da74d17"
|
||||
integrity sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==
|
||||
|
||||
tldts@^6.1.32:
|
||||
version "6.1.86"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.86.tgz#087e0555b31b9725ee48ca7e77edc56115cd82f7"
|
||||
integrity sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==
|
||||
tldts@^7.0.5:
|
||||
version "7.0.26"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.0.26.tgz#bf2472ed84e55faaaff5c2424c03a6bab69b92c5"
|
||||
integrity sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==
|
||||
dependencies:
|
||||
tldts-core "^6.1.86"
|
||||
tldts-core "^7.0.26"
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
@@ -691,17 +740,17 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tough-cookie@^5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7"
|
||||
integrity sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==
|
||||
tough-cookie@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.1.tgz#a495f833836609ed983c19bc65639cfbceb54c76"
|
||||
integrity sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==
|
||||
dependencies:
|
||||
tldts "^6.1.32"
|
||||
tldts "^7.0.5"
|
||||
|
||||
tr46@^5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca"
|
||||
integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
|
||||
tr46@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-6.0.0.tgz#f5a1ae546a0adb32a277a2278d0d17fa2f9093e6"
|
||||
integrity sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==
|
||||
dependencies:
|
||||
punycode "^2.3.1"
|
||||
|
||||
@@ -720,6 +769,11 @@ typescript@4.7.4:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
|
||||
undici@^7.21.0:
|
||||
version "7.24.4"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-7.24.4.tgz#873bce680d7c6354c941399fd4e8ea4563de4ea7"
|
||||
integrity sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==
|
||||
|
||||
w3c-xmlserializer@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
|
||||
@@ -727,35 +781,24 @@ w3c-xmlserializer@^5.0.0:
|
||||
dependencies:
|
||||
xml-name-validator "^5.0.0"
|
||||
|
||||
webidl-conversions@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
|
||||
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
|
||||
webidl-conversions@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz#0657e571fe6f06fcb15ca50ed1fdbcb495cd1686"
|
||||
integrity sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==
|
||||
|
||||
whatwg-encoding@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
|
||||
integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
|
||||
whatwg-mimetype@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz#d8232895dbd527ceaee74efd4162008fb8a8cf48"
|
||||
integrity sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==
|
||||
|
||||
whatwg-url@^16.0.0:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-16.0.1.tgz#047f7f4bd36ef76b7198c172d1b1cebc66f764dd"
|
||||
integrity sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-mimetype@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
|
||||
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
|
||||
|
||||
whatwg-url@^14.0.0, whatwg-url@^14.1.1:
|
||||
version "14.2.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663"
|
||||
integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
|
||||
dependencies:
|
||||
tr46 "^5.1.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
ws@^8.18.0:
|
||||
version "8.18.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
|
||||
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
|
||||
"@exodus/bytes" "^1.11.0"
|
||||
tr46 "^6.0.0"
|
||||
webidl-conversions "^8.0.1"
|
||||
|
||||
xml-name-validator@^5.0.0:
|
||||
version "5.0.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn run lint-staged
|
||||
yarn test
|
||||
bun run lint-staged
|
||||
bun run test
|
||||
|
||||
@@ -5,19 +5,19 @@ This is a [Next.js](https://nextjs.org/) project.
|
||||
First, install the dependencies:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
bun install
|
||||
```
|
||||
|
||||
In case you run into any dependency linking issues, you can try running:
|
||||
|
||||
```bash
|
||||
yarn add next
|
||||
bun add next
|
||||
```
|
||||
|
||||
### Run the development server:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
bun dev
|
||||
```
|
||||
|
||||
Make sure the `rewrites` in `next.config.mjs` are set up correctly for your environment. The rewrites are used to proxy requests to the API server.
|
||||
@@ -44,27 +44,30 @@ You can start editing the page by modifying any of the `.tsx` pages. The page au
|
||||
We've setup a utility command for building and serving the built files. This is useful for testing the production build locally.
|
||||
|
||||
1. Exporting code
|
||||
To build the files once and serve them, run:
|
||||
To build the files once and serve them, run:
|
||||
|
||||
```bash
|
||||
yarn export
|
||||
bun export
|
||||
```
|
||||
|
||||
If you're using Windows:
|
||||
```bash
|
||||
yarn windowsexport
|
||||
```
|
||||
|
||||
```bash
|
||||
bun windowsexport
|
||||
```
|
||||
|
||||
2. Continuously building code
|
||||
|
||||
To keep building the files and serving them, run:
|
||||
|
||||
```bash
|
||||
yarn watch
|
||||
bun watch
|
||||
```
|
||||
|
||||
If you're using Windows:
|
||||
|
||||
```bash
|
||||
yarn windowswatch
|
||||
bun windowswatch
|
||||
```
|
||||
|
||||
Now you should be able to load your custom pages from the Khoj app at http://localhost:42110/. To server any of the built files, you should update the routes in the `web_client.py` like so, where `new_file` is the new page you've added in this repo:
|
||||
|
||||
@@ -283,9 +283,9 @@ export default function Agents() {
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h2 className="text-lg">Agents</h2>
|
||||
)}
|
||||
@@ -344,14 +344,16 @@ export default function Agents() {
|
||||
/>
|
||||
<span className="font-bold">How it works</span> Use any of these
|
||||
specialized personas to tune your conversation to your needs.
|
||||
{
|
||||
!isSubscribed && (
|
||||
<span>
|
||||
{" "}
|
||||
<Link href="/settings" className="font-bold">Upgrade your plan</Link> to leverage custom models. You will fallback to the default model when chatting.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{!isSubscribed && (
|
||||
<span>
|
||||
{" "}
|
||||
<Link href="/settings" className="font-bold">
|
||||
Upgrade your plan
|
||||
</Link>{" "}
|
||||
to leverage custom models. You will fallback to the
|
||||
default model when chatting.
|
||||
</span>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="pt-6 md:pt-8">
|
||||
|
||||
@@ -46,6 +46,7 @@ import { LocationData, useIPLocationData, useIsMobileWidth } from "../common/uti
|
||||
import styles from "./automations.module.css";
|
||||
import ShareLink from "../components/shareLink/shareLink";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||
import {
|
||||
CalendarCheck,
|
||||
@@ -1059,9 +1060,9 @@ export default function Automations() {
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h2 className="text-lg">Automations</h2>
|
||||
)}
|
||||
|
||||
@@ -124,3 +124,50 @@ div.chatTitleWrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print-specific styles for chat layout */
|
||||
@media print {
|
||||
/* Chat container adjustments */
|
||||
div.main {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
div.chatBox,
|
||||
div.chatBoxBody,
|
||||
div.chatLayout {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: visible !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
div.chatBodyFull,
|
||||
div.chatBody {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
grid-template-columns: none !important;
|
||||
overflow: visible !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
div.inputBox {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Make chat content use full width in print */
|
||||
.chatHistory {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import "../globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Chat",
|
||||
@@ -39,6 +40,7 @@ export default function ChildLayout({
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Toaster />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import styles from "./chat.module.css";
|
||||
import React, { Suspense, useEffect, useRef, useState } from "react";
|
||||
import React, { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
|
||||
import ChatHistory from "../components/chatHistory/chatHistory";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Loading from "../components/loading/loading";
|
||||
|
||||
import { generateNewTitle, processMessageChunk } from "../common/chatFunctions";
|
||||
@@ -27,11 +29,13 @@ import { useAuthenticatedData } from "../common/auth";
|
||||
import { AgentData } from "@/app/components/agentCard/agentCard";
|
||||
import { ChatSessionActionMenu } from "../components/allConversations/allConversations";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { DeprecationBanner } from "@/app/components/deprecationBanner";
|
||||
import { AppSidebar } from "../components/appSidebar/appSidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { KhojLogoType } from "../components/logo/khojLogo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Joystick } from "@phosphor-icons/react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { ChatSidebar } from "../components/chatSidebar/chatSidebar";
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
@@ -45,7 +49,7 @@ interface ChatBodyDataProps {
|
||||
isMobileWidth?: boolean;
|
||||
isLoggedIn: boolean;
|
||||
setImages: (images: string[]) => void;
|
||||
setTriggeredAbort: (triggeredAbort: boolean) => void;
|
||||
setTriggeredAbort: (triggeredAbort: boolean, newMessage?: string) => void;
|
||||
isChatSideBarOpen: boolean;
|
||||
setIsChatSideBarOpen: (open: boolean) => void;
|
||||
isActive?: boolean;
|
||||
@@ -162,7 +166,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto mt-auto`}
|
||||
className={`${styles.inputBox} print-hidden p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto mt-auto`}
|
||||
>
|
||||
<ChatInputArea
|
||||
agentColor={agentMetadata?.color}
|
||||
@@ -180,13 +184,15 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ChatSidebar
|
||||
conversationId={conversationId}
|
||||
isActive={props.isActive}
|
||||
isOpen={props.isChatSideBarOpen}
|
||||
onOpenChange={props.setIsChatSideBarOpen}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
/>
|
||||
<div className="print-hidden">
|
||||
<ChatSidebar
|
||||
conversationId={conversationId}
|
||||
isActive={props.isActive}
|
||||
isOpen={props.isChatSideBarOpen}
|
||||
onOpenChange={props.setIsChatSideBarOpen}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -203,10 +209,10 @@ export default function Chat() {
|
||||
const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | undefined>(undefined);
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
|
||||
const [abortMessageStreamController, setAbortMessageStreamController] =
|
||||
useState<AbortController | null>(null);
|
||||
const [triggeredAbort, setTriggeredAbort] = useState(false);
|
||||
const [shouldSendWithInterrupt, setShouldSendWithInterrupt] = useState(false);
|
||||
const [interruptMessage, setInterruptMessage] = useState<string>("");
|
||||
const bufferRef = useRef("");
|
||||
const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const { locationData, locationDataError, locationDataLoading } = useIPLocationData() || {
|
||||
locationData: {
|
||||
@@ -220,6 +226,189 @@ export default function Chat() {
|
||||
} = useAuthenticatedData();
|
||||
const isMobileWidth = useIsMobileWidth();
|
||||
const [isChatSideBarOpen, setIsChatSideBarOpen] = useState(false);
|
||||
const [socketUrl, setSocketUrl] = useState<string | null>(null);
|
||||
// track whether we've already shown a toast for the current disconnect cycle to avoid duplicates
|
||||
const disconnectToastShownRef = useRef(false);
|
||||
// Track whether the websocket is closing due to an intentional action (page refresh/navigation or idle timeout)
|
||||
const intentionalCloseRef = useRef(false);
|
||||
|
||||
const disconnectFromServer = useCallback(() => {
|
||||
if (idleTimerRef.current) {
|
||||
clearTimeout(idleTimerRef.current);
|
||||
}
|
||||
// Mark as intentional so onClose does not show transient network error banner
|
||||
intentionalCloseRef.current = true;
|
||||
setSocketUrl(null);
|
||||
console.log("WebSocket disconnected due to inactivity.");
|
||||
}, []);
|
||||
|
||||
const resetIdleTimer = useCallback(() => {
|
||||
const idleTimeout = 10 * 60 * 1000; // 10 minutes
|
||||
if (idleTimerRef.current) {
|
||||
clearTimeout(idleTimerRef.current);
|
||||
}
|
||||
idleTimerRef.current = setTimeout(disconnectFromServer, idleTimeout);
|
||||
}, [disconnectFromServer]);
|
||||
|
||||
const { toast } = useToast();
|
||||
const { sendMessage, lastMessage } = useWebSocket(socketUrl, {
|
||||
share: true,
|
||||
shouldReconnect: (closeEvent) => true,
|
||||
reconnectAttempts: 10,
|
||||
// reconnect using exponential backoff with jitter
|
||||
reconnectInterval: (attemptNumber) => {
|
||||
const baseDelay = 1000 * Math.pow(2, attemptNumber);
|
||||
const jitter = Math.random() * 1000; // Add jitter up to 1s
|
||||
return Math.min(baseDelay + jitter, 20000); // Cap backoff at 20s
|
||||
},
|
||||
onOpen: () => {
|
||||
console.log("WebSocket connection established.");
|
||||
resetIdleTimer();
|
||||
// Reset disconnect toast guard so future disconnects can notify again
|
||||
disconnectToastShownRef.current = false;
|
||||
// Reset intentional close flag after a successful open
|
||||
intentionalCloseRef.current = false;
|
||||
},
|
||||
onClose: (event) => {
|
||||
console.log("WebSocket connection closed.");
|
||||
if (idleTimerRef.current) {
|
||||
clearTimeout(idleTimerRef.current);
|
||||
}
|
||||
// Suppress notice if:
|
||||
// - Intentional close (page refresh/navigation or idle management)
|
||||
// - Normal closure (1000) or Going Away (1001 - typical on page reload)
|
||||
// - No query to process
|
||||
if (
|
||||
!intentionalCloseRef.current &&
|
||||
event?.code !== 1000 &&
|
||||
event?.code !== 1001 &&
|
||||
queryToProcess
|
||||
) {
|
||||
if (!disconnectToastShownRef.current) {
|
||||
toast({
|
||||
title: "Network issue",
|
||||
description:
|
||||
"Connection lost. Please check your network and try again when ready.",
|
||||
variant: "destructive",
|
||||
duration: 6000,
|
||||
});
|
||||
disconnectToastShownRef.current = true;
|
||||
}
|
||||
}
|
||||
// Mark any in-progress streamed message as completed so UI updates (stop spinner, show send icon)
|
||||
setMessages((prev) => {
|
||||
if (!prev || prev.length === 0) return prev;
|
||||
const newMessages = [...prev];
|
||||
const last = newMessages[newMessages.length - 1];
|
||||
if (last && !last.completed) {
|
||||
last.completed = true;
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
// Reset processing state so ChatInputArea send button reappears
|
||||
setProcessQuerySignal(false);
|
||||
setQueryToProcess("");
|
||||
},
|
||||
onError: (event) => {
|
||||
console.error("WebSocket error", event);
|
||||
// Perform same cleanup as onClose to avoid stuck UI
|
||||
setMessages((prev) => {
|
||||
if (!prev || prev.length === 0) return prev;
|
||||
const newMessages = [...prev];
|
||||
const last = newMessages[newMessages.length - 1];
|
||||
if (last && !last.completed) {
|
||||
last.completed = true;
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
setProcessQuerySignal(false);
|
||||
setQueryToProcess("");
|
||||
if (!intentionalCloseRef.current && !disconnectToastShownRef.current) {
|
||||
toast({
|
||||
title: "Network error",
|
||||
description:
|
||||
"Connection lost. Please check your network and try again when ready.",
|
||||
variant: "destructive",
|
||||
duration: 5000,
|
||||
});
|
||||
disconnectToastShownRef.current = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Handle page unload / refresh: mark intentional so we don't show a toast
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = () => {
|
||||
intentionalCloseRef.current = true;
|
||||
};
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage !== null) {
|
||||
resetIdleTimer();
|
||||
// Check if this is a control message (JSON) rather than a streaming event
|
||||
try {
|
||||
const controlMessage = JSON.parse(lastMessage.data);
|
||||
if (controlMessage.type === "interrupt_acknowledged") {
|
||||
console.log("Interrupt acknowledged by server");
|
||||
setProcessQuerySignal(false);
|
||||
return;
|
||||
} else if (controlMessage.type === "interrupt_message_acknowledged") {
|
||||
console.log("Interrupt message acknowledged by server");
|
||||
setProcessQuerySignal(false);
|
||||
return;
|
||||
} else if (controlMessage.error) {
|
||||
console.error("WebSocket error:", controlMessage.error);
|
||||
setProcessQuerySignal(false);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Not a JSON control message, process as streaming event
|
||||
}
|
||||
|
||||
const eventDelimiter = "␃🔚␗";
|
||||
bufferRef.current += lastMessage.data;
|
||||
|
||||
let newEventIndex;
|
||||
while ((newEventIndex = bufferRef.current.indexOf(eventDelimiter)) !== -1) {
|
||||
const eventChunk = bufferRef.current.slice(0, newEventIndex);
|
||||
bufferRef.current = bufferRef.current.slice(newEventIndex + eventDelimiter.length);
|
||||
if (eventChunk) {
|
||||
setMessages((prevMessages) => {
|
||||
const newMessages = [...prevMessages];
|
||||
const currentMessage = newMessages[newMessages.length - 1];
|
||||
if (!currentMessage || currentMessage.completed) {
|
||||
return prevMessages;
|
||||
}
|
||||
|
||||
const { context, onlineContext, codeContext } = processMessageChunk(
|
||||
eventChunk,
|
||||
currentMessage,
|
||||
currentMessage.context || [],
|
||||
currentMessage.onlineContext || {},
|
||||
currentMessage.codeContext || {},
|
||||
);
|
||||
|
||||
// Update the current message with the new reference data
|
||||
currentMessage.context = context;
|
||||
currentMessage.onlineContext = onlineContext;
|
||||
currentMessage.codeContext = codeContext;
|
||||
|
||||
if (currentMessage.completed) {
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
setImages([]);
|
||||
if (conversationId) generateNewTitle(conversationId, setTitle);
|
||||
}
|
||||
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [lastMessage, setMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/chat/options")
|
||||
@@ -239,14 +428,37 @@ export default function Chat() {
|
||||
welcomeConsole();
|
||||
}, []);
|
||||
|
||||
const handleTriggeredAbort = (value: boolean, newMessage?: string) => {
|
||||
if (value) {
|
||||
setInterruptMessage(newMessage || "");
|
||||
}
|
||||
setTriggeredAbort(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (triggeredAbort) {
|
||||
abortMessageStreamController?.abort();
|
||||
handleAbortedMessage();
|
||||
setShouldSendWithInterrupt(true);
|
||||
setTriggeredAbort(false);
|
||||
sendMessage(
|
||||
JSON.stringify({
|
||||
type: "interrupt",
|
||||
query: interruptMessage,
|
||||
}),
|
||||
);
|
||||
console.log("Sent interrupt message via WebSocket:", interruptMessage);
|
||||
|
||||
// Mark the last message as completed
|
||||
setMessages((prevMessages) => {
|
||||
const newMessages = [...prevMessages];
|
||||
const currentMessage = newMessages[newMessages.length - 1];
|
||||
if (currentMessage) currentMessage.completed = true;
|
||||
return newMessages;
|
||||
});
|
||||
|
||||
// Set the interrupt message as the new query being processed
|
||||
setQueryToProcess(interruptMessage);
|
||||
setTriggeredAbort(false); // Always set to false after processing
|
||||
setInterruptMessage("");
|
||||
}
|
||||
}, [triggeredAbort]);
|
||||
}, [triggeredAbort, sendMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryToProcess) {
|
||||
@@ -264,7 +476,6 @@ export default function Chat() {
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
|
||||
setProcessQuerySignal(true);
|
||||
setAbortMessageStreamController(new AbortController());
|
||||
}
|
||||
}, [queryToProcess]);
|
||||
|
||||
@@ -278,70 +489,19 @@ export default function Chat() {
|
||||
}
|
||||
}, [processQuerySignal, locationDataLoading]);
|
||||
|
||||
async function readChatStream(response: Response) {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is null");
|
||||
useEffect(() => {
|
||||
if (!conversationId) return;
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const eventDelimiter = "␃🔚␗";
|
||||
let buffer = "";
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/chat/ws?client=web`;
|
||||
setSocketUrl(wsUrl);
|
||||
|
||||
// Track context used for chat response
|
||||
let context: Context[] = [];
|
||||
let onlineContext: OnlineContext = {};
|
||||
let codeContext: CodeContext = {};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
setImages([]);
|
||||
|
||||
if (conversationId) generateNewTitle(conversationId, setTitle);
|
||||
|
||||
break;
|
||||
return () => {
|
||||
if (idleTimerRef.current) {
|
||||
clearTimeout(idleTimerRef.current);
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
|
||||
let newEventIndex;
|
||||
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
|
||||
const event = buffer.slice(0, newEventIndex);
|
||||
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
||||
if (event) {
|
||||
const currentMessage = messages.find((message) => !message.completed);
|
||||
|
||||
if (!currentMessage) {
|
||||
console.error("No current message found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Track context used for chat response. References are rendered at the end of the chat
|
||||
({ context, onlineContext, codeContext } = processMessageChunk(
|
||||
event,
|
||||
currentMessage,
|
||||
context,
|
||||
onlineContext,
|
||||
codeContext,
|
||||
));
|
||||
|
||||
setMessages([...messages]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleAbortedMessage() {
|
||||
const currentMessage = messages.find((message) => !message.completed);
|
||||
if (!currentMessage) return;
|
||||
|
||||
currentMessage.completed = true;
|
||||
setMessages([...messages]);
|
||||
setProcessQuerySignal(false);
|
||||
}
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
async function chat() {
|
||||
localStorage.removeItem("message");
|
||||
@@ -349,12 +509,19 @@ export default function Chat() {
|
||||
setProcessQuerySignal(false);
|
||||
return;
|
||||
}
|
||||
const chatAPI = "/api/chat?client=web";
|
||||
|
||||
// Re-establish WebSocket connection if disconnected
|
||||
resetIdleTimer();
|
||||
if (!socketUrl) {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/chat/ws?client=web`;
|
||||
setSocketUrl(wsUrl);
|
||||
}
|
||||
|
||||
const chatAPIBody = {
|
||||
q: queryToProcess,
|
||||
conversation_id: conversationId,
|
||||
stream: true,
|
||||
interrupt: shouldSendWithInterrupt,
|
||||
...(locationData && {
|
||||
city: locationData.city,
|
||||
region: locationData.region,
|
||||
@@ -366,58 +533,7 @@ export default function Chat() {
|
||||
...(uploadedFiles && { files: uploadedFiles }),
|
||||
};
|
||||
|
||||
// Reset the flag after using it
|
||||
setShouldSendWithInterrupt(false);
|
||||
|
||||
const response = await fetch(chatAPI, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(chatAPIBody),
|
||||
signal: abortMessageStreamController?.signal,
|
||||
});
|
||||
|
||||
try {
|
||||
await readChatStream(response);
|
||||
} catch (err) {
|
||||
let apiError;
|
||||
try {
|
||||
apiError = await response.json();
|
||||
} catch (err) {
|
||||
// Error reading API error response
|
||||
apiError = {
|
||||
streamError: "Error reading API error response stream. Expected JSON response.",
|
||||
};
|
||||
}
|
||||
console.error(apiError);
|
||||
// Retrieve latest message being processed
|
||||
const currentMessage = messages.find((message) => !message.completed);
|
||||
if (!currentMessage) return;
|
||||
|
||||
// Render error message as current message
|
||||
const errorMessage = (err as Error).message;
|
||||
const errorName = (err as Error).name;
|
||||
if (errorMessage.includes("Error in input stream"))
|
||||
currentMessage.rawResponse = `Woops! The connection broke while I was writing my thoughts down. Maybe try again in a bit or dislike this message if the issue persists?`;
|
||||
else if (apiError.streamError) {
|
||||
currentMessage.rawResponse = `Umm, not sure what just happened but I lost my train of thought. Could you try again or ask my developers to look into this if the issue persists? They can be contacted at the Khoj Github, Discord or team@khoj.dev.`;
|
||||
} else if (response.status === 429) {
|
||||
"detail" in apiError
|
||||
? (currentMessage.rawResponse = `${apiError.detail}`)
|
||||
: (currentMessage.rawResponse = `I'm a bit overwhelmed at the moment. Could you try again in a bit or dislike this message if the issue persists?`);
|
||||
} else if (errorName === "AbortError") {
|
||||
currentMessage.rawResponse = `I've stopped processing this message. If you'd like to continue, please send the message again.`;
|
||||
} else {
|
||||
currentMessage.rawResponse = `Umm, not sure what just happened. I see this error message: ${errorMessage}. Could you try again or dislike this message if the issue persists?`;
|
||||
}
|
||||
|
||||
// Complete message streaming teardown properly
|
||||
currentMessage.completed = true;
|
||||
setMessages([...messages]);
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
}
|
||||
sendMessage(JSON.stringify(chatAPIBody));
|
||||
}
|
||||
|
||||
const handleConversationIdChange = (newConversationId: string) => {
|
||||
@@ -458,9 +574,12 @@ export default function Chat() {
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar conversationId={conversationId || ""} />
|
||||
<div className="print-hidden">
|
||||
<AppSidebar conversationId={conversationId || ""} />
|
||||
</div>
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||
<DeprecationBanner />
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 print-hidden">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{conversationId && (
|
||||
@@ -468,9 +587,9 @@ export default function Chat() {
|
||||
className={`${styles.chatTitleWrapper} text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mx-2 md:mr-8 col-auto h-fit`}
|
||||
>
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
title && (
|
||||
<>
|
||||
@@ -493,7 +612,7 @@ export default function Chat() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-12 w-12 data-[state=open]:bg-accent"
|
||||
className="h-12 w-12 data-[state=open]:bg-accent print-hidden"
|
||||
onClick={() => setIsChatSideBarOpen(!isChatSideBarOpen)}
|
||||
>
|
||||
<Joystick className="w-6 h-6" />
|
||||
@@ -518,7 +637,7 @@ export default function Chat() {
|
||||
isMobileWidth={isMobileWidth}
|
||||
onConversationIdChange={handleConversationIdChange}
|
||||
setImages={setImages}
|
||||
setTriggeredAbort={setTriggeredAbort}
|
||||
setTriggeredAbort={handleTriggeredAbort}
|
||||
isChatSideBarOpen={isChatSideBarOpen}
|
||||
setIsChatSideBarOpen={setIsChatSideBarOpen}
|
||||
isActive={authenticatedData?.is_active}
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface UserConfig {
|
||||
enabled_content_source: SyncedContent;
|
||||
has_documents: boolean;
|
||||
notion_token: string | null;
|
||||
enable_memory: boolean;
|
||||
server_memory_mode: "disabled" | "enabled_default_off" | "enabled_default_on";
|
||||
// user model settings
|
||||
search_model_options: ModelOptions[];
|
||||
selected_search_model_config: number;
|
||||
@@ -90,11 +92,9 @@ export interface UserConfig {
|
||||
|
||||
export function useUserConfig(detailed: boolean = false) {
|
||||
const url = `/api/settings?detailed=${detailed}`;
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
|
||||
const { data, error, isLoading } = useSWR<UserConfig>(url, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
if (error || !data || data?.detail === "Forbidden") {
|
||||
return { data: null, error, isLoading };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { PopoverProps } from "@radix-ui/react-popover";
|
||||
|
||||
import { Check, CaretUpDown } from "@phosphor-icons/react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsMobileWidth, useMutationObserver } from "@/app/common/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
||||
import { ModelOptions, useUserConfig } from "./auth";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
@@ -35,7 +31,7 @@ interface ModelSelectorProps extends PopoverProps {
|
||||
}
|
||||
|
||||
export function ModelSelector({ ...props }: ModelSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [peekedModel, setPeekedModel] = useState<ModelOptions | undefined>(undefined);
|
||||
const [selectedModel, setSelectedModel] = useState<ModelOptions | undefined>(undefined);
|
||||
const { data: userConfig, error, isLoading: isLoadingUserConfig } = useUserConfig(true);
|
||||
@@ -48,14 +44,18 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
|
||||
if (userConfig) {
|
||||
setModels(userConfig.chat_model_options);
|
||||
if (!props.initialModel) {
|
||||
const selectedChatModelOption = userConfig.chat_model_options.find(model => model.id === userConfig.selected_chat_model_config);
|
||||
const selectedChatModelOption = userConfig.chat_model_options.find(
|
||||
(model) => model.id === userConfig.selected_chat_model_config,
|
||||
);
|
||||
if (!selectedChatModelOption && userConfig.chat_model_options.length > 0) {
|
||||
setSelectedModel(userConfig.chat_model_options[0]);
|
||||
} else {
|
||||
setSelectedModel(selectedChatModelOption);
|
||||
}
|
||||
} else {
|
||||
const model = userConfig.chat_model_options.find(model => model.name === props.initialModel);
|
||||
const model = userConfig.chat_model_options.find(
|
||||
(model) => model.name === props.initialModel,
|
||||
);
|
||||
setSelectedModel(model);
|
||||
}
|
||||
}
|
||||
@@ -68,15 +68,11 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
|
||||
}, [selectedModel, userConfig, props.onSelect]);
|
||||
|
||||
if (isLoadingUserConfig) {
|
||||
return (
|
||||
<Skeleton className="w-full h-10" />
|
||||
);
|
||||
return <Skeleton className="w-full h-10" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-sm text-error">{error.message}</div>
|
||||
);
|
||||
return <div className="text-sm text-error">{error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -92,30 +88,85 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
|
||||
disabled={props.disabled ?? false}
|
||||
>
|
||||
<p className="truncate">
|
||||
{selectedModel ? selectedModel.name.substring(0, 20) : "Select a model..."}
|
||||
{selectedModel
|
||||
? selectedModel.name?.substring(0, 20)
|
||||
: "Select a model..."}
|
||||
</p>
|
||||
<CaretUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[250px] p-0">
|
||||
{
|
||||
isMobileWidth ?
|
||||
{isMobileWidth ? (
|
||||
<div>
|
||||
<Command loop>
|
||||
<CommandList className="h-[var(--cmdk-list-height)]">
|
||||
<CommandInput placeholder="Search Models..." />
|
||||
<CommandEmpty>No Models found.</CommandEmpty>
|
||||
<CommandGroup key={"models"} heading={"Models"}>
|
||||
{models &&
|
||||
models.length > 0 &&
|
||||
models.map((model) => (
|
||||
<ModelItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
isSelected={selectedModel?.id === model.id}
|
||||
onPeek={(model) => setPeekedModel(model)}
|
||||
onSelect={() => {
|
||||
setSelectedModel(model);
|
||||
setOpen(false);
|
||||
}}
|
||||
isActive={props.isActive}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
) : (
|
||||
<HoverCard>
|
||||
<HoverCardContent
|
||||
side="left"
|
||||
align="start"
|
||||
forceMount
|
||||
className="min-h-[280px]"
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<h4 className="font-medium leading-none">
|
||||
{peekedModel?.name}
|
||||
</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{peekedModel?.description}
|
||||
</div>
|
||||
{peekedModel?.strengths ? (
|
||||
<div className="mt-4 grid gap-2">
|
||||
<h5 className="text-sm font-medium leading-none">
|
||||
Strengths
|
||||
</h5>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{peekedModel.strengths}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
<div>
|
||||
<HoverCardTrigger />
|
||||
<Command loop>
|
||||
<CommandList className="h-[var(--cmdk-list-height)]">
|
||||
<CommandInput placeholder="Search Models..." />
|
||||
<CommandEmpty>No Models found.</CommandEmpty>
|
||||
<CommandGroup key={"models"} heading={"Models"}>
|
||||
{models && models.length > 0 && models
|
||||
.map((model) => (
|
||||
{models &&
|
||||
models.length > 0 &&
|
||||
models.map((model) => (
|
||||
<ModelItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
isSelected={selectedModel?.id === model.id}
|
||||
onPeek={(model) => setPeekedModel(model)}
|
||||
onSelect={() => {
|
||||
setSelectedModel(model)
|
||||
setOpen(false)
|
||||
setSelectedModel(model);
|
||||
setOpen(false);
|
||||
}}
|
||||
isActive={props.isActive}
|
||||
/>
|
||||
@@ -124,74 +175,24 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
:
|
||||
<HoverCard>
|
||||
<HoverCardContent
|
||||
side="left"
|
||||
align="start"
|
||||
forceMount
|
||||
className="min-h-[280px]"
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<h4 className="font-medium leading-none">{peekedModel?.name}</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{peekedModel?.description}
|
||||
</div>
|
||||
{peekedModel?.strengths ? (
|
||||
<div className="mt-4 grid gap-2">
|
||||
<h5 className="text-sm font-medium leading-none">
|
||||
Strengths
|
||||
</h5>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{peekedModel.strengths}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
<div>
|
||||
<HoverCardTrigger />
|
||||
<Command loop>
|
||||
<CommandList className="h-[var(--cmdk-list-height)]">
|
||||
<CommandInput placeholder="Search Models..." />
|
||||
<CommandEmpty>No Models found.</CommandEmpty>
|
||||
<CommandGroup key={"models"} heading={"Models"}>
|
||||
{models && models.length > 0 && models
|
||||
.map((model) => (
|
||||
<ModelItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
isSelected={selectedModel?.id === model.id}
|
||||
onPeek={(model) => setPeekedModel(model)}
|
||||
onSelect={() => {
|
||||
setSelectedModel(model)
|
||||
setOpen(false)
|
||||
}}
|
||||
isActive={props.isActive}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
</HoverCard>
|
||||
}
|
||||
</HoverCard>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ModelItemProps {
|
||||
model: ModelOptions,
|
||||
isSelected: boolean,
|
||||
onSelect: () => void,
|
||||
onPeek: (model: ModelOptions) => void
|
||||
isActive?: boolean
|
||||
model: ModelOptions;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onPeek: (model: ModelOptions) => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useMutationObserver(ref, (mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
@@ -200,10 +201,10 @@ function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemP
|
||||
mutation.attributeName === "aria-selected" &&
|
||||
ref.current?.getAttribute("aria-selected") === "true"
|
||||
) {
|
||||
onPeek(model)
|
||||
onPeek(model);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
@@ -213,10 +214,9 @@ function ModelItem({ model, isSelected, onSelect, onPeek, isActive }: ModelItemP
|
||||
className="data-[selected=true]:bg-muted data-[selected=true]:text-secondary-foreground"
|
||||
disabled={!isActive && model.tier !== "free"}
|
||||
>
|
||||
{model.name} {model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
|
||||
<Check
|
||||
className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
{model.name}{" "}
|
||||
{model.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
|
||||
<Check className={cn("ml-auto", isSelected ? "opacity-100" : "opacity-0")} />
|
||||
</CommandItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
export interface LocationData {
|
||||
city?: string;
|
||||
@@ -78,16 +78,16 @@ export const useMutationObserver = (
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
}
|
||||
},
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
const observer = new MutationObserver(callback)
|
||||
observer.observe(ref.current, options)
|
||||
return () => observer.disconnect()
|
||||
const observer = new MutationObserver(callback);
|
||||
observer.observe(ref.current, options);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [ref, callback, options])
|
||||
}
|
||||
}, [ref, callback, options]);
|
||||
};
|
||||
|
||||
export function useIsDarkMode() {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styles from "./agentCard.module.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { UserProfile, ModelOptions, UserConfig } from "@/app/common/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -705,7 +706,7 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
You need to be a Futurist subscriber to create more agents.{" "}
|
||||
<a href="/settings">Upgrade now</a>.
|
||||
<Link href="/settings">Upgrade now</Link>.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
@@ -767,7 +768,7 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
|
||||
<FormDescription>
|
||||
{!props.isSubscribed ? (
|
||||
<p className="text-secondary-foreground">
|
||||
Upgrade to the <a href="/settings">Futurist plan</a> to
|
||||
Upgrade to the <Link href="/settings">Futurist plan</Link> to
|
||||
access all models.
|
||||
</p>
|
||||
) : (
|
||||
@@ -971,7 +972,7 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
|
||||
<FormLabel>Knowledge Base</FormLabel>
|
||||
<FormDescription>
|
||||
Which information should be part of its digital brain?{" "}
|
||||
<a href="/settings">Manage data</a>.
|
||||
<Link href="/settings">Manage data</Link>.
|
||||
</FormDescription>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex items-center justify-between text-sm gap-2 bg-muted p-2 rounded-lg">
|
||||
@@ -1061,12 +1062,27 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const filteredFiles = allFileOptions.filter(file =>
|
||||
file.toLowerCase().includes(fileSearchValue.toLowerCase())
|
||||
const filteredFiles =
|
||||
allFileOptions.filter((file) =>
|
||||
file
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
fileSearchValue.toLowerCase(),
|
||||
),
|
||||
);
|
||||
const currentFiles =
|
||||
props.form.getValues("files") ||
|
||||
[];
|
||||
const newFiles = [
|
||||
...new Set([
|
||||
...currentFiles,
|
||||
...filteredFiles,
|
||||
]),
|
||||
];
|
||||
props.form.setValue(
|
||||
"files",
|
||||
newFiles,
|
||||
);
|
||||
const currentFiles = props.form.getValues("files") || [];
|
||||
const newFiles = [...new Set([...currentFiles, ...filteredFiles])];
|
||||
props.form.setValue("files", newFiles);
|
||||
}}
|
||||
>
|
||||
Select All
|
||||
@@ -1078,12 +1094,28 @@ export function AgentModificationForm(props: AgentModificationFormProps) {
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const filteredFiles = allFileOptions.filter(file =>
|
||||
file.toLowerCase().includes(fileSearchValue.toLowerCase())
|
||||
const filteredFiles =
|
||||
allFileOptions.filter((file) =>
|
||||
file
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
fileSearchValue.toLowerCase(),
|
||||
),
|
||||
);
|
||||
const currentFiles =
|
||||
props.form.getValues("files") ||
|
||||
[];
|
||||
const newFiles =
|
||||
currentFiles.filter(
|
||||
(file) =>
|
||||
!filteredFiles.includes(
|
||||
file,
|
||||
),
|
||||
);
|
||||
props.form.setValue(
|
||||
"files",
|
||||
newFiles,
|
||||
);
|
||||
const currentFiles = props.form.getValues("files") || [];
|
||||
const newFiles = currentFiles.filter(file => !filteredFiles.includes(file));
|
||||
props.form.setValue("files", newFiles);
|
||||
}}
|
||||
>
|
||||
Deselect All
|
||||
|
||||
@@ -127,7 +127,7 @@ function renameConversation(conversationId: string, newTitle: string) {
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => { })
|
||||
.then((data) => {})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
@@ -171,7 +171,7 @@ function deleteConversation(conversationId: string) {
|
||||
mutate("/api/chat/sessions");
|
||||
}
|
||||
})
|
||||
.then((data) => { })
|
||||
.then((data) => {})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
@@ -245,9 +245,7 @@ export function FilesMenu(props: FilesMenuProps) {
|
||||
Context
|
||||
<p>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{
|
||||
error ? "Failed to load files" : "Failed to load selected files"
|
||||
}
|
||||
{error ? "Failed to load files" : "Failed to load selected files"}
|
||||
</span>
|
||||
</p>
|
||||
</h4>
|
||||
@@ -257,7 +255,7 @@ export function FilesMenu(props: FilesMenuProps) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!files) return <InlineLoading />;
|
||||
@@ -443,10 +441,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
|
||||
<div>
|
||||
{props.sideBarOpen && (
|
||||
<ScrollArea>
|
||||
<ScrollAreaScrollbar
|
||||
orientation="vertical"
|
||||
className="h-full w-2.5"
|
||||
/>
|
||||
<ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5" />
|
||||
<div className="p-0 m-0">
|
||||
{props.subsetOrganizedData != null &&
|
||||
Object.keys(props.subsetOrganizedData)
|
||||
@@ -471,7 +466,9 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
|
||||
agent_name={chatHistory.agent_name}
|
||||
agent_color={chatHistory.agent_color}
|
||||
agent_icon={chatHistory.agent_icon}
|
||||
agent_is_hidden={chatHistory.agent_is_hidden}
|
||||
agent_is_hidden={
|
||||
chatHistory.agent_is_hidden
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
@@ -709,7 +706,7 @@ function ChatSession(props: ChatHistory) {
|
||||
className="flex items-center gap-2 no-underline"
|
||||
>
|
||||
<p
|
||||
className={`${styles.session} ${props.compressed ? styles.compressed : 'max-w-[15rem] md:max-w-[22rem]'}`}
|
||||
className={`${styles.session} ${props.compressed ? styles.compressed : "max-w-[15rem] md:max-w-[22rem]"}`}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
@@ -949,7 +946,7 @@ export default function AllConversations(props: SidePanelProps) {
|
||||
|
||||
const currentDate = new Date();
|
||||
|
||||
chatSessions.forEach((chatSessionMetadata) => {
|
||||
chatSessions?.forEach((chatSessionMetadata) => {
|
||||
const chatDate = new Date(chatSessionMetadata.updated);
|
||||
const diffTime = Math.abs(currentDate.getTime() - chatDate.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
@@ -26,6 +26,8 @@ import { useIsDarkMode, useIsMobileWidth } from "@/app/common/utils";
|
||||
import { UserPlusIcon } from "lucide-react";
|
||||
import { useAuthenticatedData, UserProfile } from "@/app/common/auth";
|
||||
import LoginPrompt from "../loginPrompt/loginPrompt";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
async function openChat(userData: UserProfile | null | undefined) {
|
||||
const unauthenticatedRedirectUrl = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
|
||||
@@ -48,13 +50,12 @@ async function openChat(userData: UserProfile | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Menu items.
|
||||
const items = [
|
||||
{
|
||||
title: "Home",
|
||||
url: "/",
|
||||
icon: HouseSimple
|
||||
icon: HouseSimple,
|
||||
},
|
||||
{
|
||||
title: "Agents",
|
||||
@@ -89,17 +90,21 @@ interface AppSidebarProps {
|
||||
export function AppSidebar(props: AppSidebarProps) {
|
||||
const isMobileWidth = useIsMobileWidth();
|
||||
const { data, isLoading, error } = useAuthenticatedData();
|
||||
const pathname = usePathname();
|
||||
|
||||
const { state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar } =
|
||||
useSidebar();
|
||||
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
|
||||
// Check if we're on a shared chat page
|
||||
const isSharedChatPage = pathname?.startsWith("/share/chat");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !data) {
|
||||
if (!isLoading && !data && !isSharedChatPage) {
|
||||
setShowLoginPrompt(true);
|
||||
}
|
||||
}, [isLoading, data]);
|
||||
}, [isLoading, data, isSharedChatPage]);
|
||||
|
||||
return (
|
||||
<Sidebar collapsible={"icon"} variant="sidebar" className="md:py-2">
|
||||
@@ -108,15 +113,15 @@ export function AppSidebar(props: AppSidebarProps) {
|
||||
<SidebarMenuItem className="p-0 m-0">
|
||||
{open ? (
|
||||
<SidebarMenuButton>
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
) : (
|
||||
<SidebarMenuButton asChild>
|
||||
<a className="flex items-center gap-2 no-underline" href="/">
|
||||
<Link className="flex items-center gap-2 no-underline" href="/">
|
||||
<KhojLogo className="w-14 h-auto" />
|
||||
</a>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -16,3 +16,45 @@ div.trainOfThought {
|
||||
padding: 8px 16px;
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
/* If there is an inline element holding extremely long content, ensure it wraps */
|
||||
div.trainOfThought pre,
|
||||
div.trainOfThought code,
|
||||
div.trainOfThought p,
|
||||
div.trainOfThought span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Print-specific styles for chat history */
|
||||
@media print {
|
||||
div.chatHistory {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
position: static !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
div.chatHistory > * {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Show agent indicators clearly in print */
|
||||
div.agentIndicator {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Train of thought styling for print */
|
||||
div.trainOfThought {
|
||||
border-left: 2px solid #ccc !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.5rem 1rem !important;
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AgentData } from "@/app/components/agentCard/agentCard";
|
||||
import React from "react";
|
||||
import { useIsMobileWidth } from "@/app/common/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { KhojLogo } from "../logo/khojLogo";
|
||||
|
||||
interface ChatResponse {
|
||||
status: string;
|
||||
@@ -51,7 +52,7 @@ interface TrainOfThoughtFrame {
|
||||
}
|
||||
|
||||
interface TrainOfThoughtGroup {
|
||||
type: 'video' | 'text';
|
||||
type: "video" | "text";
|
||||
frames?: TrainOfThoughtFrame[];
|
||||
textEntries?: TrainOfThoughtObject[];
|
||||
}
|
||||
@@ -64,7 +65,9 @@ interface TrainOfThoughtComponentProps {
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): TrainOfThoughtGroup[] {
|
||||
function extractTrainOfThoughtGroups(
|
||||
trainOfThought?: TrainOfThoughtObject[],
|
||||
): TrainOfThoughtGroup[] {
|
||||
if (!trainOfThought) return [];
|
||||
|
||||
const groups: TrainOfThoughtGroup[] = [];
|
||||
@@ -93,8 +96,8 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
|
||||
// If we have accumulated text entries, add them as a text group
|
||||
if (currentTextEntries.length > 0) {
|
||||
groups.push({
|
||||
type: 'text',
|
||||
textEntries: [...currentTextEntries]
|
||||
type: "text",
|
||||
textEntries: [...currentTextEntries],
|
||||
});
|
||||
currentTextEntries = [];
|
||||
}
|
||||
@@ -115,8 +118,8 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
|
||||
// If we have accumulated video frames, add them as a video group
|
||||
if (currentVideoFrames.length > 0) {
|
||||
groups.push({
|
||||
type: 'video',
|
||||
frames: [...currentVideoFrames]
|
||||
type: "video",
|
||||
frames: [...currentVideoFrames],
|
||||
});
|
||||
currentVideoFrames = [];
|
||||
}
|
||||
@@ -129,14 +132,14 @@ function extractTrainOfThoughtGroups(trainOfThought?: TrainOfThoughtObject[]): T
|
||||
// Add any remaining frames/entries
|
||||
if (currentVideoFrames.length > 0) {
|
||||
groups.push({
|
||||
type: 'video',
|
||||
frames: currentVideoFrames
|
||||
type: "video",
|
||||
frames: currentVideoFrames,
|
||||
});
|
||||
}
|
||||
if (currentTextEntries.length > 0) {
|
||||
groups.push({
|
||||
type: 'text',
|
||||
textEntries: currentTextEntries
|
||||
type: "text",
|
||||
textEntries: currentTextEntries,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,10 +179,10 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
|
||||
// Convert string array to TrainOfThoughtObject array if needed
|
||||
let trainOfThoughtObjects: TrainOfThoughtObject[];
|
||||
|
||||
if (typeof props.trainOfThought[0] === 'string') {
|
||||
if (typeof props.trainOfThought[0] === "string") {
|
||||
trainOfThoughtObjects = (props.trainOfThought as string[]).map((data, index) => ({
|
||||
type: 'text',
|
||||
data: data
|
||||
type: "text",
|
||||
data: data,
|
||||
}));
|
||||
} else {
|
||||
trainOfThoughtObjects = props.trainOfThought as TrainOfThoughtObject[];
|
||||
@@ -220,28 +223,37 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
|
||||
<motion.div initial="closed" animate="open" exit="closed" variants={variants}>
|
||||
{trainOfThoughtGroups.map((group, groupIndex) => (
|
||||
<div key={`train-group-${groupIndex}`}>
|
||||
{group.type === 'video' && group.frames && group.frames.length > 0 && (
|
||||
<TrainOfThoughtVideoPlayer
|
||||
frames={group.frames}
|
||||
autoPlay={false}
|
||||
playbackSpeed={1500}
|
||||
/>
|
||||
)}
|
||||
{group.type === 'text' && group.textEntries && group.textEntries.map((entry, entryIndex) => {
|
||||
const lastIndex = trainOfThoughtGroups.length - 1;
|
||||
const isLastGroup = groupIndex === lastIndex;
|
||||
const isLastEntry = entryIndex === group.textEntries!.length - 1;
|
||||
const isPrimaryEntry = isLastGroup && isLastEntry && props.lastMessage && !props.completed;
|
||||
|
||||
return (
|
||||
<TrainOfThought
|
||||
key={`train-text-${groupIndex}-${entryIndex}-${entry.data.length}`}
|
||||
message={entry.data}
|
||||
primary={isPrimaryEntry}
|
||||
agentColor={props.agentColor}
|
||||
{group.type === "video" &&
|
||||
group.frames &&
|
||||
group.frames.length > 0 && (
|
||||
<TrainOfThoughtVideoPlayer
|
||||
frames={group.frames}
|
||||
autoPlay={false}
|
||||
playbackSpeed={1500}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
{group.type === "text" &&
|
||||
group.textEntries &&
|
||||
group.textEntries.map((entry, entryIndex) => {
|
||||
const lastIndex = trainOfThoughtGroups.length - 1;
|
||||
const isLastGroup = groupIndex === lastIndex;
|
||||
const isLastEntry =
|
||||
entryIndex === group.textEntries!.length - 1;
|
||||
const isPrimaryEntry =
|
||||
isLastGroup &&
|
||||
isLastEntry &&
|
||||
props.lastMessage &&
|
||||
!props.completed;
|
||||
|
||||
return (
|
||||
<TrainOfThought
|
||||
key={`train-text-${groupIndex}-${entryIndex}-${entry.data.length}`}
|
||||
message={entry.data}
|
||||
primary={isPrimaryEntry}
|
||||
agentColor={props.agentColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
@@ -299,7 +311,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
// ResizeObserver to handle content height changes (e.g., images loading)
|
||||
useEffect(() => {
|
||||
const contentWrapper = scrollableContentWrapperRef.current;
|
||||
const scrollViewport = scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector);
|
||||
const scrollViewport =
|
||||
scrollAreaRef.current?.querySelector<HTMLElement>(scrollAreaSelector);
|
||||
|
||||
if (!contentWrapper || !scrollViewport) return;
|
||||
|
||||
@@ -307,14 +320,18 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
// Check current scroll position to decide if auto-scroll is warranted
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollViewport;
|
||||
const bottomThreshold = 50;
|
||||
const currentlyNearBottom = (scrollHeight - (scrollTop + clientHeight)) <= bottomThreshold;
|
||||
const currentlyNearBottom =
|
||||
scrollHeight - (scrollTop + clientHeight) <= bottomThreshold;
|
||||
|
||||
if (currentlyNearBottom) {
|
||||
// Only auto-scroll if there are incoming messages being processed
|
||||
if (props.incomingMessages && props.incomingMessages.length > 0) {
|
||||
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
|
||||
// If the last message is not completed, or it just completed (indicated by incompleteIncomingMessageIndex still being set)
|
||||
if (!lastMessage.completed || (lastMessage.completed && incompleteIncomingMessageIndex !== null)) {
|
||||
if (
|
||||
!lastMessage.completed ||
|
||||
(lastMessage.completed && incompleteIncomingMessageIndex !== null)
|
||||
) {
|
||||
scrollToBottom(true); // Use instant scroll
|
||||
}
|
||||
}
|
||||
@@ -462,7 +479,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
});
|
||||
});
|
||||
// Optimistically set, the scroll listener will verify
|
||||
if (instant || scrollAreaEl && (scrollAreaEl.scrollHeight - (scrollAreaEl.scrollTop + scrollAreaEl.clientHeight)) < 5) {
|
||||
if (
|
||||
instant ||
|
||||
(scrollAreaEl &&
|
||||
scrollAreaEl.scrollHeight - (scrollAreaEl.scrollTop + scrollAreaEl.clientHeight) <
|
||||
5)
|
||||
) {
|
||||
setIsNearBottom(true);
|
||||
}
|
||||
};
|
||||
@@ -536,6 +558,24 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
ref={scrollAreaRef}
|
||||
>
|
||||
<div ref={scrollableContentWrapperRef}>
|
||||
{/* Print-only header with conversation info */}
|
||||
<div className="print-only-header">
|
||||
<div className="print-header-content">
|
||||
<div className="print-header-left">
|
||||
<KhojLogo className="print-logo" />
|
||||
</div>
|
||||
<div className="print-header-right">
|
||||
<h1>{data?.slug || "Conversation with Khoj"}</h1>
|
||||
<div className="conversation-meta">
|
||||
<p>
|
||||
<strong>Agent:</strong> {constructAgentName()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div className={`${styles.chatHistory} ${props.customClassName}`}>
|
||||
<div ref={sentinelRef} style={{ height: "1px" }}>
|
||||
{fetchingData && <InlineLoading className="opacity-50" />}
|
||||
@@ -607,16 +647,19 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
conversationId={props.conversationId}
|
||||
turnId={messageTurnId}
|
||||
/>
|
||||
{message.trainOfThought && message.trainOfThought.length > 0 && (
|
||||
<TrainOfThoughtComponent
|
||||
trainOfThought={message.trainOfThought}
|
||||
lastMessage={index === incompleteIncomingMessageIndex}
|
||||
agentColor={data?.agent?.color || "orange"}
|
||||
key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map(t => t.length).join('-')}`}
|
||||
keyId={`${index}trainOfThought`}
|
||||
completed={message.completed}
|
||||
/>
|
||||
)}
|
||||
{message.trainOfThought &&
|
||||
message.trainOfThought.length > 0 && (
|
||||
<TrainOfThoughtComponent
|
||||
trainOfThought={message.trainOfThought}
|
||||
lastMessage={
|
||||
index === incompleteIncomingMessageIndex
|
||||
}
|
||||
agentColor={data?.agent?.color || "orange"}
|
||||
key={`${index}trainOfThought-${message.trainOfThought.length}-${message.trainOfThought.map((t) => t.length).join("-")}`}
|
||||
keyId={`${index}trainOfThought`}
|
||||
completed={message.completed}
|
||||
/>
|
||||
)}
|
||||
<ChatMessage
|
||||
key={`${index}incoming`}
|
||||
isMobileWidth={isMobileWidth}
|
||||
|
||||
@@ -82,7 +82,7 @@ interface ChatInputProps {
|
||||
isLoggedIn: boolean;
|
||||
agentColor?: string;
|
||||
isResearchModeEnabled?: boolean;
|
||||
setTriggeredAbort: (value: boolean) => void;
|
||||
setTriggeredAbort: (value: boolean, newMessage?: string) => void;
|
||||
prefillMessage?: string;
|
||||
focus?: ChatInputFocus;
|
||||
}
|
||||
@@ -189,9 +189,11 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
|
||||
return;
|
||||
}
|
||||
|
||||
// If currently processing, trigger abort first
|
||||
// If currently processing, handle interrupt first
|
||||
if (props.sendDisabled) {
|
||||
props.setTriggeredAbort(true);
|
||||
props.setTriggeredAbort(true, message.trim());
|
||||
setMessage(""); // Clear the input
|
||||
return; // Don't continue with regular message sending
|
||||
}
|
||||
|
||||
if (imageUploaded) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface FileContentSnippetProps {
|
||||
content: string;
|
||||
targetLine?: number;
|
||||
maxLines?: number; // Used when no target line; defaults to 20
|
||||
}
|
||||
|
||||
export default function FileContentSnippet({
|
||||
content,
|
||||
targetLine,
|
||||
maxLines = 20,
|
||||
}: FileContentSnippetProps) {
|
||||
const lines = (content || "").split("\n");
|
||||
|
||||
if (targetLine && targetLine > 0 && targetLine <= lines.length) {
|
||||
const startLine = Math.max(1, targetLine - 2);
|
||||
const endLine = Math.min(lines.length, targetLine + 5);
|
||||
const contextLines = lines.slice(startLine - 1, endLine);
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap text-xs">
|
||||
{contextLines.map((line, idx) => {
|
||||
const lineNum = startLine + idx;
|
||||
const isTarget = lineNum === targetLine;
|
||||
return (
|
||||
<div
|
||||
key={lineNum}
|
||||
className={isTarget ? "bg-green-100 dark:bg-green-900" : ""}
|
||||
>
|
||||
<span className="text-gray-400 select-none mr-2">
|
||||
{lineNum.toString().padStart(3, " ")}:
|
||||
</span>
|
||||
{line}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
const previewLines = lines.slice(0, maxLines);
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap text-xs">
|
||||
{previewLines.map((line, index) => (
|
||||
<div key={index}>
|
||||
<span className="text-gray-400 select-none mr-2">
|
||||
{(index + 1).toString().padStart(3, " ")}:
|
||||
</span>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
{lines.length > maxLines && (
|
||||
<div className="text-gray-500 italic">
|
||||
... and {lines.length - maxLines} more lines
|
||||
</div>
|
||||
)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,57 @@ div.chatMessageWrapper a span {
|
||||
display: revert !important;
|
||||
}
|
||||
|
||||
/* File link styling */
|
||||
.chatMessageWrapper a.file-link {
|
||||
color: hsl(var(--primary));
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.chatMessageWrapper a.file-link:hover {
|
||||
color: hsl(var(--primary-foreground));
|
||||
background-color: hsl(var(--primary));
|
||||
text-decoration: none;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.chatMessageWrapper table {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid hsl(var(--border));
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chatMessageWrapper table th,
|
||||
.chatMessageWrapper table td {
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.chatMessageWrapper table th {
|
||||
background-color: hsl(var(--muted));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Alternating row colors for better readability */
|
||||
.chatMessageWrapper table tbody tr:nth-child(even) {
|
||||
background-color: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.chatMessageWrapper table tbody tr:nth-child(odd) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Hover effect for table rows */
|
||||
.chatMessageWrapper table tbody tr:hover {
|
||||
background-color: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
div.khojfullHistory {
|
||||
padding-left: 4px;
|
||||
}
|
||||
@@ -76,7 +127,9 @@ div.imageWrapper img {
|
||||
}
|
||||
|
||||
div.khoj div.imageWrapper img {
|
||||
height: 512px;
|
||||
max-height: 512px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
div.khoj div.imageWrapper {
|
||||
@@ -197,4 +250,141 @@ div.trainOfThoughtElement ul {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chatMessageWrapper table th,
|
||||
.chatMessageWrapper table td {
|
||||
min-width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print-specific styles for chat messages */
|
||||
@media print {
|
||||
div.chatMessageContainer {
|
||||
background: transparent !important;
|
||||
border: 1px solid #ccc !important;
|
||||
border-radius: 8px !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.5rem !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
div.chatMessageContainer.you {
|
||||
margin: 1rem 0 0 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
div.chatMessageContainer.khoj {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
div.chatMessageWrapper {
|
||||
padding-left: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
div.you div.chatMessageWrapper {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
div.you {
|
||||
background-color: transparent !important;
|
||||
color: #000 !important;
|
||||
font-size: 16pt !important;
|
||||
font-weight: 500 !important;
|
||||
padding: 0.8rem 0 0.1rem 0 !important;
|
||||
align-self: flex-start !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
div.khoj {
|
||||
background-color: transparent !important;
|
||||
color: #000 !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
align-self: stretch !important;
|
||||
}
|
||||
|
||||
div.youfullHistory,
|
||||
div.khojfullHistory {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
div.author {
|
||||
color: #666 !important;
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
/* Add timestamp as divider between title and body */
|
||||
div.you.chatMessageContainer::after {
|
||||
content: "🕐 " attr(data-created) !important;
|
||||
display: block !important;
|
||||
color: #888 !important;
|
||||
font-size: 9pt !important;
|
||||
font-weight: normal !important;
|
||||
margin: 0.2rem 0 0.6rem 0 !important;
|
||||
padding-bottom: 0.4rem !important;
|
||||
}
|
||||
|
||||
/* Hide interactive elements */
|
||||
div.chatFooter,
|
||||
div.chatButtons,
|
||||
button.codeCopyButton,
|
||||
button.copyButton,
|
||||
button.retryButton,
|
||||
div.feedbackButtons {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Image styling for print */
|
||||
div.imagesContainer {
|
||||
display: block !important;
|
||||
overflow: visible !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
div.imageWrapper {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
div.imageWrapper img,
|
||||
div.khoj div.imageWrapper img {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 4in !important;
|
||||
object-fit: contain !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/* Train of thought styling for print */
|
||||
div.trainOfThought {
|
||||
border-left: 2px solid #ccc !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.5rem !important;
|
||||
font-size: 0.9em !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
div.trainOfThought strong {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
div.trainOfThought.primary {
|
||||
border-left-color: #000 !important;
|
||||
}
|
||||
|
||||
div.trainOfThought.primary strong {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
div.trainOfThoughtElement {
|
||||
display: block !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,19 @@ import markdownIt from "markdown-it";
|
||||
import mditHljs from "markdown-it-highlightjs";
|
||||
import React, { useEffect, useRef, useState, forwardRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import { TeaserReferencesSection, constructAllReferences } from "../referencePanel/referencePanel";
|
||||
import {
|
||||
TeaserReferencesSection,
|
||||
constructAllReferences,
|
||||
} from "@/app/components/referencePanel/referencePanel";
|
||||
import { renderCodeGenImageInline } from "@/app/common/chatFunctions";
|
||||
import { fileLinksPlugin } from "@/app/components/chatMessage/fileLinksPlugin";
|
||||
import { imageValidationPlugin } from "@/app/components/chatMessage/imageValidationPlugin";
|
||||
import FileContentSnippet from "@/app/components/chatMessage/FileContentSnippet";
|
||||
import { useFileContent } from "@/app/components/chatMessage/useFileContent";
|
||||
|
||||
import {
|
||||
ThumbsUp,
|
||||
@@ -41,7 +49,6 @@ import { convertColorToTextClass } from "@/app/common/colorUtils";
|
||||
import { AgentData } from "@/app/components/agentCard/agentCard";
|
||||
|
||||
import renderMathInElement from "katex/contrib/auto-render";
|
||||
import "katex/dist/katex.min.css";
|
||||
import ExcalidrawComponent from "../excalidraw/excalidraw";
|
||||
import { AttachedFileText } from "../chatInputArea/chatInputArea";
|
||||
import {
|
||||
@@ -68,6 +75,9 @@ md.use(mditHljs, {
|
||||
code: true,
|
||||
});
|
||||
|
||||
md.use(fileLinksPlugin);
|
||||
md.use(imageValidationPlugin);
|
||||
|
||||
export interface Context {
|
||||
compiled: string;
|
||||
file: string;
|
||||
@@ -304,11 +314,15 @@ function chooseIconFromHeader(header: string, iconColor: string) {
|
||||
return <Toolbox className={`${classNames}`} />;
|
||||
}
|
||||
|
||||
if (compareHeader.includes("notes")) {
|
||||
if (
|
||||
compareHeader.includes("notes") ||
|
||||
compareHeader.includes("documents") ||
|
||||
compareHeader.includes("files")
|
||||
) {
|
||||
return <Folder className={`${classNames}`} />;
|
||||
}
|
||||
|
||||
if (compareHeader.includes("read")) {
|
||||
if (compareHeader.includes("browsing")) {
|
||||
return <Book className={`${classNames}`} />;
|
||||
}
|
||||
|
||||
@@ -385,6 +399,43 @@ export function TrainOfThought(props: TrainOfThoughtProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Clean mermaid chart by removing/fixing invalid syntax patterns
|
||||
function cleanMermaidChart(chart: string): string {
|
||||
return chart
|
||||
.split("\n")
|
||||
.filter((line) => !line.trim().match(/^title\s*\[.*\]\s*$/i)) // Remove invalid title[...] lines
|
||||
.map((line) => {
|
||||
// Fix parentheses inside square bracket node labels: [Text (with parens)]
|
||||
// Mermaid interprets () as special syntax, so we need to quote the content
|
||||
// Replace [Label (text)] with ["Label (text)"]
|
||||
return line.replace(/\[([^\]]*\([^\]]*\)[^\]]*)\]/g, '["$1"]');
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// Extract mermaid code blocks from markdown content
|
||||
function extractMermaidBlocks(content: string): { cleanedContent: string; mermaidBlocks: string[] } {
|
||||
const mermaidBlocks: string[] = [];
|
||||
// Match ```mermaid ... ``` code blocks
|
||||
// Allow optional whitespace before/after delimiters and handle various line endings
|
||||
const mermaidRegex = /```\s*mermaid\s*\r?\n([\s\S]*?)```/gi;
|
||||
|
||||
const cleanedContent = content.replace(mermaidRegex, (match, mermaidCode) => {
|
||||
const trimmedCode = mermaidCode.trim();
|
||||
if (trimmedCode) {
|
||||
// Clean the mermaid chart before adding
|
||||
const cleanedChart = cleanMermaidChart(trimmedCode);
|
||||
if (cleanedChart.trim()) {
|
||||
mermaidBlocks.push(cleanedChart);
|
||||
}
|
||||
}
|
||||
// Replace with empty string to remove from markdown
|
||||
return "";
|
||||
});
|
||||
|
||||
return { cleanedContent, mermaidBlocks };
|
||||
}
|
||||
|
||||
const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) => {
|
||||
const [copySuccess, setCopySuccess] = useState<boolean>(false);
|
||||
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||
@@ -394,6 +445,24 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
const [interrupted, setInterrupted] = useState<boolean>(false);
|
||||
const [excalidrawData, setExcalidrawData] = useState<string>("");
|
||||
const [mermaidjsData, setMermaidjsData] = useState<string>("");
|
||||
const [inlineMermaidBlocks, setInlineMermaidBlocks] = useState<string[]>([]);
|
||||
|
||||
// State for file content preview on file link click, hover
|
||||
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
|
||||
const [previewFilePath, setPreviewFilePath] = useState<string>("");
|
||||
const [previewLineNumber, setPreviewLineNumber] = useState<number | undefined>(undefined);
|
||||
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
const [previewContent, setPreviewContent] = useState<string>("");
|
||||
|
||||
const [hoverOpen, setHoverOpen] = useState<boolean>(false);
|
||||
const [hoverFilePath, setHoverFilePath] = useState<string>("");
|
||||
const [hoverLineNumber, setHoverLineNumber] = useState<number | undefined>(undefined);
|
||||
const [hoverLoading, setHoverLoading] = useState<boolean>(false);
|
||||
const [hoverError, setHoverError] = useState<string | null>(null);
|
||||
const [hoverContent, setHoverContent] = useState<string>("");
|
||||
const [hoverPos, setHoverPos] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const hoverCloseTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
const interruptedRef = useRef<boolean>(false);
|
||||
const messageRef = useRef<HTMLDivElement>(null);
|
||||
@@ -441,12 +510,10 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
setMermaidjsData(props.chatMessage.mermaidjsDiagram);
|
||||
}
|
||||
|
||||
// Replace LaTeX delimiters with placeholders
|
||||
message = message
|
||||
.replace(/\\\(/g, "LEFTPAREN")
|
||||
.replace(/\\\)/g, "RIGHTPAREN")
|
||||
.replace(/\\\[/g, "LEFTBRACKET")
|
||||
.replace(/\\\]/g, "RIGHTBRACKET");
|
||||
// Extract mermaid blocks from the message content
|
||||
const { cleanedContent, mermaidBlocks } = extractMermaidBlocks(message);
|
||||
message = cleanedContent;
|
||||
setInlineMermaidBlocks(mermaidBlocks);
|
||||
|
||||
// Replace file links with base64 data
|
||||
message = renderCodeGenImageInline(message, props.chatMessage.codeContext);
|
||||
@@ -465,7 +532,7 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
});
|
||||
}
|
||||
|
||||
// Handle user attached images rendering
|
||||
// Handle rendering user attached or khoj generated images
|
||||
let messageForClipboard = message;
|
||||
let messageToRender = message;
|
||||
if (props.chatMessage.images && props.chatMessage.images.length > 0) {
|
||||
@@ -477,12 +544,12 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
});
|
||||
const imagesInMd = sanitizedImages
|
||||
.map((sanitizedImage, index) => {
|
||||
return ``;
|
||||
return ``;
|
||||
})
|
||||
.join("\n");
|
||||
const imagesInHtml = sanitizedImages
|
||||
.map((sanitizedImage, index) => {
|
||||
return `<div class="${styles.imageWrapper}"><img src="${sanitizedImage}" alt="uploaded image ${index + 1}" /></div>`;
|
||||
return `<div class="${styles.imageWrapper}"><img src="${sanitizedImage}" alt="rendered image ${index + 1}" /></div>`;
|
||||
})
|
||||
.join("");
|
||||
const userImagesInHtml = `<div class="${styles.imagesContainer}">${imagesInHtml}</div>`;
|
||||
@@ -493,10 +560,27 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
// Set the message text
|
||||
setTextRendered(messageForClipboard);
|
||||
|
||||
// Replace LaTeX delimiters with placeholders
|
||||
messageToRender = messageToRender
|
||||
.replace(/\\\(/g, "LEFTPAREN")
|
||||
.replace(/\\\)/g, "RIGHTPAREN")
|
||||
.replace(/\\\[/g, "LEFTBRACKET")
|
||||
.replace(/\\\]/g, "RIGHTBRACKET");
|
||||
|
||||
// Preprocess file:// links so markdown-it processes them
|
||||
// We convert them to a custom scheme (filelink://) and handle in the plugin
|
||||
messageToRender = messageToRender.replace(
|
||||
/\[([^\]]+)\]\(file:\/\/([^)]+)\)/g,
|
||||
(match, text, path) => {
|
||||
// Use a special scheme that markdown-it will process
|
||||
return `[${text}](filelink://${path})`;
|
||||
},
|
||||
);
|
||||
|
||||
// Render the markdown
|
||||
let markdownRendered = md.render(messageToRender);
|
||||
|
||||
// Replace placeholders with LaTeX delimiters
|
||||
// Revert placeholders with LaTeX delimiters
|
||||
markdownRendered = markdownRendered
|
||||
.replace(/LEFTPAREN/g, "\\(")
|
||||
.replace(/RIGHTPAREN/g, "\\)")
|
||||
@@ -504,7 +588,12 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
.replace(/RIGHTBRACKET/g, "\\]");
|
||||
|
||||
// Sanitize and set the rendered markdown
|
||||
setMarkdownRendered(DOMPurify.sanitize(markdownRendered));
|
||||
// Configure DOMPurify to allow file link attributes
|
||||
const cleanMarkdown = DOMPurify.sanitize(markdownRendered, {
|
||||
ADD_ATTR: ["data-file-path", "data-line-number"],
|
||||
});
|
||||
|
||||
setMarkdownRendered(cleanMarkdown);
|
||||
}, [props.chatMessage.message, props.chatMessage.images, props.chatMessage.intent]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -542,6 +631,90 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation on the message container for reliability
|
||||
const container = messageRef.current;
|
||||
|
||||
const delegatedPointerDown = (ev: Event) => {
|
||||
const e = ev as MouseEvent;
|
||||
const target = e.target as HTMLElement | null;
|
||||
const anchor = target?.closest?.("a.file-link") as HTMLAnchorElement | null;
|
||||
if (!anchor) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const path = anchor.getAttribute("data-file-path") || "";
|
||||
const line = anchor.getAttribute("data-line-number") || undefined;
|
||||
if (!path) return;
|
||||
// Close hover popover if open
|
||||
setHoverOpen(false);
|
||||
setPreviewFilePath(path);
|
||||
setPreviewLineNumber(line ? parseInt(line) : undefined);
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
let currentHoverAnchor: HTMLAnchorElement | null = null;
|
||||
const delegatedMouseOver = (ev: Event) => {
|
||||
const e = ev as MouseEvent;
|
||||
const target = e.target as HTMLElement | null;
|
||||
const anchor = target?.closest?.("a.file-link") as HTMLAnchorElement | null;
|
||||
if (!anchor) return;
|
||||
if (currentHoverAnchor === anchor) return;
|
||||
currentHoverAnchor = anchor;
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const path = anchor.getAttribute("data-file-path") || "";
|
||||
const line = anchor.getAttribute("data-line-number") || undefined;
|
||||
if (!path) return;
|
||||
setHoverPos({ x: Math.max(8, rect.left), y: rect.bottom + 6 });
|
||||
setHoverFilePath(path);
|
||||
setHoverLineNumber(line ? parseInt(line) : undefined);
|
||||
if (hoverCloseTimeoutRef.current) {
|
||||
window.clearTimeout(hoverCloseTimeoutRef.current);
|
||||
hoverCloseTimeoutRef.current = null;
|
||||
}
|
||||
// Open immediately for reliability
|
||||
setHoverOpen(true);
|
||||
};
|
||||
|
||||
const delegatedMouseOut = (ev: Event) => {
|
||||
const e = ev as MouseEvent;
|
||||
const target = e.target as HTMLElement | null;
|
||||
const related = e.relatedTarget as HTMLElement | null;
|
||||
const anchor = target?.closest?.("a.file-link") as HTMLAnchorElement | null;
|
||||
const stillInsideAnchor = !!(related && anchor && anchor.contains(related));
|
||||
// If moving between descendants of the same anchor, ignore
|
||||
if (stillInsideAnchor) return;
|
||||
// Schedule close; will be canceled if we move into the popover
|
||||
if (hoverCloseTimeoutRef.current) {
|
||||
window.clearTimeout(hoverCloseTimeoutRef.current);
|
||||
}
|
||||
hoverCloseTimeoutRef.current = window.setTimeout(() => {
|
||||
setHoverOpen(false);
|
||||
currentHoverAnchor = null;
|
||||
hoverCloseTimeoutRef.current = null;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const delegatedKeyDown = (ev: Event) => {
|
||||
const e = ev as KeyboardEvent;
|
||||
const target = e.target as HTMLElement | null;
|
||||
const anchor = target?.closest?.("a.file-link") as HTMLAnchorElement | null;
|
||||
if (!anchor) return;
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const path = anchor.getAttribute("data-file-path") || "";
|
||||
const line = anchor.getAttribute("data-line-number") || undefined;
|
||||
if (!path) return;
|
||||
setHoverOpen(false);
|
||||
setPreviewFilePath(path);
|
||||
setPreviewLineNumber(line ? parseInt(line) : undefined);
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
container.addEventListener("pointerdown", delegatedPointerDown);
|
||||
container.addEventListener("keydown", delegatedKeyDown);
|
||||
container.addEventListener("mouseover", delegatedMouseOver);
|
||||
container.addEventListener("mouseout", delegatedMouseOut);
|
||||
|
||||
renderMathInElement(messageRef.current, {
|
||||
delimiters: [
|
||||
{ left: "$$", right: "$$", display: true },
|
||||
@@ -549,12 +722,70 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
{ left: "\\(", right: "\\)", display: false },
|
||||
],
|
||||
});
|
||||
// Cleanup old listeners when content changes
|
||||
return () => {
|
||||
container.removeEventListener("pointerdown", delegatedPointerDown);
|
||||
container.removeEventListener("keydown", delegatedKeyDown);
|
||||
container.removeEventListener("mouseover", delegatedMouseOver);
|
||||
container.removeEventListener("mouseout", delegatedMouseOut);
|
||||
if (hoverCloseTimeoutRef.current) {
|
||||
window.clearTimeout(hoverCloseTimeoutRef.current);
|
||||
hoverCloseTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [markdownRendered, isHovering, messageRef]);
|
||||
}, [markdownRendered, messageRef]);
|
||||
|
||||
// Fetch file content for dialog and hover using shared hook
|
||||
const {
|
||||
content: previewContentHook,
|
||||
loading: previewLoadingHook,
|
||||
error: previewErrorHook,
|
||||
} = useFileContent(previewFilePath, previewOpen);
|
||||
const {
|
||||
content: hoverContentHook,
|
||||
loading: hoverLoadingHook,
|
||||
error: hoverErrorHook,
|
||||
} = useFileContent(hoverFilePath, hoverOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewContent(previewContentHook);
|
||||
}, [previewContentHook]);
|
||||
useEffect(() => {
|
||||
setPreviewLoading(previewLoadingHook);
|
||||
}, [previewLoadingHook]);
|
||||
useEffect(() => {
|
||||
setPreviewError(previewErrorHook);
|
||||
}, [previewErrorHook]);
|
||||
|
||||
useEffect(() => {
|
||||
setHoverContent(hoverContentHook);
|
||||
}, [hoverContentHook]);
|
||||
useEffect(() => {
|
||||
setHoverLoading(hoverLoadingHook);
|
||||
}, [hoverLoadingHook]);
|
||||
useEffect(() => {
|
||||
setHoverError(hoverErrorHook);
|
||||
}, [hoverErrorHook]);
|
||||
|
||||
function formatDate(timestamp: string) {
|
||||
// Format date in HH:MM, DD MMM YYYY format
|
||||
let date = new Date(timestamp + "Z");
|
||||
// Handle timestamps in "YYYY-MM-DD HH:MM:SS" format from backend
|
||||
let date: Date;
|
||||
if (timestamp.includes(" ") && !timestamp.includes("T")) {
|
||||
// Convert "YYYY-MM-DD HH:MM:SS" to ISO format
|
||||
date = new Date(timestamp.replace(" ", "T") + "Z");
|
||||
} else if (!timestamp.endsWith("Z")) {
|
||||
date = new Date(timestamp + "Z");
|
||||
} else {
|
||||
date = new Date(timestamp);
|
||||
}
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return "Invalid Date";
|
||||
}
|
||||
|
||||
let time_string = date
|
||||
.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true })
|
||||
.toUpperCase();
|
||||
@@ -713,6 +944,7 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
className={constructClasses(props.chatMessage)}
|
||||
onMouseLeave={(event) => setIsHovering(false)}
|
||||
onMouseEnter={(event) => setIsHovering(true)}
|
||||
data-created={formatDate(props.chatMessage.created)}
|
||||
>
|
||||
<div className={chatMessageWrapperClasses(props.chatMessage)}>
|
||||
{props.chatMessage.queryFiles && props.chatMessage.queryFiles.length > 0 && (
|
||||
@@ -760,8 +992,125 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
className={styles.chatMessage}
|
||||
dangerouslySetInnerHTML={{ __html: markdownRendered }}
|
||||
/>
|
||||
{/* File preview hover dialog */}
|
||||
{hoverOpen &&
|
||||
typeof window !== "undefined" &&
|
||||
createPortal(
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
if (hoverCloseTimeoutRef.current) {
|
||||
window.clearTimeout(hoverCloseTimeoutRef.current);
|
||||
hoverCloseTimeoutRef.current = null;
|
||||
}
|
||||
setHoverOpen(true);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
// If user clicks the hover preview, open the dialog for the same file
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setHoverOpen(false);
|
||||
if (hoverFilePath) {
|
||||
setPreviewFilePath(hoverFilePath);
|
||||
setPreviewLineNumber(hoverLineNumber);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (hoverCloseTimeoutRef.current) {
|
||||
window.clearTimeout(hoverCloseTimeoutRef.current);
|
||||
}
|
||||
hoverCloseTimeoutRef.current = window.setTimeout(() => {
|
||||
setHoverOpen(false);
|
||||
hoverCloseTimeoutRef.current = null;
|
||||
}, 200);
|
||||
}}
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: hoverPos.x,
|
||||
top: hoverPos.y,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
className="w-96 max-h-80 rounded-md border bg-popover p-4 text-popover-foreground shadow-md"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-sm font-medium">
|
||||
<span className="truncate">
|
||||
{hoverFilePath.split("/").pop() || hoverFilePath}
|
||||
</span>
|
||||
{hoverLineNumber && (
|
||||
<span className="text-gray-500 ml-2">
|
||||
- Line {hoverLineNumber}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ScrollArea className="max-h-60">
|
||||
{hoverLoading && (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<InlineLoading />
|
||||
</div>
|
||||
)}
|
||||
{!hoverLoading && hoverError && (
|
||||
<div className="p-3 text-red-500 text-sm">
|
||||
Error: {hoverError}
|
||||
</div>
|
||||
)}
|
||||
{!hoverLoading && !hoverError && (
|
||||
<div className="text-sm">
|
||||
<FileContentSnippet
|
||||
content={hoverContent}
|
||||
targetLine={hoverLineNumber}
|
||||
maxLines={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
{/* File preview popup dialog */}
|
||||
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<div className="truncate min-w-0 break-words break-all text-wrap max-w-full whitespace-normal">
|
||||
{previewFilePath.split("/").pop() || previewFilePath}
|
||||
<span className="text-gray-500 ml-2">
|
||||
{previewLineNumber ? `- Line ${previewLineNumber}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-left">
|
||||
<ScrollArea className="h-80 w-full rounded-md">
|
||||
{previewLoading && (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<InlineLoading />
|
||||
</div>
|
||||
)}
|
||||
{!previewLoading && previewError && (
|
||||
<div className="p-3 text-red-500 text-sm">
|
||||
Error: {previewError}
|
||||
</div>
|
||||
)}
|
||||
{!previewLoading && !previewError && (
|
||||
<div className="text-sm">
|
||||
<FileContentSnippet
|
||||
content={previewContent}
|
||||
targetLine={previewLineNumber}
|
||||
maxLines={20}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{excalidrawData && <ExcalidrawComponent data={excalidrawData} />}
|
||||
{mermaidjsData && <Mermaid chart={mermaidjsData} />}
|
||||
{inlineMermaidBlocks.map((chart, index) => (
|
||||
<Mermaid key={`inline-mermaid-${index}`} chart={chart} />
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.teaserReferencesContainer}>
|
||||
<TeaserReferencesSection
|
||||
@@ -816,38 +1165,44 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{props.chatMessage.by === "khoj" && props.onRetryMessage && props.isLastMessage && (
|
||||
<button
|
||||
title="Retry"
|
||||
className={`${styles.retryButton}`}
|
||||
onClick={() => {
|
||||
const turnId = props.chatMessage.turnId || props.turnId;
|
||||
const query = props.chatMessage.rawQuery || props.chatMessage.intent?.query;
|
||||
console.log("Retry button clicked for turnId:", turnId);
|
||||
console.log("ChatMessage data:", {
|
||||
rawQuery: props.chatMessage.rawQuery,
|
||||
intent: props.chatMessage.intent,
|
||||
message: props.chatMessage.message
|
||||
});
|
||||
console.log("Extracted query:", query);
|
||||
if (query) {
|
||||
props.onRetryMessage?.(query, turnId);
|
||||
} else {
|
||||
console.error("No original query found for retry");
|
||||
// Fallback: try to get from a previous user message or show an input dialog
|
||||
const fallbackQuery = prompt("Enter the original query to retry:");
|
||||
if (fallbackQuery) {
|
||||
props.onRetryMessage?.(fallbackQuery, turnId);
|
||||
{props.chatMessage.by === "khoj" &&
|
||||
props.onRetryMessage &&
|
||||
props.isLastMessage && (
|
||||
<button
|
||||
title="Retry"
|
||||
className={`${styles.retryButton}`}
|
||||
onClick={() => {
|
||||
const turnId = props.chatMessage.turnId || props.turnId;
|
||||
const query =
|
||||
props.chatMessage.rawQuery ||
|
||||
props.chatMessage.intent?.query;
|
||||
console.log("Retry button clicked for turnId:", turnId);
|
||||
console.log("ChatMessage data:", {
|
||||
rawQuery: props.chatMessage.rawQuery,
|
||||
intent: props.chatMessage.intent,
|
||||
message: props.chatMessage.message,
|
||||
});
|
||||
console.log("Extracted query:", query);
|
||||
if (query) {
|
||||
props.onRetryMessage?.(query, turnId);
|
||||
} else {
|
||||
console.error("No original query found for retry");
|
||||
// Fallback: try to get from a previous user message or show an input dialog
|
||||
const fallbackQuery = prompt(
|
||||
"Enter the original query to retry:",
|
||||
);
|
||||
if (fallbackQuery) {
|
||||
props.onRetryMessage?.(fallbackQuery, turnId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowClockwise
|
||||
alt="Retry Message"
|
||||
className="hsl(var(--muted-foreground)) hover:text-blue-500"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
<ArrowClockwise
|
||||
alt="Retry Message"
|
||||
className="hsl(var(--muted-foreground)) hover:text-blue-500"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
title="Copy"
|
||||
className={`${styles.copyButton}`}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
// File link renderer plugin for markdown-it
|
||||
// Handles links of the form [text](file:///path/to/file) or [text](file:///path/to/file#line=123)
|
||||
export function fileLinksPlugin(md: MarkdownIt) {
|
||||
// Store the original link_open renderer
|
||||
const defaultLinkOpenRenderer =
|
||||
md.renderer.rules.link_open ||
|
||||
function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// Override the link_open renderer
|
||||
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||
const token = tokens[idx];
|
||||
const hrefIndex = token.attrIndex("href");
|
||||
|
||||
if (hrefIndex >= 0) {
|
||||
const href = token.attrs![hrefIndex][1];
|
||||
|
||||
// Check if this is a filelink:// link (our preprocessed file:// links)
|
||||
if (href.startsWith("filelink://")) {
|
||||
// Extract file path and line number from filelink://path format
|
||||
const filePath = href.replace("filelink://", "");
|
||||
const fileMatch = filePath.match(/^(.+?)(?:#line=(\d+))?$/);
|
||||
|
||||
if (fileMatch) {
|
||||
const actualFilePath = fileMatch[1];
|
||||
const lineNumber = fileMatch[2];
|
||||
|
||||
// Add custom attributes for file links
|
||||
token.attrSet("data-file-path", actualFilePath);
|
||||
if (lineNumber) {
|
||||
token.attrSet("data-line-number", lineNumber);
|
||||
}
|
||||
// Append class if it exists; otherwise set it
|
||||
const classIdx = token.attrIndex("class");
|
||||
if (classIdx >= 0 && token.attrs) {
|
||||
token.attrs[classIdx][1] = `${token.attrs[classIdx][1]} file-link`;
|
||||
} else {
|
||||
token.attrSet("class", "file-link");
|
||||
}
|
||||
token.attrSet("href", "#"); // Prevent default navigation
|
||||
token.attrSet("role", "button");
|
||||
token.attrSet("tabindex", "0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultLinkOpenRenderer(tokens, idx, options, env, self);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
/**
|
||||
* Checks if a URL is a valid, loadable image URL.
|
||||
* Returns true for URLs that browsers can actually fetch:
|
||||
* - data: URLs (base64 encoded)
|
||||
* - blob: URLs (object URLs)
|
||||
* - http:// and https:// URLs
|
||||
*/
|
||||
function isValidImageUrl(url: string): boolean {
|
||||
if (!url || typeof url !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const trimmedUrl = url.trim();
|
||||
|
||||
// Allow data URLs (base64 encoded images)
|
||||
if (trimmedUrl.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow blob URLs
|
||||
if (trimmedUrl.startsWith("blob:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow HTTP/HTTPS URLs
|
||||
if (trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reject everything else (file://, relative paths, absolute paths, etc.)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image validation plugin for markdown-it
|
||||
* Filters out images with invalid/non-loadable URLs at render time.
|
||||
* This prevents broken images from ever being added to the DOM.
|
||||
*/
|
||||
export function imageValidationPlugin(md: MarkdownIt) {
|
||||
// Store the original image renderer
|
||||
const defaultImageRenderer =
|
||||
md.renderer.rules.image ||
|
||||
function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// Override the image renderer
|
||||
md.renderer.rules.image = function (tokens, idx, options, env, self) {
|
||||
const token = tokens[idx];
|
||||
const srcIndex = token.attrIndex("src");
|
||||
|
||||
if (srcIndex >= 0 && token.attrs) {
|
||||
const src = token.attrs[srcIndex][1];
|
||||
|
||||
// If the URL is not valid, don't render the image at all
|
||||
if (!isValidImageUrl(src)) {
|
||||
// Return alt text as fallback, or empty string
|
||||
const altText = token.content || "";
|
||||
if (altText) {
|
||||
return `<em style="color: #888; font-size: 0.9em;">[Image: ${md.utils.escapeHtml(altText)}]</em>`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return defaultImageRenderer(tokens, idx, options, env, self);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface UseFileContentResult {
|
||||
content: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Fetch file content for a given path when `enabled` is true.
|
||||
export function useFileContent(path: string | undefined, enabled: boolean): UseFileContentResult {
|
||||
const [content, setContent] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function run() {
|
||||
if (!enabled || !path) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setContent("");
|
||||
try {
|
||||
const resp = await fetch(`/api/content/file?file_name=${encodeURIComponent(path)}`);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to fetch file content (${resp.status})`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (!cancelled) setContent(data.raw_text || "");
|
||||
} catch (err) {
|
||||
if (!cancelled)
|
||||
setError(err instanceof Error ? err.message : "Failed to load file content");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [path, enabled]);
|
||||
|
||||
return { content, loading, error };
|
||||
}
|
||||
@@ -1,10 +1,29 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ArrowsDownUp, CaretCircleDown, CheckCircle, Circle, CircleNotch, PersonSimpleTaiChi, Sparkle } from "@phosphor-icons/react";
|
||||
import {
|
||||
ArrowsDownUp,
|
||||
CaretCircleDown,
|
||||
CheckCircle,
|
||||
Circle,
|
||||
CircleNotch,
|
||||
PersonSimpleTaiChi,
|
||||
Sparkle,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ModelSelector } from "@/app/common/modelSelector";
|
||||
import { FilesMenu } from "../allConversations/allConversations";
|
||||
@@ -14,21 +33,39 @@ import { mutate } from "swr";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { AgentData } from "../agentCard/agentCard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAvailableIcons, getIconForSlashCommand, getIconFromIconName } from "@/app/common/iconUtils";
|
||||
import {
|
||||
getAvailableIcons,
|
||||
getIconForSlashCommand,
|
||||
getIconFromIconName,
|
||||
} from "@/app/common/iconUtils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { TooltipContent } from "@radix-ui/react-tooltip";
|
||||
import { useAuthenticatedData } from "@/app/common/auth";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { convertColorToTextClass, tailwindColors } from "@/app/common/colorUtils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
|
||||
interface ChatSideBarProps {
|
||||
conversationId: string;
|
||||
isOpen: boolean;
|
||||
@@ -40,15 +77,10 @@ interface ChatSideBarProps {
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export function ChatSidebar({ ...props }: ChatSideBarProps) {
|
||||
|
||||
if (props.isMobileWidth) {
|
||||
return (
|
||||
<Sheet
|
||||
open={props.isOpen}
|
||||
onOpenChange={props.onOpenChange}>
|
||||
<SheetContent
|
||||
className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
>
|
||||
<Sheet open={props.isOpen} onOpenChange={props.onOpenChange}>
|
||||
<SheetContent className="w-[300px] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden">
|
||||
<ChatSidebarInternal {...props} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
@@ -56,7 +88,7 @@ export function ChatSidebar({ ...props }: ChatSideBarProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative h-full">
|
||||
<ChatSidebarInternal {...props} />
|
||||
</div>
|
||||
);
|
||||
@@ -110,14 +142,14 @@ function AgentCreationForm(props: IAgentCreationProps) {
|
||||
fetch(createAgentUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data: AgentData | AgentError) => {
|
||||
console.log("Success:", data);
|
||||
if ('detail' in data) {
|
||||
if ("detail" in data) {
|
||||
setError(`Error creating agent: ${data.detail}`);
|
||||
setIsCreating(false);
|
||||
return;
|
||||
@@ -142,162 +174,151 @@ function AgentCreationForm(props: IAgentCreationProps) {
|
||||
}, [customAgentName, customAgentIcon, customAgentColor]);
|
||||
|
||||
return (
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
>
|
||||
<Button className="w-full" variant="secondary">
|
||||
Create Agent
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
{
|
||||
doneCreating && createdSlug ? (
|
||||
<DialogTitle>
|
||||
Created {customAgentName}
|
||||
</DialogTitle>
|
||||
) : (
|
||||
<DialogTitle>
|
||||
Create a New Agent
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
{doneCreating && createdSlug ? (
|
||||
<DialogTitle>Created {customAgentName}</DialogTitle>
|
||||
) : (
|
||||
<DialogTitle>Create a New Agent</DialogTitle>
|
||||
)}
|
||||
<DialogClose />
|
||||
<DialogDescription>
|
||||
If these settings have been helpful, create a dedicated agent you can re-use across conversations.
|
||||
If these settings have been helpful, create a dedicated agent you can re-use
|
||||
across conversations.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
{
|
||||
doneCreating && createdSlug ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20
|
||||
}}
|
||||
>
|
||||
<CheckCircle
|
||||
className="w-16 h-16 text-green-500"
|
||||
weight="fill"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-center text-lg font-medium text-accent-foreground"
|
||||
>
|
||||
Created successfully!
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<Link href={`/agents?agent=${createdSlug}`}>
|
||||
<Button variant="secondary" className="mt-2">
|
||||
Manage Agent
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
{doneCreating && createdSlug ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<CheckCircle className="w-16 h-16 text-green-500" weight="fill" />
|
||||
</motion.div>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-center text-lg font-medium text-accent-foreground"
|
||||
>
|
||||
Created successfully!
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<Link href={`/agents?agent=${createdSlug}`}>
|
||||
<Button variant="secondary" className="mt-2">
|
||||
Manage Agent
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label htmlFor="agent_name">Name</Label>
|
||||
<Input
|
||||
id="agent_name"
|
||||
className="w-full p-2 border mt-4 border-slate-500 rounded-lg"
|
||||
disabled={isCreating}
|
||||
value={customAgentName}
|
||||
onChange={(e) => setCustomAgentName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) :
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label htmlFor="agent_name">Name</Label>
|
||||
<Input
|
||||
id="agent_name"
|
||||
className="w-full p-2 border mt-4 border-slate-500 rounded-lg"
|
||||
disabled={isCreating}
|
||||
value={customAgentName}
|
||||
onChange={(e) => setCustomAgentName(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
onValueChange={setCustomAgentColor}
|
||||
defaultValue={customAgentColor}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full dark:bg-muted"
|
||||
disabled={isCreating}
|
||||
>
|
||||
<SelectValue placeholder="Color" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="items-center space-y-1 inline-flex flex-col">
|
||||
{colorOptions.map((colorOption) => (
|
||||
<SelectItem key={colorOption} value={colorOption}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Circle
|
||||
className={`w-6 h-6 mr-2 ${convertColorToTextClass(colorOption)}`}
|
||||
weight="fill"
|
||||
/>
|
||||
{colorOption}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<Select onValueChange={setCustomAgentColor} defaultValue={customAgentColor}>
|
||||
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}>
|
||||
<SelectValue placeholder="Color" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="items-center space-y-1 inline-flex flex-col">
|
||||
{colorOptions.map((colorOption) => (
|
||||
<SelectItem key={colorOption} value={colorOption}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Circle
|
||||
className={`w-6 h-6 mr-2 ${convertColorToTextClass(colorOption)}`}
|
||||
weight="fill"
|
||||
/>
|
||||
{colorOption}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Select onValueChange={setCustomAgentIcon} defaultValue={customAgentIcon}>
|
||||
<SelectTrigger className="w-full dark:bg-muted" disabled={isCreating}>
|
||||
<SelectValue placeholder="Icon" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="items-center space-y-1 inline-flex flex-col">
|
||||
{iconOptions.map((iconOption) => (
|
||||
<SelectItem key={iconOption} value={iconOption}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getIconFromIconName(
|
||||
iconOption,
|
||||
customAgentColor ?? "gray",
|
||||
"w-6",
|
||||
"h-6",
|
||||
)}
|
||||
{iconOption}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
onValueChange={setCustomAgentIcon}
|
||||
defaultValue={customAgentIcon}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full dark:bg-muted"
|
||||
disabled={isCreating}
|
||||
>
|
||||
<SelectValue placeholder="Icon" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="items-center space-y-1 inline-flex flex-col">
|
||||
{iconOptions.map((iconOption) => (
|
||||
<SelectItem key={iconOption} value={iconOption}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getIconFromIconName(
|
||||
iconOption,
|
||||
customAgentColor ?? "gray",
|
||||
"w-6",
|
||||
"h-6",
|
||||
)}
|
||||
{iconOption}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
{
|
||||
error && (
|
||||
<div className="text-red-500 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!doneCreating && (
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => createAgent()}
|
||||
disabled={isCreating || !isValid}
|
||||
>
|
||||
{
|
||||
isCreating ?
|
||||
<CircleNotch className="animate-spin" />
|
||||
:
|
||||
<PersonSimpleTaiChi />
|
||||
}
|
||||
Create
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{error && <div className="text-red-500 text-sm">{error}</div>}
|
||||
{!doneCreating && (
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => createAgent()}
|
||||
disabled={isCreating || !isValid}
|
||||
>
|
||||
{isCreating ? (
|
||||
<CircleNotch className="animate-spin" />
|
||||
) : (
|
||||
<PersonSimpleTaiChi />
|
||||
)}
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
<DialogClose />
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog >
|
||||
|
||||
)
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
@@ -305,7 +326,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
const { data: agentConfigurationOptions, error: agentConfigurationOptionsError } =
|
||||
useSWR<AgentConfigurationOptions>("/api/agents/options", fetcher);
|
||||
|
||||
const { data: agentData, isLoading: agentDataLoading, error: agentDataError } = useSWR<AgentData>(`/api/agents/conversation?conversation_id=${props.conversationId}`, fetcher);
|
||||
const {
|
||||
data: agentData,
|
||||
isLoading: agentDataLoading,
|
||||
error: agentDataError,
|
||||
} = useSWR<AgentData>(
|
||||
`/api/agents/conversation?conversation_id=${props.conversationId}`,
|
||||
fetcher,
|
||||
);
|
||||
const {
|
||||
data: authenticatedData,
|
||||
error: authenticationError,
|
||||
@@ -317,7 +345,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
const [inputTools, setInputTools] = useState<string[] | undefined>();
|
||||
const [outputModes, setOutputModes] = useState<string[] | undefined>();
|
||||
const [hasModified, setHasModified] = useState<boolean>(false);
|
||||
const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(!agentData || agentData?.slug?.toLowerCase() === "khoj");
|
||||
const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(
|
||||
!agentData || agentData?.slug?.toLowerCase() === "khoj",
|
||||
);
|
||||
const [displayInputTools, setDisplayInputTools] = useState<string[] | undefined>();
|
||||
const [displayOutputModes, setDisplayOutputModes] = useState<string[] | undefined>();
|
||||
|
||||
@@ -330,12 +360,20 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
setInputTools(agentData.input_tools);
|
||||
setDisplayInputTools(agentData.input_tools);
|
||||
if (agentData.input_tools === undefined || agentData.input_tools.length === 0) {
|
||||
setDisplayInputTools(agentConfigurationOptions?.input_tools ? Object.keys(agentConfigurationOptions.input_tools) : []);
|
||||
setDisplayInputTools(
|
||||
agentConfigurationOptions?.input_tools
|
||||
? Object.keys(agentConfigurationOptions.input_tools)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
setOutputModes(agentData.output_modes);
|
||||
setDisplayOutputModes(agentData.output_modes);
|
||||
if (agentData.output_modes === undefined || agentData.output_modes.length === 0) {
|
||||
setDisplayOutputModes(agentConfigurationOptions?.output_modes ? Object.keys(agentConfigurationOptions.output_modes) : []);
|
||||
setDisplayOutputModes(
|
||||
agentConfigurationOptions?.output_modes
|
||||
? Object.keys(agentConfigurationOptions.output_modes)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
if (agentData.name?.toLowerCase() === "khoj" || agentData.is_hidden === true) {
|
||||
@@ -367,8 +405,12 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
const promptChanged = !!customPrompt && customPrompt !== agentData.persona;
|
||||
|
||||
// Order independent check to ensure input tools or output modes haven't been changed.
|
||||
const toolsChanged = JSON.stringify(inputTools?.sort() || []) !== JSON.stringify(agentData.input_tools?.sort());
|
||||
const modesChanged = JSON.stringify(outputModes?.sort() || []) !== JSON.stringify(agentData.output_modes?.sort());
|
||||
const toolsChanged =
|
||||
JSON.stringify(inputTools?.sort() || []) !==
|
||||
JSON.stringify(agentData.input_tools?.sort());
|
||||
const modesChanged =
|
||||
JSON.stringify(outputModes?.sort() || []) !==
|
||||
JSON.stringify(agentData.output_modes?.sort());
|
||||
|
||||
setHasModified(modelChanged || promptChanged || toolsChanged || modesChanged);
|
||||
|
||||
@@ -394,7 +436,9 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
function handleSave() {
|
||||
if (hasModified) {
|
||||
if (!isDefaultAgent && agentData?.is_hidden === false) {
|
||||
alert("This agent is not a hidden agent. It cannot be modified from this interface.");
|
||||
alert(
|
||||
"This agent is not a hidden agent. It cannot be modified from this interface.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -409,12 +453,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
chat_model: selectedModel,
|
||||
input_tools: inputTools,
|
||||
output_modes: outputModes,
|
||||
...(isDefaultAgent ? {} : { slug: agentData?.slug })
|
||||
...(isDefaultAgent ? {} : { slug: agentData?.slug }),
|
||||
};
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
const url = !isDefaultAgent ? `/api/agents/hidden` : `/api/agents/hidden?conversation_id=${props.conversationId}`;
|
||||
const url = !isDefaultAgent
|
||||
? `/api/agents/hidden`
|
||||
: `/api/agents/hidden?conversation_id=${props.conversationId}`;
|
||||
|
||||
// There are four scenarios here.
|
||||
// 1. If the agent is a default agent, then we need to create a new agent just to associate with this conversation.
|
||||
@@ -424,13 +470,13 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
fetch(url, {
|
||||
method: mode,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((res) => {
|
||||
setIsSaving(false);
|
||||
res.json()
|
||||
res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
mutate(`/api/agents/conversation?conversation_id=${props.conversationId}`);
|
||||
@@ -456,43 +502,47 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className={`ml-auto opacity-30 rounded-lg p-2 transition-all transform duration-300 ease-in-out
|
||||
${props.isOpen
|
||||
? "translate-x-0 opacity-100 w-[300px] relative"
|
||||
: "translate-x-full opacity-100 w-0 p-0 m-0"}
|
||||
${
|
||||
props.isOpen
|
||||
? "translate-x-0 opacity-100 w-[300px] relative"
|
||||
: "translate-x-full opacity-100 w-0 p-0 m-0"
|
||||
}
|
||||
`}
|
||||
variant="floating">
|
||||
variant="floating"
|
||||
>
|
||||
<SidebarContent>
|
||||
<SidebarHeader>
|
||||
{
|
||||
agentData && !isEditable ? (
|
||||
<div className="flex items-center relative text-sm">
|
||||
<a className="text-lg font-bold flex flex-row items-center" href={`/agents?agent=${agentData.slug}`}>
|
||||
{getIconFromIconName(agentData.icon, agentData.color)}
|
||||
{agentData.name}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center relative text-sm justify-between">
|
||||
<p>
|
||||
Chat Options
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{agentData && !isEditable ? (
|
||||
<div className="flex items-center relative text-sm">
|
||||
<a
|
||||
className="text-lg font-bold flex flex-row items-center"
|
||||
href={`/agents?agent=${agentData.slug}`}
|
||||
>
|
||||
{getIconFromIconName(agentData.icon, agentData.color)}
|
||||
{agentData.name}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center relative text-sm justify-between">
|
||||
<p>Chat Options</p>
|
||||
</div>
|
||||
)}
|
||||
</SidebarHeader>
|
||||
<SidebarGroup key={"knowledge"} className="border-b last:border-none">
|
||||
<SidebarGroupContent className="gap-0">
|
||||
<SidebarMenu className="p-0 m-0">
|
||||
{
|
||||
agentData && agentData.has_files ? (
|
||||
<SidebarMenuItem key={"agent_knowledge"} className="list-none">
|
||||
<div className="flex items-center space-x-2 rounded-full">
|
||||
<div className="text-muted-foreground"><Sparkle /></div>
|
||||
<div className="text-muted-foreground text-sm">Using custom knowledge base</div>
|
||||
{agentData && agentData.has_files ? (
|
||||
<SidebarMenuItem key={"agent_knowledge"} className="list-none">
|
||||
<div className="flex items-center space-x-2 rounded-full">
|
||||
<div className="text-muted-foreground">
|
||||
<Sparkle />
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
) : null
|
||||
}
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Using custom knowledge base
|
||||
</div>
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
) : null}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
@@ -506,39 +556,41 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
value={customPrompt || ""}
|
||||
onChange={(e) => handleCustomPromptChange(e.target.value)}
|
||||
readOnly={!isEditable}
|
||||
disabled={!isEditable} />
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
{
|
||||
!agentDataLoading && agentData && (
|
||||
<SidebarGroup key={"model"}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarGroupLabel>
|
||||
Model
|
||||
{
|
||||
!isSubscribed && (
|
||||
<a href="/settings" className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg">
|
||||
Upgrade
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarMenu className="p-0 m-0">
|
||||
<SidebarMenuItem key={"model"} className="list-none">
|
||||
<ModelSelector
|
||||
disabled={!isEditable}
|
||||
onSelect={(model) => handleModelSelect(model.name)}
|
||||
initialModel={isDefaultAgent ? undefined : agentData?.chat_model}
|
||||
isActive={props.isActive}
|
||||
/>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
{!agentDataLoading && agentData && (
|
||||
<SidebarGroup key={"model"}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarGroupLabel>
|
||||
Model
|
||||
{!isSubscribed && (
|
||||
<a
|
||||
href="/settings"
|
||||
className="hover:font-bold text-accent-foreground m-2 bg-accent bg-opacity-10 p-1 rounded-lg"
|
||||
>
|
||||
Upgrade
|
||||
</a>
|
||||
)}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarMenu className="p-0 m-0">
|
||||
<SidebarMenuItem key={"model"} className="list-none">
|
||||
<ModelSelector
|
||||
disabled={!isEditable}
|
||||
onSelect={(model) => handleModelSelect(model.name)}
|
||||
initialModel={
|
||||
isDefaultAgent ? undefined : agentData?.chat_model
|
||||
}
|
||||
isActive={props.isActive}
|
||||
/>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
<Popover defaultOpen={false}>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel asChild>
|
||||
@@ -550,82 +602,118 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
<PopoverContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="p-1 m-0">
|
||||
{
|
||||
Object.entries(agentConfigurationOptions?.input_tools ?? {}).map(([key, value]) => {
|
||||
return (
|
||||
<SidebarMenuItem key={key} className="list-none">
|
||||
<Tooltip>
|
||||
<TooltipTrigger key={key} asChild>
|
||||
<div className="flex items-center space-x-2 py-1 justify-between">
|
||||
<Label htmlFor={key} className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer">
|
||||
{getIconForSlashCommand(key)}
|
||||
<p className="text-sm my-auto flex items-center">
|
||||
{key}
|
||||
</p>
|
||||
</Label>
|
||||
<Checkbox
|
||||
id={key}
|
||||
className={`${isEditable ? "cursor-pointer" : ""}`}
|
||||
checked={isValueChecked(key, displayInputTools ?? [])}
|
||||
onCheckedChange={() => {
|
||||
let updatedInputTools = handleCheckToggle(key, displayInputTools ?? [])
|
||||
setInputTools(updatedInputTools);
|
||||
setDisplayInputTools(updatedInputTools);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
{Object.entries(
|
||||
agentConfigurationOptions?.input_tools ?? {},
|
||||
).map(([key, value]) => {
|
||||
return (
|
||||
<SidebarMenuItem key={key} className="list-none">
|
||||
<Tooltip>
|
||||
<TooltipTrigger key={key} asChild>
|
||||
<div className="flex items-center space-x-2 py-1 justify-between">
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className="flex items-center gap-2 text-accent-foreground p-1 cursor-pointer"
|
||||
>
|
||||
{getIconForSlashCommand(key)}
|
||||
<p className="text-sm my-auto flex items-center">
|
||||
{key}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg">
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
{
|
||||
Object.entries(agentConfigurationOptions?.output_modes ?? {}).map(([key, value]) => {
|
||||
return (
|
||||
<SidebarMenuItem key={key} className="list-none">
|
||||
<Tooltip>
|
||||
<TooltipTrigger key={key} asChild>
|
||||
<div className="flex items-center space-x-2 py-1 justify-between">
|
||||
<Label htmlFor={key} className="flex items-center gap-2 p-1 rounded-lg cursor-pointer">
|
||||
{getIconForSlashCommand(key)}
|
||||
<p className="text-sm my-auto flex items-center">
|
||||
{key}
|
||||
</p>
|
||||
</Label>
|
||||
<Checkbox
|
||||
id={key}
|
||||
className={`${isEditable ? "cursor-pointer" : ""}`}
|
||||
checked={isValueChecked(key, displayOutputModes ?? [])}
|
||||
onCheckedChange={() => {
|
||||
let updatedOutputModes = handleCheckToggle(key, displayOutputModes ?? [])
|
||||
setOutputModes(updatedOutputModes);
|
||||
setDisplayOutputModes(updatedOutputModes);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
</p>
|
||||
</Label>
|
||||
<Checkbox
|
||||
id={key}
|
||||
className={`${isEditable ? "cursor-pointer" : ""}`}
|
||||
checked={isValueChecked(
|
||||
key,
|
||||
displayInputTools ?? [],
|
||||
)}
|
||||
onCheckedChange={() => {
|
||||
let updatedInputTools =
|
||||
handleCheckToggle(
|
||||
key,
|
||||
displayInputTools ?? [],
|
||||
);
|
||||
setInputTools(
|
||||
updatedInputTools,
|
||||
);
|
||||
setDisplayInputTools(
|
||||
updatedInputTools,
|
||||
);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
side="left"
|
||||
align="start"
|
||||
className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"
|
||||
>
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
{Object.entries(
|
||||
agentConfigurationOptions?.output_modes ?? {},
|
||||
).map(([key, value]) => {
|
||||
return (
|
||||
<SidebarMenuItem key={key} className="list-none">
|
||||
<Tooltip>
|
||||
<TooltipTrigger key={key} asChild>
|
||||
<div className="flex items-center space-x-2 py-1 justify-between">
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className="flex items-center gap-2 p-1 rounded-lg cursor-pointer"
|
||||
>
|
||||
{getIconForSlashCommand(key)}
|
||||
<p className="text-sm my-auto flex items-center">
|
||||
{key}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={5} side="left" align="start" className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg">
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</Label>
|
||||
<Checkbox
|
||||
id={key}
|
||||
className={`${isEditable ? "cursor-pointer" : ""}`}
|
||||
checked={isValueChecked(
|
||||
key,
|
||||
displayOutputModes ?? [],
|
||||
)}
|
||||
onCheckedChange={() => {
|
||||
let updatedOutputModes =
|
||||
handleCheckToggle(
|
||||
key,
|
||||
displayOutputModes ??
|
||||
[],
|
||||
);
|
||||
setOutputModes(
|
||||
updatedOutputModes,
|
||||
);
|
||||
setDisplayOutputModes(
|
||||
updatedOutputModes,
|
||||
);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
side="left"
|
||||
align="start"
|
||||
className="text-sm bg-background text-foreground shadow-sm border border-slate-500 border-opacity-20 p-2 rounded-lg"
|
||||
>
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
|
||||
</SidebarGroupContent>
|
||||
</PopoverContent>
|
||||
</SidebarGroup>
|
||||
@@ -645,79 +733,75 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
{
|
||||
props.isOpen && (
|
||||
<SidebarFooter key={"actions"}>
|
||||
<SidebarMenu className="p-0 m-0">
|
||||
|
||||
{
|
||||
(agentData && !isEditable && agentData.is_creator) ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant={"ghost"}
|
||||
onClick={() => window.location.href = `/agents?agent=${agentData?.slug}`}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) :
|
||||
<>
|
||||
{
|
||||
!hasModified && isEditable && customPrompt && !isDefaultAgent && selectedModel && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<AgentCreationForm
|
||||
customPrompt={customPrompt}
|
||||
selectedModel={selectedModel}
|
||||
inputTools={displayInputTools ?? []}
|
||||
outputModes={displayOutputModes ?? []}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
{props.isOpen && (
|
||||
<SidebarFooter key={"actions"}>
|
||||
<SidebarMenu className="p-0 m-0">
|
||||
{agentData && !isEditable && agentData.is_creator ? (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant={"ghost"}
|
||||
onClick={() =>
|
||||
(window.location.href = `/agents?agent=${agentData?.slug}`)
|
||||
}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
) : (
|
||||
<>
|
||||
{!hasModified &&
|
||||
isEditable &&
|
||||
customPrompt &&
|
||||
!isDefaultAgent &&
|
||||
selectedModel && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleReset()}
|
||||
variant={"ghost"}
|
||||
disabled={!isEditable || !hasModified}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<AgentCreationForm
|
||||
customPrompt={customPrompt}
|
||||
selectedModel={selectedModel}
|
||||
inputTools={displayInputTools ?? []}
|
||||
outputModes={displayOutputModes ?? []}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`}
|
||||
variant={"secondary"}
|
||||
onClick={() => handleSave()}
|
||||
disabled={!isEditable || !hasModified || isSaving}
|
||||
>
|
||||
{
|
||||
isSaving ?
|
||||
<CircleNotch className="animate-spin" />
|
||||
:
|
||||
<ArrowsDownUp />
|
||||
}
|
||||
{
|
||||
isSaving ? "Saving" : "Save"
|
||||
}
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
</>
|
||||
}
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleReset()}
|
||||
variant={"ghost"}
|
||||
disabled={!isEditable || !hasModified}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
className={`w-full ${hasModified ? "bg-accent-foreground text-accent" : ""}`}
|
||||
variant={"secondary"}
|
||||
onClick={() => handleSave()}
|
||||
disabled={!isEditable || !hasModified || isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<CircleNotch className="animate-spin" />
|
||||
) : (
|
||||
<ArrowsDownUp />
|
||||
)}
|
||||
{isSaving ? "Saving" : "Save"}
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
</Sidebar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
53
src/interface/web/app/components/deprecationBanner.tsx
Normal file
53
src/interface/web/app/components/deprecationBanner.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { X, Warning } from "@phosphor-icons/react";
|
||||
import { useUserConfig } from "@/app/common/auth";
|
||||
|
||||
const DISMISS_KEY = "khoj-cloud-deprecation-dismissed";
|
||||
|
||||
export function DeprecationBanner() {
|
||||
const [isDismissed, setIsDismissed] = useState(true);
|
||||
const { data: userConfig } = useUserConfig(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDismissed(localStorage.getItem(DISMISS_KEY) === "true");
|
||||
}, []);
|
||||
|
||||
function dismiss() {
|
||||
localStorage.setItem(DISMISS_KEY, "true");
|
||||
setIsDismissed(true);
|
||||
}
|
||||
|
||||
if (isDismissed || !userConfig?.billing_enabled) return null;
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 py-2.5 bg-orange-600/90 shadow-sm flex items-center justify-center gap-2 text-sm text-orange-50 z-0 relative">
|
||||
<Warning className="h-4 w-4 shrink-0 text-orange-200" weight="bold" />
|
||||
<p>
|
||||
<strong>Khoj Cloud is being deprecated on April 15, 2026.</strong>{" "}
|
||||
Please{" "}
|
||||
<Link href="/settings#account" className="underline font-medium hover:text-white">
|
||||
export your data
|
||||
</Link>
|
||||
{" "}before then. To continue using Khoj, you can{" "}
|
||||
<a
|
||||
href="https://docs.khoj.dev/get-started/setup"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline font-medium hover:text-white"
|
||||
>
|
||||
self-host it
|
||||
</a>.
|
||||
</p>
|
||||
<button
|
||||
onClick={dismiss}
|
||||
className="ml-2 p-0.5 rounded hover:bg-orange-700 shrink-0"
|
||||
aria-label="Dismiss banner"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import Link from "next/link";
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
import { AppSidebar } from "../appSidebar/appSidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@@ -23,9 +24,9 @@ export default function Loading(props: LoadingProps) {
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h2 className="text-lg">Ask Anything</h2>
|
||||
)}
|
||||
|
||||
@@ -147,9 +147,7 @@ const Mermaid: React.FC<MermaidProps> = ({ chart }) => {
|
||||
<span>{mermaidError}</span>
|
||||
</div>
|
||||
<code className="block bg-secondary text-secondary-foreground p-4 mt-3 rounded-lg font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-[400px] border border-gray-200">
|
||||
{
|
||||
chart
|
||||
}
|
||||
{chart}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -12,7 +12,15 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Moon, Sun, UserCircle, Question, ArrowRight, Code, BuildingOffice } from "@phosphor-icons/react";
|
||||
import {
|
||||
Moon,
|
||||
Sun,
|
||||
UserCircle,
|
||||
Question,
|
||||
ArrowRight,
|
||||
Code,
|
||||
BuildingOffice,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useIsDarkMode, useIsMobileWidth } from "@/app/common/utils";
|
||||
import LoginPrompt from "../loginPrompt/loginPrompt";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -69,7 +77,7 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
|
||||
icon: <BuildingOffice className="w-6 h-6" />,
|
||||
link: "https://khoj.dev/teams",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<SidebarMenu className="border-none p-0 m-0">
|
||||
@@ -131,18 +139,16 @@ export default function FooterMenu({ sideBarIsOpen }: NavMenuProps) {
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{
|
||||
menuItems.map((menuItem, index) => (
|
||||
<DropdownMenuItem key={index}>
|
||||
<Link href={menuItem.link} className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
{menuItem.icon}
|
||||
<p className="ml-3 font-semibold">{menuItem.title}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
{menuItems.map((menuItem, index) => (
|
||||
<DropdownMenuItem key={index}>
|
||||
<Link href={menuItem.link} className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
{menuItem.icon}
|
||||
<p className="ml-3 font-semibold">{menuItem.title}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{!userData ? (
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useIsDarkMode } from '@/app/common/utils'
|
||||
import { useIsDarkMode } from "@/app/common/utils";
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [darkMode, setDarkMode] = useIsDarkMode();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ArrowCircleDown, ArrowRight, Code, Note } from "@phosphor-icons/react";
|
||||
import { ArrowCircleDown, ArrowRight, Code, Note, Clipboard, Check } from "@phosphor-icons/react";
|
||||
|
||||
import markdownIt from "markdown-it";
|
||||
const md = new markdownIt({
|
||||
@@ -32,6 +32,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||
import DOMPurify from "dompurify";
|
||||
import { getIconFromFilename } from "@/app/common/iconUtils";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface NotesContextReferenceData {
|
||||
title: string;
|
||||
@@ -467,9 +468,12 @@ export function constructAllReferences(
|
||||
if (contextData) {
|
||||
let localContextReferences = contextData.map((context) => {
|
||||
if (!context.compiled && context.compiled !== "") {
|
||||
const fileContent = context as unknown as string;
|
||||
const title = fileContent.split("\n")[0];
|
||||
const content = fileContent.split("\n").slice(1).join("\n");
|
||||
const raw = context as unknown;
|
||||
const fileContent = typeof raw === "string" ? raw : raw == null ? "" : String(raw);
|
||||
|
||||
const lines = fileContent.split("\n");
|
||||
const title = lines[0] && lines[0].trim() ? lines[0] : "(untitled)";
|
||||
const content = lines.slice(1).join("\n");
|
||||
return {
|
||||
title: title,
|
||||
content: content,
|
||||
@@ -491,6 +495,18 @@ export function constructAllReferences(
|
||||
};
|
||||
}
|
||||
|
||||
export function formatReferencesAsMarkdown(
|
||||
notesReferenceCardData: NotesContextReferenceData[],
|
||||
onlineReferenceCardData: OnlineReferenceData[],
|
||||
codeReferenceCardData: CodeReferenceData[],
|
||||
): string {
|
||||
return [
|
||||
...notesReferenceCardData.map((note) => `- ${note.title}`),
|
||||
...onlineReferenceCardData.map((online) => `- [${online.title}](${online.link})`),
|
||||
...codeReferenceCardData.map((_, index) => `- Code Reference ${index + 1}`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
interface SimpleIconProps {
|
||||
type: string;
|
||||
link?: string;
|
||||
@@ -586,10 +602,20 @@ interface ReferencePanelDataProps {
|
||||
export default function ReferencePanel(props: ReferencePanelDataProps) {
|
||||
const [numTeaserSlots, setNumTeaserSlots] = useState(3);
|
||||
|
||||
const [copyReferencesSuccess, setCopyReferencesSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setNumTeaserSlots(props.isMobileWidth ? 3 : 5);
|
||||
}, [props.isMobileWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copyReferencesSuccess) {
|
||||
setTimeout(() => {
|
||||
setCopyReferencesSuccess(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [copyReferencesSuccess]);
|
||||
|
||||
if (!props.notesReferenceCardData && !props.onlineReferenceCardData) {
|
||||
return null;
|
||||
}
|
||||
@@ -606,6 +632,17 @@ export default function ReferencePanel(props: ReferencePanelDataProps) {
|
||||
.slice(0, numTeaserSlots - codeDataToShow.length - notesDataToShow.length)
|
||||
: [];
|
||||
|
||||
const copyReferencesToClipboard = () => {
|
||||
navigator.clipboard.writeText(
|
||||
formatReferencesAsMarkdown(
|
||||
props.notesReferenceCardData,
|
||||
props.onlineReferenceCardData,
|
||||
props.codeReferenceCardData,
|
||||
),
|
||||
);
|
||||
setCopyReferencesSuccess(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger className="text-balance w-auto justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center m-0 inline-flex">
|
||||
@@ -642,6 +679,23 @@ export default function ReferencePanel(props: ReferencePanelDataProps) {
|
||||
<SheetHeader>
|
||||
<SheetTitle>References</SheetTitle>
|
||||
<SheetDescription>View all references for this response</SheetDescription>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={copyReferencesToClipboard}
|
||||
className="mt-4"
|
||||
>
|
||||
{copyReferencesSuccess ? (
|
||||
<>
|
||||
<Check className="mr-2 text-green-500" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className="mr-2" />
|
||||
Copy References
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-wrap gap-2 w-auto mt-2">
|
||||
{props.codeReferenceCardData.map((code, index) => {
|
||||
|
||||
90
src/interface/web/app/components/userMemory/userMemory.tsx
Normal file
90
src/interface/web/app/components/userMemory/userMemory.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Pencil, TrashSimple, FloppyDisk, X } from "@phosphor-icons/react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
export interface UserMemorySchema {
|
||||
id: number;
|
||||
raw: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface UserMemoryProps {
|
||||
memory: UserMemorySchema;
|
||||
onDelete: (id: number) => void;
|
||||
onUpdate: (id: number, raw: string) => void;
|
||||
}
|
||||
|
||||
export function UserMemory({ memory, onDelete, onUpdate }: UserMemoryProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [content, setContent] = useState(memory.raw);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleUpdate = () => {
|
||||
onUpdate(memory.id, content);
|
||||
setIsEditing(false);
|
||||
toast({
|
||||
title: "Memory Updated",
|
||||
description: "Your memory has been successfully updated.",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
onDelete(memory.id);
|
||||
toast({
|
||||
title: "Memory Deleted",
|
||||
description: "Your memory has been successfully deleted.",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Input
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleUpdate}
|
||||
title="Save"
|
||||
>
|
||||
<FloppyDisk className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsEditing(false)}
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input value={memory.raw} readOnly className="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsEditing(true)}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDelete}
|
||||
title="Delete"
|
||||
>
|
||||
<TrashSimple className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
366
src/interface/web/app/globals-print.css
Normal file
366
src/interface/web/app/globals-print.css
Normal file
@@ -0,0 +1,366 @@
|
||||
/* Hide print-only elements on screen */
|
||||
.print-only-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Print-specific styles for clean PDF export */
|
||||
@media print {
|
||||
/* Show print-only header */
|
||||
.print-only-header {
|
||||
display: block !important;
|
||||
margin-bottom: 2rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.print-header-content {
|
||||
display: flex !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 1.5rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.print-header-left {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.print-logo {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
fill: #000 !important;
|
||||
}
|
||||
|
||||
.print-header-right {
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
.print-only-header h1 {
|
||||
font-size: 28pt !important;
|
||||
font-weight: bold !important;
|
||||
color: #000 !important;
|
||||
margin: 0 0 0.75rem 0 !important;
|
||||
line-height: 1.2 !important;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.conversation-meta {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 0.25rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.conversation-meta p {
|
||||
margin: 0 !important;
|
||||
font-size: 14pt !important;
|
||||
color: #000 !important;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
.conversation-meta strong {
|
||||
font-weight: bold !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.print-only-header hr {
|
||||
border: none !important;
|
||||
height: 2px !important;
|
||||
background-color: #000 !important;
|
||||
margin: 1rem 0 0 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
/* Hide non-essential elements */
|
||||
.sidebar,
|
||||
.sidebar-trigger,
|
||||
.sidebar-inset > header,
|
||||
.print-hidden,
|
||||
button,
|
||||
nav,
|
||||
[data-sidebar],
|
||||
[data-sidebar-trigger],
|
||||
.chat-sidebar,
|
||||
.app-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reset page margins and layout */
|
||||
@page {
|
||||
margin: 0.5in;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
/* Main layout adjustments for print */
|
||||
body {
|
||||
font-size: 12pt !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #000 !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
/* Remove background colors and shadows */
|
||||
* {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Remove any height constraints that could limit content */
|
||||
div,
|
||||
section,
|
||||
main,
|
||||
article,
|
||||
aside {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
/* Specific height fixes for flex containers */
|
||||
[style*="height"],
|
||||
[style*="max-height"] {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
/* Ensure all content is visible - target all scroll containers */
|
||||
.scroll-area,
|
||||
.scroll-area > div,
|
||||
[data-radix-scroll-area-viewport],
|
||||
[data-radix-scroll-area-scrollbar],
|
||||
[data-radix-scroll-area-content] {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
/* Chat history styling */
|
||||
.chat-history,
|
||||
.chat-history > *,
|
||||
.chat-body,
|
||||
.chat-body-full {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
/* Ensure chat messages container expands fully */
|
||||
.chat-messages,
|
||||
.chat-messages-container,
|
||||
.messages-container {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Make chat messages use full width in print */
|
||||
.w-4\/6,
|
||||
.w-2\/3,
|
||||
.w-3\/4,
|
||||
.max-w-2xl,
|
||||
.max-w-3xl,
|
||||
.max-w-4xl,
|
||||
.max-w-5xl {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Message styling for print */
|
||||
.chat-message {
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 1rem !important;
|
||||
padding: 0.5rem !important;
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Ensure message containers use full width */
|
||||
.chat-message-container,
|
||||
.chat-message-wrapper {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Title styling */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
color: #000 !important;
|
||||
font-weight: bold !important;
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
pre,
|
||||
code {
|
||||
font-family: "Courier New", monospace !important;
|
||||
font-size: 0.85em !important;
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word !important;
|
||||
border: 1px solid #ccc !important;
|
||||
padding: 0.5rem !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
background: #f9f9f9 !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* Link styling */
|
||||
a {
|
||||
color: #000 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* Image handling */
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/* Force black text color */
|
||||
p,
|
||||
div,
|
||||
span,
|
||||
li,
|
||||
td,
|
||||
th {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ol,
|
||||
ul {
|
||||
margin-left: 1rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
table {
|
||||
border-collapse: collapse !important;
|
||||
width: 100% !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #000 !important;
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
margin-bottom: 0.5rem !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing */
|
||||
.chat-message + .chat-message {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
/* Hide scroll indicators */
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide interactive elements */
|
||||
.retry-button,
|
||||
.action-button,
|
||||
.chat-footer,
|
||||
.chat-buttons,
|
||||
.code-copy-button,
|
||||
.copy-button,
|
||||
.feedback-buttons {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Train of thought styling for print */
|
||||
.train-of-thought {
|
||||
border-left: 2px solid #ccc !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.5rem !important;
|
||||
font-size: 0.9em !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.train-of-thought strong {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.train-of-thought.primary {
|
||||
border-left-color: #000 !important;
|
||||
}
|
||||
|
||||
.train-of-thought-element {
|
||||
display: block !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Chat message container styling */
|
||||
.chat-message-container {
|
||||
background: transparent !important;
|
||||
border: 1px solid #ccc !important;
|
||||
border-radius: 8px !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.5rem !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.chat-message-wrapper {
|
||||
padding-left: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.you {
|
||||
background-color: #f5f5f5 !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.khoj {
|
||||
background-color: transparent !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #666 !important;
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
/* Image containers */
|
||||
.images-container {
|
||||
display: block !important;
|
||||
overflow: visible !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 4in !important;
|
||||
object-fit: contain !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/* Agent indicators */
|
||||
.agent-indicator {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { noto_sans, noto_sans_arabic } from "@/app/fonts";
|
||||
import "./globals.css";
|
||||
import "./globals-print.css";
|
||||
import { ContentSecurityPolicy } from "./common/layoutHelper";
|
||||
import { ThemeProvider } from "./components/providers/themeProvider";
|
||||
|
||||
|
||||
@@ -40,11 +40,13 @@ import { AgentData } from "@/app/components/agentCard/agentCard";
|
||||
import { createNewConversation } from "./common/chatFunctions";
|
||||
import { useDebounce, useIsMobileWidth } from "./common/utils";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { AgentCard } from "@/app/components/agentCard/agentCard";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import LoginPopup from "./components/loginPrompt/loginPopup";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { DeprecationBanner } from "@/app/components/deprecationBanner";
|
||||
import { AppSidebar } from "./components/appSidebar/appSidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { KhojLogoType } from "./components/logo/khojLogo";
|
||||
@@ -566,13 +568,14 @@ export default function Home() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar conversationId={conversationId} />
|
||||
<SidebarInset>
|
||||
<DeprecationBanner />
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h2 className="text-lg">Ask Anything</h2>
|
||||
)}
|
||||
|
||||
@@ -508,7 +508,7 @@ function FileFilterComboBox(props: FileFilterComboBoxProps) {
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Popover open={open || (noMatchingFiles && (!!inputText))} onOpenChange={setOpen}>
|
||||
<Popover open={open || (noMatchingFiles && !!inputText)} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -521,14 +521,18 @@ function FileFilterComboBox(props: FileFilterComboBoxProps) {
|
||||
? "✔️"
|
||||
: "Selected"
|
||||
: props.isMobileWidth
|
||||
? " "
|
||||
: "Select file"}
|
||||
? " "
|
||||
: "Select file"}
|
||||
<Funnel className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search files..." value={inputText} onInput={(e) => setInputText(e.currentTarget.value)} />
|
||||
<CommandInput
|
||||
placeholder="Search files..."
|
||||
value={inputText}
|
||||
onInput={(e) => setInputText(e.currentTarget.value)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No files found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
@@ -614,7 +618,6 @@ export default function Search() {
|
||||
setSelectedFileFilter("INITIALIZE");
|
||||
}
|
||||
}
|
||||
|
||||
}, [searchQuery]);
|
||||
|
||||
function handleSearchInputChange(value: string) {
|
||||
@@ -775,9 +778,9 @@ export default function Search() {
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h2 className="text-lg">Search Your Knowledge Base</h2>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import styles from "./settings.module.css";
|
||||
import "intl-tel-input/styles";
|
||||
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
import { useUserConfig, ModelOptions, UserConfig, SubscriptionStates } from "../common/auth";
|
||||
import { useUserConfig, ModelOptions, UserConfig, SubscriptionStates, isUserSubscribed } from "../common/auth";
|
||||
import { toTitleCase, useIsMobileWidth } from "../common/utils";
|
||||
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
@@ -15,6 +16,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -23,9 +26,25 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
AlertDialog, AlertDialogAction, AlertDialogCancel,
|
||||
AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||
|
||||
import {
|
||||
@@ -66,13 +85,15 @@ import Loading from "../components/loading/loading";
|
||||
|
||||
import IntlTelInput from "intl-tel-input/react";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { DeprecationBanner } from "@/app/components/deprecationBanner";
|
||||
import { AppSidebar } from "../components/appSidebar/appSidebar";
|
||||
import { UserMemory, UserMemorySchema } from "../components/userMemory/userMemory";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { KhojLogoType } from "../components/logo/khojLogo";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
import JSZip from "jszip";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
interface DropdownComponentProps {
|
||||
items: ModelOptions[];
|
||||
@@ -81,7 +102,12 @@ interface DropdownComponentProps {
|
||||
callbackFunc: (value: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected, isActive, callbackFunc }) => {
|
||||
const DropdownComponent: React.FC<DropdownComponentProps> = ({
|
||||
items,
|
||||
selected,
|
||||
isActive,
|
||||
callbackFunc,
|
||||
}) => {
|
||||
const [position, setPosition] = useState(selected?.toString() ?? "0");
|
||||
|
||||
return (
|
||||
@@ -114,7 +140,10 @@ const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected,
|
||||
value={item.id.toString()}
|
||||
disabled={!isActive && item.tier !== "free"}
|
||||
>
|
||||
{item.name} {item.tier === "standard" && <span className="text-green-500 ml-2">(Futurist)</span>}
|
||||
{item.name}{" "}
|
||||
{item.tier === "standard" && (
|
||||
<span className="text-green-500 ml-2">(Futurist)</span>
|
||||
)}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
@@ -308,6 +337,9 @@ export default function SettingsView() {
|
||||
const [numberValidationState, setNumberValidationState] = useState<PhoneNumberValidationState>(
|
||||
PhoneNumberValidationState.Verified,
|
||||
);
|
||||
const [memories, setMemories] = useState<UserMemorySchema[]>([]);
|
||||
const [enableMemory, setEnableMemory] = useState<boolean>(true);
|
||||
const [serverMemoryMode, setServerMemoryMode] = useState<string>("enabled_default_on");
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [exportProgress, setExportProgress] = useState(0);
|
||||
const [exportedConversations, setExportedConversations] = useState(0);
|
||||
@@ -327,11 +359,13 @@ export default function SettingsView() {
|
||||
initialUserConfig?.is_phone_number_verified
|
||||
? PhoneNumberValidationState.Verified
|
||||
: initialUserConfig?.phone_number
|
||||
? PhoneNumberValidationState.SendOTP
|
||||
: PhoneNumberValidationState.Setup,
|
||||
? PhoneNumberValidationState.SendOTP
|
||||
: PhoneNumberValidationState.Setup,
|
||||
);
|
||||
setName(initialUserConfig?.given_name);
|
||||
setNotionToken(initialUserConfig?.notion_token ?? null);
|
||||
setEnableMemory(initialUserConfig?.enable_memory ?? true);
|
||||
setServerMemoryMode(initialUserConfig?.server_memory_mode ?? "enabled_default_on");
|
||||
}, [initialUserConfig]);
|
||||
|
||||
const sendOTP = async () => {
|
||||
@@ -445,51 +479,6 @@ export default function SettingsView() {
|
||||
}
|
||||
};
|
||||
|
||||
const enableFreeTrial = async () => {
|
||||
const formatDate = (dateString: Date) => {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/subscription/trial`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to enable free trial");
|
||||
|
||||
const responseBody = await response.json();
|
||||
|
||||
// Set updated user settings
|
||||
if (responseBody.trial_enabled && userConfig) {
|
||||
let newUserConfig = userConfig;
|
||||
newUserConfig.subscription_state = SubscriptionStates.TRIAL;
|
||||
const renewalDate = new Date(
|
||||
Date.now() + userConfig.length_of_free_trial * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
newUserConfig.subscription_renewal_date = formatDate(renewalDate);
|
||||
newUserConfig.subscription_enabled_trial_at = new Date().toISOString();
|
||||
setUserConfig(newUserConfig);
|
||||
|
||||
// Notify user of free trial
|
||||
toast({
|
||||
title: "🎉 Trial Enabled",
|
||||
description: `Your free trial will end on ${newUserConfig.subscription_renewal_date}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error enabling free trial:", error);
|
||||
toast({
|
||||
title: "⚠️ Failed to Enable Free Trial",
|
||||
description:
|
||||
"Failed to enable free trial. Try again or contact us at team@khoj.dev",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveName = async () => {
|
||||
if (!name) return;
|
||||
try {
|
||||
@@ -524,13 +513,14 @@ export default function SettingsView() {
|
||||
|
||||
const updateModel = (modelType: string) => async (id: string) => {
|
||||
// Get the selected model from the options
|
||||
const modelOptions = modelType === "chat"
|
||||
? userConfig?.chat_model_options
|
||||
: modelType === "paint"
|
||||
? userConfig?.paint_model_options
|
||||
: userConfig?.voice_model_options;
|
||||
const modelOptions =
|
||||
modelType === "chat"
|
||||
? userConfig?.chat_model_options
|
||||
: modelType === "paint"
|
||||
? userConfig?.paint_model_options
|
||||
: userConfig?.voice_model_options;
|
||||
|
||||
const selectedModel = modelOptions?.find(model => model.id.toString() === id);
|
||||
const selectedModel = modelOptions?.find((model) => model.id.toString() === id);
|
||||
const modelName = selectedModel?.name;
|
||||
|
||||
// Check if the model is free tier or if the user is active
|
||||
@@ -551,7 +541,8 @@ export default function SettingsView() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Failed to switch ${modelType} model to ${modelName}`);
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to switch ${modelType} model to ${modelName}`);
|
||||
|
||||
toast({
|
||||
title: `✅ Switched ${modelType} model to ${modelName}`,
|
||||
@@ -570,7 +561,7 @@ export default function SettingsView() {
|
||||
setIsExporting(true);
|
||||
|
||||
// Get total conversation count
|
||||
const statsResponse = await fetch('/api/chat/stats');
|
||||
const statsResponse = await fetch("/api/chat/stats");
|
||||
const stats = await statsResponse.json();
|
||||
const total = stats.num_conversations;
|
||||
setTotalConversations(total);
|
||||
@@ -586,7 +577,7 @@ export default function SettingsView() {
|
||||
conversations.push(...data);
|
||||
|
||||
setExportedConversations((page + 1) * 10);
|
||||
setExportProgress(((page + 1) * 10 / total) * 100);
|
||||
setExportProgress((((page + 1) * 10) / total) * 100);
|
||||
}
|
||||
|
||||
// Add conversations to zip
|
||||
@@ -605,7 +596,7 @@ export default function SettingsView() {
|
||||
toast({
|
||||
title: "Export Failed",
|
||||
description: "Failed to export chats. Please try again.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
@@ -649,6 +640,88 @@ export default function SettingsView() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMemories = async () => {
|
||||
try {
|
||||
console.log("Fetching memories...");
|
||||
const response = await fetch('/api/memories');
|
||||
if (!response.ok) throw new Error('Failed to fetch memories');
|
||||
const data = await response.json();
|
||||
setMemories(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching memories:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to fetch memories. Please try again.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMemory = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/memories/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete memory');
|
||||
setMemories(memories.filter(memory => memory.id !== id));
|
||||
} catch (error) {
|
||||
console.error('Error deleting memory:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to delete memory. Please try again.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateMemory = async (id: number, raw: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/memories/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ raw, memory_id: id }),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update memory');
|
||||
const updatedMemory: UserMemorySchema = await response.json();
|
||||
setMemories(memories.map(memory =>
|
||||
memory.id === id ? updatedMemory : memory
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('Error updating memory:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update memory. Please try again.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleMemory = async (enabled: boolean) => {
|
||||
try {
|
||||
const response = await fetch(`/api/user/memory?enable_memory=${enabled}`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update memory setting');
|
||||
setEnableMemory(enabled);
|
||||
toast({
|
||||
title: enabled ? "Memory enabled" : "Memory disabled",
|
||||
description: enabled
|
||||
? "Khoj will learn and remember from your conversations."
|
||||
: "Khoj will no longer learn or remember from your conversations.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling memory:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update memory setting. Please try again.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const syncContent = async (type: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/content?t=${type}`, {
|
||||
@@ -724,13 +797,14 @@ export default function SettingsView() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar conversationId={""} />
|
||||
<SidebarInset>
|
||||
<DeprecationBanner />
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h2 className="text-lg">Settings</h2>
|
||||
)}
|
||||
@@ -775,6 +849,7 @@ export default function SettingsView() {
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{isUserSubscribed(userConfig) && (
|
||||
<Card id="subscription" className={cardClassName}>
|
||||
<CardHeader className="text-xl flex flex-row">
|
||||
<CreditCard className="h-7 w-7 mr-2" />
|
||||
@@ -808,94 +883,66 @@ export default function SettingsView() {
|
||||
)) ||
|
||||
(userConfig.subscription_state ===
|
||||
"subscribed" && (
|
||||
<>
|
||||
<p className="text-xl text-primary/80">
|
||||
Futurist
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
Subscription <b>renews</b> on{" "}
|
||||
<b>
|
||||
{
|
||||
userConfig.subscription_renewal_date
|
||||
}
|
||||
</b>
|
||||
</p>
|
||||
</>
|
||||
)) ||
|
||||
<>
|
||||
<p className="text-xl text-primary/80">
|
||||
Futurist
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
Subscription <b>renews</b> on{" "}
|
||||
<b>
|
||||
{
|
||||
userConfig.subscription_renewal_date
|
||||
}
|
||||
</b>
|
||||
</p>
|
||||
</>
|
||||
)) ||
|
||||
(userConfig.subscription_state ===
|
||||
"unsubscribed" && (
|
||||
<>
|
||||
<p className="text-xl">Futurist</p>
|
||||
<p className="text-gray-400">
|
||||
Subscription <b>ends</b> on{" "}
|
||||
<b>
|
||||
{
|
||||
userConfig.subscription_renewal_date
|
||||
}
|
||||
</b>
|
||||
</p>
|
||||
</>
|
||||
)) ||
|
||||
(userConfig.subscription_state ===
|
||||
"expired" && (
|
||||
<>
|
||||
<p className="text-xl">Humanist</p>
|
||||
{(userConfig.subscription_renewal_date && (
|
||||
<p className="text-gray-400">
|
||||
Subscription <b>expired</b>{" "}
|
||||
on{" "}
|
||||
<b>
|
||||
{
|
||||
userConfig.subscription_renewal_date
|
||||
}
|
||||
</b>
|
||||
</p>
|
||||
)) || (
|
||||
<p className="text-gray-400">
|
||||
Check{" "}
|
||||
<a
|
||||
href="https://khoj.dev/#pricing"
|
||||
target="_blank"
|
||||
>
|
||||
pricing page
|
||||
</a>{" "}
|
||||
to compare plans.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
<>
|
||||
<p className="text-xl">Futurist</p>
|
||||
<p className="text-gray-400">
|
||||
Subscription <b>ends</b> on{" "}
|
||||
<b>
|
||||
{
|
||||
userConfig.subscription_renewal_date
|
||||
}
|
||||
</b>
|
||||
</p>
|
||||
</>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-wrap gap-4">
|
||||
{(userConfig.subscription_state ==
|
||||
"subscribed" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hover:text-red-400"
|
||||
onClick={() =>
|
||||
setSubscription("cancel")
|
||||
}
|
||||
>
|
||||
<ArrowCircleDown className="h-5 w-5 mr-2" />
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)) ||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hover:text-red-400"
|
||||
onClick={() =>
|
||||
setSubscription("cancel")
|
||||
}
|
||||
>
|
||||
<ArrowCircleDown className="h-5 w-5 mr-2" />
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)) ||
|
||||
(userConfig.subscription_state ==
|
||||
"unsubscribed" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary/80 hover:text-primary"
|
||||
onClick={() =>
|
||||
setSubscription("resubscribe")
|
||||
}
|
||||
>
|
||||
<ArrowCircleUp
|
||||
weight="bold"
|
||||
className="h-5 w-5 mr-2"
|
||||
/>
|
||||
Resubscribe
|
||||
</Button>
|
||||
)) ||
|
||||
(userConfig.subscription_enabled_trial_at && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary/80 hover:text-primary"
|
||||
onClick={() =>
|
||||
setSubscription("resubscribe")
|
||||
}
|
||||
>
|
||||
<ArrowCircleUp
|
||||
weight="bold"
|
||||
className="h-5 w-5 mr-2"
|
||||
/>
|
||||
Resubscribe
|
||||
</Button>
|
||||
)) ||
|
||||
(
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary/80 hover:text-primary"
|
||||
@@ -913,21 +960,10 @@ export default function SettingsView() {
|
||||
/>
|
||||
Subscribe
|
||||
</Button>
|
||||
)) || (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary/80 hover:text-primary"
|
||||
onClick={enableFreeTrial}
|
||||
>
|
||||
<ArrowCircleUp
|
||||
weight="bold"
|
||||
className="h-5 w-5 mr-2"
|
||||
/>
|
||||
Enable Trial
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="section grid gap-8">
|
||||
@@ -984,16 +1020,16 @@ export default function SettingsView() {
|
||||
<Button variant="outline" size="sm">
|
||||
{(userConfig.enabled_content_source
|
||||
.github && (
|
||||
<>
|
||||
<Files className="h-5 w-5 inline mr-1" />
|
||||
Manage
|
||||
</>
|
||||
)) || (
|
||||
<>
|
||||
<Plugs className="h-5 w-5 inline mr-1" />
|
||||
Connect
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Files className="h-5 w-5 inline mr-1" />
|
||||
Manage
|
||||
</>
|
||||
)) || (
|
||||
<>
|
||||
<Plugs className="h-5 w-5 inline mr-1" />
|
||||
Connect
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -1035,8 +1071,8 @@ export default function SettingsView() {
|
||||
{
|
||||
/* Show connect to notion button if notion oauth url setup and user disconnected*/
|
||||
userConfig.notion_oauth_url &&
|
||||
!userConfig.enabled_content_source
|
||||
.notion ? (
|
||||
!userConfig.enabled_content_source
|
||||
.notion ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -1050,39 +1086,39 @@ export default function SettingsView() {
|
||||
Connect
|
||||
</Button>
|
||||
) : /* Show sync button if user connected to notion and API key unchanged */
|
||||
userConfig.enabled_content_source.notion &&
|
||||
notionToken ===
|
||||
userConfig.notion_token ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
syncContent("notion")
|
||||
}
|
||||
>
|
||||
<ArrowsClockwise className="h-5 w-5 inline mr-1" />
|
||||
Sync
|
||||
</Button>
|
||||
) : /* Show set API key button notion oauth url not set setup */
|
||||
!userConfig.notion_oauth_url ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={saveNotionToken}
|
||||
disabled={
|
||||
notionToken ===
|
||||
userConfig.notion_token
|
||||
}
|
||||
>
|
||||
<FloppyDisk className="h-5 w-5 inline mr-1" />
|
||||
{(userConfig.enabled_content_source
|
||||
.notion &&
|
||||
"Update API Key") ||
|
||||
"Set API Key"}
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
userConfig.enabled_content_source.notion &&
|
||||
notionToken ===
|
||||
userConfig.notion_token ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
syncContent("notion")
|
||||
}
|
||||
>
|
||||
<ArrowsClockwise className="h-5 w-5 inline mr-1" />
|
||||
Sync
|
||||
</Button>
|
||||
) : /* Show set API key button notion oauth url not set setup */
|
||||
!userConfig.notion_oauth_url ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={saveNotionToken}
|
||||
disabled={
|
||||
notionToken ===
|
||||
userConfig.notion_token
|
||||
}
|
||||
>
|
||||
<FloppyDisk className="h-5 w-5 inline mr-1" />
|
||||
{(userConfig.enabled_content_source
|
||||
.notion &&
|
||||
"Update API Key") ||
|
||||
"Set API Key"}
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -1123,7 +1159,10 @@ export default function SettingsView() {
|
||||
<CardFooter className="flex flex-wrap gap-4">
|
||||
{!userConfig.is_active && (
|
||||
<p className="text-gray-400">
|
||||
{userConfig.chat_model_options.some(model => model.tier === "free")
|
||||
{userConfig.chat_model_options.some(
|
||||
(model) =>
|
||||
model.tier === "free",
|
||||
)
|
||||
? "Free models available"
|
||||
: "Subscribe to switch model"}
|
||||
</p>
|
||||
@@ -1154,7 +1193,10 @@ export default function SettingsView() {
|
||||
<CardFooter className="flex flex-wrap gap-4">
|
||||
{!userConfig.is_active && (
|
||||
<p className="text-gray-400">
|
||||
{userConfig.paint_model_options.some(model => model.tier === "free")
|
||||
{userConfig.paint_model_options.some(
|
||||
(model) =>
|
||||
model.tier === "free",
|
||||
)
|
||||
? "Free models available"
|
||||
: "Subscribe to switch model"}
|
||||
</p>
|
||||
@@ -1185,7 +1227,10 @@ export default function SettingsView() {
|
||||
<CardFooter className="flex flex-wrap gap-4">
|
||||
{!userConfig.is_active && (
|
||||
<p className="text-gray-400">
|
||||
{userConfig.voice_model_options.some(model => model.tier === "free")
|
||||
{userConfig.voice_model_options.some(
|
||||
(model) =>
|
||||
model.tier === "free",
|
||||
)
|
||||
? "Free models available"
|
||||
: "Subscribe to switch model"}
|
||||
</p>
|
||||
@@ -1204,7 +1249,7 @@ export default function SettingsView() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="section grid gap-8">
|
||||
<div id="clients" className="text-2xl">
|
||||
<div id="account" className="text-2xl">
|
||||
Account
|
||||
</div>
|
||||
<div className="cards flex flex-wrap gap-16">
|
||||
@@ -1219,9 +1264,13 @@ export default function SettingsView() {
|
||||
</p>
|
||||
{exportProgress > 0 && (
|
||||
<div className="w-full mt-4">
|
||||
<Progress value={exportProgress} className="w-full" />
|
||||
<Progress
|
||||
value={exportProgress}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Exported {exportedConversations} of {totalConversations} conversations
|
||||
Exported {exportedConversations} of{" "}
|
||||
{totalConversations} conversations
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1233,11 +1282,70 @@ export default function SettingsView() {
|
||||
disabled={isExporting}
|
||||
>
|
||||
<Download className="h-5 w-5 mr-2" />
|
||||
{isExporting ? "Exporting..." : "Export Chats"}
|
||||
{isExporting
|
||||
? "Exporting..."
|
||||
: "Export Chats"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card className={cardClassName}>
|
||||
<CardHeader className="text-xl flex flex-row">
|
||||
<Brain className="h-7 w-7 mr-2" />
|
||||
Memories
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-hidden">
|
||||
<p className="pb-4 text-gray-400">
|
||||
View and manage your long-term memories
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="enable-memory"
|
||||
className={`text-sm font-medium leading-none ${serverMemoryMode === "disabled" ? "text-gray-400" : ""}`}
|
||||
>
|
||||
Enable Memory
|
||||
</label>
|
||||
<Switch
|
||||
id="enable-memory"
|
||||
checked={enableMemory}
|
||||
onCheckedChange={(checked) => handleToggleMemory(checked)}
|
||||
disabled={serverMemoryMode === "disabled"}
|
||||
/>
|
||||
</div>
|
||||
{serverMemoryMode === "disabled" && (
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
Memory has been disabled by the server administrator.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-wrap gap-4">
|
||||
<Dialog onOpenChange={(open) => open && fetchMemories()}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Brain className="h-5 w-5 mr-2" />
|
||||
Browse Memories
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Your Memories</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
{memories.map((memory) => (
|
||||
<UserMemory
|
||||
key={memory.id}
|
||||
memory={memory}
|
||||
onDelete={handleDeleteMemory}
|
||||
onUpdate={handleUpdateMemory}
|
||||
/>
|
||||
))}
|
||||
{memories.length === 0 && (
|
||||
<p className="text-center text-gray-500">No memories found</p>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className={cardClassName}>
|
||||
<CardHeader className="text-xl flex flex-row">
|
||||
<TrashSimple className="h-7 w-7 mr-2 text-red-500" />
|
||||
@@ -1245,7 +1353,11 @@ export default function SettingsView() {
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-hidden">
|
||||
<p className="pb-4 text-gray-400">
|
||||
This will delete all your account data, including conversations, agents, and any assets you{"'"}ve generated. Be sure to export before you do this if you want to keep your information.
|
||||
This will delete all your account data,
|
||||
including conversations, agents, and any
|
||||
assets you{"'"}ve generated. Be sure to
|
||||
export before you do this if you want to
|
||||
keep your information.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-wrap gap-4">
|
||||
@@ -1261,36 +1373,56 @@ export default function SettingsView() {
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
Are you absolutely sure?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action is irreversible. This will permanently delete your account
|
||||
and remove all your data from our servers.
|
||||
This action is irreversible.
|
||||
This will permanently delete
|
||||
your account and remove all your
|
||||
data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const response = await fetch('/api/self', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete account');
|
||||
const response =
|
||||
await fetch(
|
||||
"/api/self",
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
"Failed to delete account",
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "Account Deleted",
|
||||
description: "Your account has been successfully deleted.",
|
||||
description:
|
||||
"Your account has been successfully deleted.",
|
||||
});
|
||||
|
||||
// Redirect to home page after successful deletion
|
||||
window.location.href = "/";
|
||||
window.location.href =
|
||||
"/";
|
||||
} catch (error) {
|
||||
console.error('Error deleting account:', error);
|
||||
console.error(
|
||||
"Error deleting account:",
|
||||
error,
|
||||
);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to delete account. Please try again or contact support.",
|
||||
variant: "destructive"
|
||||
description:
|
||||
"Failed to delete account. Please try again or contact support.",
|
||||
variant:
|
||||
"destructive",
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import styles from "./sharedChat.module.css";
|
||||
import React, { Suspense, useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
import { StreamMessage } from "@/app/components/chatMessage/chatMessage";
|
||||
import { AgentData } from "@/app/components/agentCard/agentCard";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { DeprecationBanner } from "@/app/components/deprecationBanner";
|
||||
import { AppSidebar } from "@/app/components/appSidebar/appSidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { KhojLogoType } from "@/app/components/logo/khojLogo";
|
||||
@@ -232,6 +234,7 @@ export default function SharedChat() {
|
||||
<SidebarProvider>
|
||||
<AppSidebar conversationId={conversationId || ""} />
|
||||
<SidebarInset>
|
||||
<DeprecationBanner />
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
@@ -240,9 +243,9 @@ export default function SharedChat() {
|
||||
className={`${styles.chatTitleWrapper} text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mx-2 md:mr-8 col-auto h-fit`}
|
||||
>
|
||||
{isMobileWidth ? (
|
||||
<a className="p-0 no-underline" href="/">
|
||||
<Link className="p-0 no-underline" href="/">
|
||||
<KhojLogoType className="h-auto w-16" />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
title && (
|
||||
<>
|
||||
|
||||
1617
src/interface/web/bun.lock
Normal file
1617
src/interface/web/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,30 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-secondary-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-secondary-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-gray-500 data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
|
||||
@@ -1,118 +1,99 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Pagination.displayName = "Pagination";
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
|
||||
),
|
||||
);
|
||||
PaginationContent.displayName = "PaginationContent";
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
|
||||
({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
|
||||
);
|
||||
PaginationItem.displayName = "PaginationItem";
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
React.ComponentProps<"a">;
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
"no-underline",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
"no-underline",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
PaginationLink.displayName = "PaginationLink";
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = "PaginationPrevious";
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = "PaginationNext";
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis";
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
}
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
};
|
||||
|
||||
29
src/interface/web/components/ui/switch.tsx
Normal file
29
src/interface/web/components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-gray-300 dark:data-[state=unchecked]:bg-gray-600",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -1,30 +1,30 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "khoj-ai",
|
||||
"version": "1.42.8",
|
||||
"version": "2.0.0-beta.28",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -8,101 +8,102 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"collectstatic": "bash -c 'pushd ../../../ && source .venv/bin/activate && python3 src/khoj/manage.py collectstatic --noinput && deactivate && popd'",
|
||||
"cicollectstatic": "bash -c 'pushd ../../../ && python3 src/khoj/manage.py collectstatic --noinput && popd'",
|
||||
"export": "yarn build && cp -r out/ ../../khoj/interface/built && yarn collectstatic",
|
||||
"ciexport": "yarn build && cp -r out/ ../../khoj/interface/built && yarn cicollectstatic",
|
||||
"pypiciexport": "yarn build && cp -r out/ /opt/hostedtoolcache/Python/3.11.12/x64/lib/python3.11/site-packages/khoj/interface/compiled && yarn cicollectstatic",
|
||||
"watch": "nodemon --watch . --ext js,jsx,ts,tsx,css --ignore 'out/**/*' --exec 'yarn export'",
|
||||
"windowswatch": "nodemon --watch . --ext js,jsx,ts,tsx,css --ignore 'out/**/*' --exec 'yarn windowsexport'",
|
||||
"cicollectstatic": "bash -c 'pushd ../../../ && uv run python src/khoj/manage.py collectstatic --noinput && popd'",
|
||||
"export": "bun run build && cp -r out/ ../../khoj/interface/built && bun run collectstatic",
|
||||
"ciexport": "bun run build && cp -r out/ ../../khoj/interface/built && bun run cicollectstatic",
|
||||
"pypiciexport": "bun run build && mkdir -p ../../khoj/interface/compiled && cp -r out/. ../../khoj/interface/compiled && bun run cicollectstatic",
|
||||
"watch": "nodemon --watch . --ext js,jsx,ts,tsx,css --ignore 'out/**/*' --exec 'bun run export'",
|
||||
"windowswatch": "nodemon --watch . --ext js,jsx,ts,tsx,css --ignore 'out/**/*' --exec 'bun run windowsexport'",
|
||||
"windowscollectstatic": "cd ..\\..\\.. && .\\.venv\\Scripts\\Activate.bat && py .\\src\\khoj\\manage.py collectstatic --noinput && .\\.venv\\Scripts\\deactivate.bat && cd ..",
|
||||
"windowsexport": "yarn build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && yarn windowscollectstatic",
|
||||
"windowsexport": "bun run build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && bun run windowscollectstatic",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "^0.17.6",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-menubar": "^1.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@radix-ui/themes": "^3.1.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"cronstrue": "^2.50.0",
|
||||
"dompurify": "^3.1.6",
|
||||
"embla-carousel-autoplay": "^8.5.1",
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"cmdk": "^1.1.1",
|
||||
"cronstrue": "^2.61.0",
|
||||
"dompurify": "^3.3.2",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^12.0.6",
|
||||
"input-otp": "^1.2.4",
|
||||
"intl-tel-input": "^23.8.1",
|
||||
"framer-motion": "^12.23.12",
|
||||
"input-otp": "^1.4.2",
|
||||
"intl-tel-input": "^23.9.3",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.21",
|
||||
"libphonenumber-js": "^1.11.4",
|
||||
"katex": "^0.16.22",
|
||||
"libphonenumber-js": "^1.12.17",
|
||||
"lucide-react": "^0.468.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-highlightjs": "^4.1.0",
|
||||
"mermaid": "^11.4.1",
|
||||
"next": "14.2.30",
|
||||
"nodemon": "^3.1.3",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"shadcn-ui": "^0.9.0",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"markdown-it-highlightjs": "^4.2.0",
|
||||
"mermaid": "^11.11.0",
|
||||
"next": "15.5.14",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5",
|
||||
"vaul": "^0.9.1",
|
||||
"zod": "^3.23.8"
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/intl-tel-input": "^18.1.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.7",
|
||||
"nodemon": "^3.1.3",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^20.19.15",
|
||||
"@types/react": "^18.3.24",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.5.14",
|
||||
"eslint-config-prettier": "^9.1.2",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "^5"
|
||||
"shadcn-ui": "^0.9.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"printWidth": 100
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "yarn lint --fix"
|
||||
"*": "bun run lint"
|
||||
},
|
||||
"overrides": {
|
||||
"picomatch": ">=2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,7 @@ sudo -u postgres createdb khoj
|
||||
### Install Khoj
|
||||
|
||||
```bash
|
||||
pip install -e '.[dev]'
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
### Make Khoj DB migrations
|
||||
|
||||
@@ -14,6 +14,7 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import path
|
||||
|
||||
@@ -11,9 +11,15 @@ import requests
|
||||
import schedule
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.db import close_old_connections, connections
|
||||
from django.db import (
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
close_old_connections,
|
||||
connections,
|
||||
)
|
||||
from django.utils.timezone import make_aware
|
||||
from fastapi import Request, Response
|
||||
from fastapi import HTTPException, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from starlette.authentication import (
|
||||
AuthCredentials,
|
||||
AuthenticationBackend,
|
||||
@@ -44,13 +50,11 @@ from khoj.database.adapters import (
|
||||
)
|
||||
from khoj.database.models import ClientApplication, KhojUser, ProcessLock, Subscription
|
||||
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
||||
from khoj.routers.api_content import configure_content, configure_search
|
||||
from khoj.routers.api_content import configure_content
|
||||
from khoj.routers.twilio import is_twilio_enabled
|
||||
from khoj.utils import constants, state
|
||||
from khoj.utils.config import SearchType
|
||||
from khoj.utils.fs_syncer import collect_files
|
||||
from khoj.utils.helpers import is_none_or_empty, telemetry_disabled
|
||||
from khoj.utils.rawconfig import FullConfig
|
||||
from khoj.utils.helpers import is_none_or_empty
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -113,13 +117,24 @@ class UserAuthenticationBackend(AuthenticationBackend):
|
||||
Subscription.objects.create(user=default_user, type=Subscription.Type.STANDARD, renewal_date=renewal_date)
|
||||
|
||||
async def authenticate(self, request: HTTPConnection):
|
||||
# Skip authentication for error pages to avoid infinite recursion
|
||||
if request.url.path == "/server/error":
|
||||
return AuthCredentials(), UnauthenticatedUser()
|
||||
|
||||
current_user = request.session.get("user")
|
||||
if current_user and current_user.get("email"):
|
||||
user = (
|
||||
await self.khojuser_manager.filter(email=current_user.get("email"))
|
||||
.prefetch_related("subscription")
|
||||
.afirst()
|
||||
)
|
||||
try:
|
||||
user = (
|
||||
await self.khojuser_manager.filter(email=current_user.get("email"))
|
||||
.prefetch_related("subscription")
|
||||
.afirst()
|
||||
)
|
||||
except (DatabaseError, OperationalError):
|
||||
logger.error("DB Exception: Failed to authenticate user", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Please report this issue on Github, Discord or email team@khoj.dev and try again later.",
|
||||
)
|
||||
if user:
|
||||
subscribed = await ais_user_subscribed(user)
|
||||
if subscribed:
|
||||
@@ -131,12 +146,19 @@ class UserAuthenticationBackend(AuthenticationBackend):
|
||||
# Get bearer token from header
|
||||
bearer_token = request.headers["Authorization"].split("Bearer ")[1]
|
||||
# Get user owning token
|
||||
user_with_token = (
|
||||
await self.khojapiuser_manager.filter(token=bearer_token)
|
||||
.select_related("user")
|
||||
.prefetch_related("user__subscription")
|
||||
.afirst()
|
||||
)
|
||||
try:
|
||||
user_with_token = (
|
||||
await self.khojapiuser_manager.filter(token=bearer_token)
|
||||
.select_related("user")
|
||||
.prefetch_related("user__subscription")
|
||||
.afirst()
|
||||
)
|
||||
except (DatabaseError, OperationalError):
|
||||
logger.error("DB Exception: Failed to authenticate user applications", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Please report this issue on Github, Discord or email team@khoj.dev and try again later.",
|
||||
)
|
||||
if user_with_token:
|
||||
subscribed = await ais_user_subscribed(user_with_token.user)
|
||||
if subscribed:
|
||||
@@ -155,7 +177,16 @@ class UserAuthenticationBackend(AuthenticationBackend):
|
||||
)
|
||||
|
||||
# Get the client application
|
||||
client_application = await ClientApplicationAdapters.aget_client_application_by_id(client_id, client_secret)
|
||||
try:
|
||||
client_application = await ClientApplicationAdapters.aget_client_application_by_id(
|
||||
client_id, client_secret
|
||||
)
|
||||
except (DatabaseError, OperationalError):
|
||||
logger.error("DB Exception: Failed to authenticate first party application", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Please report this issue on Github, Discord or email team@khoj.dev and try again later.",
|
||||
)
|
||||
if client_application is None:
|
||||
return AuthCredentials(), UnauthenticatedUser()
|
||||
# Get the identifier used for the user
|
||||
@@ -185,21 +216,20 @@ class UserAuthenticationBackend(AuthenticationBackend):
|
||||
|
||||
# No auth required if server in anonymous mode
|
||||
if state.anonymous_mode:
|
||||
user = await self.khojuser_manager.filter(username="default").prefetch_related("subscription").afirst()
|
||||
try:
|
||||
user = await self.khojuser_manager.filter(username="default").prefetch_related("subscription").afirst()
|
||||
except (DatabaseError, OperationalError):
|
||||
logger.error("DB Exception: Failed to fetch default user from DB", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Please report this issue on Github, Discord or email team@khoj.dev and try again later.",
|
||||
)
|
||||
if user:
|
||||
return AuthCredentials(["authenticated", "premium"]), AuthenticatedKhojUser(user)
|
||||
|
||||
return AuthCredentials(), UnauthenticatedUser()
|
||||
|
||||
|
||||
def initialize_server(config: Optional[FullConfig]):
|
||||
try:
|
||||
configure_server(config, init=True)
|
||||
except Exception as e:
|
||||
logger.error(f"🚨 Failed to configure server on app load: {e}", exc_info=True)
|
||||
raise e
|
||||
|
||||
|
||||
def clean_connections(func):
|
||||
"""
|
||||
A decorator that ensures that Django database connections that have become unusable, or are obsolete, are closed
|
||||
@@ -220,19 +250,7 @@ def clean_connections(func):
|
||||
return func_wrapper
|
||||
|
||||
|
||||
def configure_server(
|
||||
config: FullConfig,
|
||||
regenerate: bool = False,
|
||||
search_type: Optional[SearchType] = None,
|
||||
init=False,
|
||||
user: KhojUser = None,
|
||||
):
|
||||
# Update Config
|
||||
if config == None:
|
||||
logger.info(f"Initializing with default config.")
|
||||
config = FullConfig()
|
||||
state.config = config
|
||||
|
||||
def initialize_server():
|
||||
if ConversationAdapters.has_valid_ai_model_api():
|
||||
ai_model_api = ConversationAdapters.get_ai_model_api()
|
||||
state.openai_client = openai.OpenAI(api_key=ai_model_api.api_key, base_url=ai_model_api.api_base_url)
|
||||
@@ -269,43 +287,33 @@ def configure_server(
|
||||
)
|
||||
|
||||
state.SearchType = configure_search_types()
|
||||
state.search_models = configure_search(state.search_models, state.config.search_type)
|
||||
setup_default_agent(user)
|
||||
setup_default_agent()
|
||||
|
||||
message = (
|
||||
"📡 Telemetry disabled"
|
||||
if telemetry_disabled(state.config.app, state.telemetry_disabled)
|
||||
else "📡 Telemetry enabled"
|
||||
)
|
||||
message = "📡 Telemetry disabled" if state.telemetry_disabled else "📡 Telemetry enabled"
|
||||
logger.info(message)
|
||||
|
||||
if not init:
|
||||
initialize_content(user, regenerate, search_type)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load some search models: {e}", exc_info=True)
|
||||
|
||||
|
||||
def setup_default_agent(user: KhojUser):
|
||||
AgentAdapters.create_default_agent(user)
|
||||
def setup_default_agent():
|
||||
AgentAdapters.create_default_agent()
|
||||
|
||||
|
||||
def initialize_content(user: KhojUser, regenerate: bool, search_type: Optional[SearchType] = None):
|
||||
# Initialize Content from Config
|
||||
if state.search_models:
|
||||
try:
|
||||
logger.info("📬 Updating content index...")
|
||||
all_files = collect_files(user=user)
|
||||
status = configure_content(
|
||||
user,
|
||||
all_files,
|
||||
regenerate,
|
||||
search_type,
|
||||
)
|
||||
if not status:
|
||||
raise RuntimeError("Failed to update content index")
|
||||
except Exception as e:
|
||||
raise e
|
||||
try:
|
||||
logger.info("📬 Updating content index...")
|
||||
status = configure_content(
|
||||
user,
|
||||
{},
|
||||
regenerate,
|
||||
search_type,
|
||||
)
|
||||
if not status:
|
||||
raise RuntimeError("Failed to update content index")
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
def configure_routes(app):
|
||||
@@ -315,6 +323,7 @@ def configure_routes(app):
|
||||
from khoj.routers.api_automation import api_automation
|
||||
from khoj.routers.api_chat import api_chat
|
||||
from khoj.routers.api_content import api_content
|
||||
from khoj.routers.api_memories import api_memories
|
||||
from khoj.routers.api_model import api_model
|
||||
from khoj.routers.notion import notion_router
|
||||
from khoj.routers.web_client import web_client
|
||||
@@ -324,6 +333,7 @@ def configure_routes(app):
|
||||
app.include_router(api_agents, prefix="/api/agents")
|
||||
app.include_router(api_automation, prefix="/api/automation")
|
||||
app.include_router(api_model, prefix="/api/model")
|
||||
app.include_router(api_memories, prefix="/api/memories")
|
||||
app.include_router(api_content, prefix="/api/content")
|
||||
app.include_router(notion_router, prefix="/api/notion")
|
||||
app.include_router(web_client)
|
||||
@@ -368,19 +378,37 @@ def configure_middleware(app, ssl_enabled: bool = False):
|
||||
# and prevent further error logging.
|
||||
return Response(status_code=499)
|
||||
|
||||
class ServerErrorMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
try:
|
||||
return await call_next(request)
|
||||
except HTTPException as e:
|
||||
# Check if this is a server error (5xx) that we want to handle
|
||||
if e.status_code >= 500 and e.status_code < 600:
|
||||
# Check if this is a web route (not API route)
|
||||
path = request.url.path
|
||||
is_api_route = path.startswith("/api/") or path.startswith("/server/")
|
||||
|
||||
# Redirect web routes to error page, let API routes get the raw error
|
||||
if not is_api_route:
|
||||
return RedirectResponse(url="/server/error", status_code=302)
|
||||
|
||||
# Re-raise for API routes and non-5xx errors
|
||||
raise e
|
||||
|
||||
if ssl_enabled:
|
||||
app.add_middleware(HTTPSRedirectMiddleware)
|
||||
app.add_middleware(SuppressClientDisconnectMiddleware)
|
||||
app.add_middleware(AsyncCloseConnectionsMiddleware)
|
||||
app.add_middleware(AuthenticationMiddleware, backend=UserAuthenticationBackend())
|
||||
app.add_middleware(ServerErrorMiddleware) # Add after AuthenticationMiddleware to catch its exceptions
|
||||
app.add_middleware(NextJsMiddleware)
|
||||
app.add_middleware(SessionMiddleware, secret_key=os.environ.get("KHOJ_DJANGO_SECRET_KEY", "!secret"))
|
||||
|
||||
|
||||
def update_content_index():
|
||||
for user in get_all_users():
|
||||
all_files = collect_files(user=user)
|
||||
success = configure_content(user, all_files)
|
||||
success = configure_content(user, {})
|
||||
if not success:
|
||||
raise RuntimeError("Failed to update content index")
|
||||
logger.info("📪 Content index updated via Scheduler")
|
||||
@@ -405,7 +433,7 @@ def configure_search_types():
|
||||
@schedule.repeat(schedule.every(2).minutes)
|
||||
@clean_connections
|
||||
def upload_telemetry():
|
||||
if telemetry_disabled(state.config.app, state.telemetry_disabled) or not state.telemetry:
|
||||
if state.telemetry_disabled or not state.telemetry:
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user