Compare commits
475 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
387b7c7887 | ||
|
|
7b8b3a66ae | ||
|
|
5927ca8032 | ||
|
|
bdb81260ac | ||
|
|
238bc11a50 | ||
|
|
9986c183ea | ||
|
|
8a4c20d59a | ||
|
|
ab7fb5117c | ||
|
|
de24ffcf0d | ||
|
|
a60baa55fb | ||
|
|
1ac8de6c3a | ||
|
|
5d59acd1f4 | ||
|
|
f6ce2fd432 | ||
|
|
029775420c | ||
|
|
4808ce778a | ||
|
|
58c8068079 | ||
|
|
2d9dd81e76 | ||
|
|
2c5350329a | ||
|
|
acdc3f9470 | ||
|
|
ca45fce8ac | ||
|
|
c0316a6b5d | ||
|
|
8dad9362e7 | ||
|
|
2b1482d2b4 | ||
|
|
0b568e204e | ||
|
|
39e566ba91 | ||
|
|
27ad9b1302 | ||
|
|
f75606d7f5 | ||
|
|
3675938df6 | ||
|
|
b5bcce7f85 | ||
|
|
05c0aa3882 | ||
|
|
2d9bf14ecb | ||
|
|
7815e02dd4 | ||
|
|
d951e36945 | ||
|
|
16b31c3e35 | ||
|
|
f2f37ae444 | ||
|
|
ec9add9a51 | ||
|
|
d99f03e4f3 | ||
|
|
f16b0f628b | ||
|
|
0e9e9648e6 | ||
|
|
6f94a076f7 | ||
|
|
acb825f4f5 | ||
|
|
5075d13902 | ||
|
|
b3c6c8c84b | ||
|
|
fc411091c8 | ||
|
|
a7623e64fa | ||
|
|
af1d4b9ba4 | ||
|
|
1d581464e6 | ||
|
|
acf1c14122 | ||
|
|
7d3a25f8c0 | ||
|
|
faf3584acd | ||
|
|
5ef198a5b2 | ||
|
|
c08b9e89f0 | ||
|
|
64b2073e63 | ||
|
|
7ee0d9067d | ||
|
|
f28693c8c7 | ||
|
|
2943bed5d4 | ||
|
|
37afa3411f | ||
|
|
1ee21f5150 | ||
|
|
93f4ceabc1 | ||
|
|
370ebdee24 | ||
|
|
52fed6023f | ||
|
|
823f8d58bb | ||
|
|
09b71846be | ||
|
|
167ef000f4 | ||
|
|
00ee4c2697 | ||
|
|
d4a8ff0683 | ||
|
|
ccccb8e7e6 | ||
|
|
c4be3b43e5 | ||
|
|
265d2a79be | ||
|
|
24d0fdb262 | ||
|
|
23b1b36f8c | ||
|
|
81c75e1024 | ||
|
|
694f551625 | ||
|
|
7607abc726 | ||
|
|
e9f9d92989 | ||
|
|
c23688e2de | ||
|
|
a4388c5e65 | ||
|
|
e9d6899fc2 | ||
|
|
b17577c138 | ||
|
|
ec106d743d | ||
|
|
4258392fc7 | ||
|
|
020a956c89 | ||
|
|
998d08f155 | ||
|
|
20d95dc45e | ||
|
|
1eab6c8590 | ||
|
|
bafda233e2 | ||
|
|
e412ed3bcb | ||
|
|
9f785dbafe | ||
|
|
7d3a208f8b | ||
|
|
2a63439b16 | ||
|
|
b7ed32f455 | ||
|
|
7e6b611a19 | ||
|
|
34d54c75f7 | ||
|
|
7cb14ff07a | ||
|
|
91047d1619 | ||
|
|
1151d14466 | ||
|
|
c56072aa7b | ||
|
|
484b0aa96b | ||
|
|
1b35a3b16e | ||
|
|
5a5bbe3852 | ||
|
|
c61b289bd1 | ||
|
|
f835e330b8 | ||
|
|
af6a70c9fb | ||
|
|
e0775446c9 | ||
|
|
de1cd8c264 | ||
|
|
37e261ff93 | ||
|
|
8bc28fb11d | ||
|
|
22cfedcaff | ||
|
|
8220dc6115 | ||
|
|
e296d387e1 | ||
|
|
95c2a52775 | ||
|
|
18a973b666 | ||
|
|
842036688d | ||
|
|
41bdd6d6d9 | ||
|
|
1cdfa8087c | ||
|
|
46f928165c | ||
|
|
f7840782a4 | ||
|
|
b803ed19d3 | ||
|
|
69c3635ce7 | ||
|
|
51e56e17ee | ||
|
|
b744dffefd | ||
|
|
70f670dcf7 | ||
|
|
c627527a6f | ||
|
|
c7b67a978e | ||
|
|
60af173c4a | ||
|
|
4f2fcc82f0 | ||
|
|
322fb34d4b | ||
|
|
3e1e4a1857 | ||
|
|
caf5c3d74c | ||
|
|
692058bbdd | ||
|
|
015c155582 | ||
|
|
bf71e472c4 | ||
|
|
f38c072f07 | ||
|
|
2f7a8698a0 | ||
|
|
5541bc09c8 | ||
|
|
6a9865ace7 | ||
|
|
f28208d35b | ||
|
|
75559a55aa | ||
|
|
185dcb61f7 | ||
|
|
3e74d383fe | ||
|
|
87e97e40f4 | ||
|
|
5a75f2c00f | ||
|
|
e6260a7bb6 | ||
|
|
7a8a9fc807 | ||
|
|
30304ccc56 | ||
|
|
5b17fa5dda | ||
|
|
687a881ad2 | ||
|
|
0db630a123 | ||
|
|
261f62e353 | ||
|
|
4ce17acd00 | ||
|
|
6c35ee4960 | ||
|
|
e66adf60c5 | ||
|
|
cf8745ef78 | ||
|
|
529ffdb7e3 | ||
|
|
8d1c5226ec | ||
|
|
f136214290 | ||
|
|
f9606ce9b7 | ||
|
|
d8fe677933 | ||
|
|
f3765a20b9 | ||
|
|
a6e1b2c7cb | ||
|
|
eed9e401a2 | ||
|
|
f188396395 | ||
|
|
9c5ff1699a | ||
|
|
b1d3979ed9 | ||
|
|
5f8b76c8f2 | ||
|
|
1bb746aaed | ||
|
|
07b3bdf181 | ||
|
|
e62888659f | ||
|
|
0adee07d40 | ||
|
|
bbe7491f2f | ||
|
|
d48a789442 | ||
|
|
e6014e89bf | ||
|
|
1509c536f9 | ||
|
|
0d8cdee60a | ||
|
|
d3c07a098d | ||
|
|
5a8ea884a9 | ||
|
|
02b46a1784 | ||
|
|
a733e5c1d4 | ||
|
|
7858aff2e2 | ||
|
|
cab0957fd3 | ||
|
|
3f607b3978 | ||
|
|
4f783b911c | ||
|
|
4492017b96 | ||
|
|
13dee7d89e | ||
|
|
6babd5c0ce | ||
|
|
1b2cad2a2c | ||
|
|
723b37955a | ||
|
|
84dd1b57fe | ||
|
|
ed16914ac3 | ||
|
|
7941f4d54d | ||
|
|
db93ac5d4b | ||
|
|
fd0e0405af | ||
|
|
9a43622cef | ||
|
|
bfeb64b48f | ||
|
|
833553c3a3 | ||
|
|
dbbcf2564f | ||
|
|
cd85a51980 | ||
|
|
60870a7a3e | ||
|
|
32ce564b7c | ||
|
|
ecb873c488 | ||
|
|
f58cff5bcc | ||
|
|
f0bb6883f8 | ||
|
|
b1eb564706 | ||
|
|
4a7efdc552 | ||
|
|
ffbf57292c | ||
|
|
ccc46a09b5 | ||
|
|
9d86cb57ac | ||
|
|
7ee179ee1f | ||
|
|
00a908ae12 | ||
|
|
058c902dc7 | ||
|
|
b8c9b3ffa3 | ||
|
|
8a447107dd | ||
|
|
44e0b20202 | ||
|
|
51e83bcc26 | ||
|
|
efcad4996d | ||
|
|
48548684c0 | ||
|
|
8ec90f194f | ||
|
|
60cdf61737 | ||
|
|
2e165a0e0a | ||
|
|
00fa4fa0fa | ||
|
|
13292fc4ca | ||
|
|
a5a06da3fc | ||
|
|
ade2f6f5d1 | ||
|
|
b3253562a5 | ||
|
|
7e8e80f29e | ||
|
|
88007d7552 | ||
|
|
a6339bb973 | ||
|
|
551630f0f1 | ||
|
|
413255ddc7 | ||
|
|
41eb85c933 | ||
|
|
1a1d9c7257 | ||
|
|
1685c60e3c | ||
|
|
8503d7a07b | ||
|
|
878cc023a0 | ||
|
|
a47a54f207 | ||
|
|
e86143dbb0 | ||
|
|
eb5af38f33 | ||
|
|
5dcac18ba5 | ||
|
|
3daef910c0 | ||
|
|
44d34f9090 | ||
|
|
377f7668c5 | ||
|
|
6607e666dc | ||
|
|
778c571288 | ||
|
|
7482797605 | ||
|
|
662dffea3b | ||
|
|
19cd607c96 | ||
|
|
75a370cc06 | ||
|
|
5adbfe14ab | ||
|
|
52db15706d | ||
|
|
cfe7a1068e | ||
|
|
ebe92ef16d | ||
|
|
37b8fc5577 | ||
|
|
4d30e5b158 | ||
|
|
694bedc25b | ||
|
|
3e4325edab | ||
|
|
70201e8db8 | ||
|
|
b36a7833a6 | ||
|
|
eb4e12d3c5 | ||
|
|
498fe2458c | ||
|
|
0277d16daf | ||
|
|
e439a6ddac | ||
|
|
fafc467173 | ||
|
|
fc33162ec6 | ||
|
|
c5ad172616 | ||
|
|
54b4203683 | ||
|
|
3f5f418d0e | ||
|
|
8303b09129 | ||
|
|
b224d7ffad | ||
|
|
daec439d52 | ||
|
|
2d4b284218 | ||
|
|
6b9550238f | ||
|
|
b8d3e3669a | ||
|
|
91fe41106e | ||
|
|
9cf52bb7e4 | ||
|
|
e694c82343 | ||
|
|
1af9dbb083 | ||
|
|
6d5ca5a3e1 | ||
|
|
7f0d1bd414 | ||
|
|
7426a4f819 | ||
|
|
07f36fa95a | ||
|
|
f03525f431 | ||
|
|
3832ef0236 | ||
|
|
1197266912 | ||
|
|
469a1cb6a2 | ||
|
|
bba4e0b529 | ||
|
|
5923b6d89e | ||
|
|
e9f86e320b | ||
|
|
b0ee78586c | ||
|
|
6f46e6afc6 | ||
|
|
53eabe0c06 | ||
|
|
65dade4838 | ||
|
|
2ab8fb78b1 | ||
|
|
bf815e4463 | ||
|
|
a1c362a4f7 | ||
|
|
b015b0e83d | ||
|
|
71ebf31a54 | ||
|
|
30d60aaae9 | ||
|
|
583fa3c188 | ||
|
|
7316e6b9d3 | ||
|
|
4759c4ac96 | ||
|
|
466ef3f8f1 | ||
|
|
59000a47cb | ||
|
|
a5c16ad600 | ||
|
|
de15a7a3fc | ||
|
|
dd31936746 | ||
|
|
e8176b41ef | ||
|
|
1a5405e24c | ||
|
|
c837f3779e | ||
|
|
1c6ed9bc6d | ||
|
|
c7764c7470 | ||
|
|
6c630bc6c3 | ||
|
|
9a5bf4c701 | ||
|
|
2e9275c0f3 | ||
|
|
ba0ba6b59f | ||
|
|
23f61d49e0 | ||
|
|
6f8f846086 | ||
|
|
06dce4729b | ||
|
|
6dd90931e8 | ||
|
|
47b754c07b | ||
|
|
b47f30ad77 | ||
|
|
e6b21144e2 | ||
|
|
c2bf405489 | ||
|
|
63719747cb | ||
|
|
dbbd4b9777 | ||
|
|
a0f38e079f | ||
|
|
e9567741eb | ||
|
|
b26a6e25d1 | ||
|
|
f69f9e3523 | ||
|
|
d51011314f | ||
|
|
2668e42e7f | ||
|
|
aeaebfb515 | ||
|
|
e00c6b486e | ||
|
|
5fccccfdff | ||
|
|
b98a0cfe1b | ||
|
|
3e7e73ddd6 | ||
|
|
bea0aa5445 | ||
|
|
02658ad4fd | ||
|
|
cbae8b68fb | ||
|
|
3a75838196 | ||
|
|
6c1861b319 | ||
|
|
21fe1a917b | ||
|
|
6f1d799759 | ||
|
|
1b4a51f4a2 | ||
|
|
0369eb6e0e | ||
|
|
375685530f | ||
|
|
c5cfd0f2cf | ||
|
|
e1a5c17775 | ||
|
|
e358723baa | ||
|
|
c8c5d50b1a | ||
|
|
c25bf97831 | ||
|
|
23b71b0dff | ||
|
|
998e2aec30 | ||
|
|
0c6b6de09e | ||
|
|
cc22e1b013 | ||
|
|
5b69252337 | ||
|
|
a0e9530fa4 | ||
|
|
260aa61818 | ||
|
|
4471c1e37f | ||
|
|
609e7ee19c | ||
|
|
0b1b262512 | ||
|
|
43413cd21f | ||
|
|
bf4c2f219e | ||
|
|
037e157648 | ||
|
|
6b80bb3f37 | ||
|
|
9e31ebff93 | ||
|
|
54132efd67 | ||
|
|
510d9b3a29 | ||
|
|
3e0c882e27 | ||
|
|
0eb000c3ea | ||
|
|
6f8a65c529 | ||
|
|
a31cd0dec1 | ||
|
|
08b379c2ab | ||
|
|
4a471979eb | ||
|
|
d693baccbc | ||
|
|
1baebb8d0e | ||
|
|
010486fb36 | ||
|
|
6a135b1ed7 | ||
|
|
e6ffb6b52c | ||
|
|
1ab59865b5 | ||
|
|
05138cbd0a | ||
|
|
9bdb48807b | ||
|
|
b334db0fca | ||
|
|
2f034f807a | ||
|
|
69c9e8cc08 | ||
|
|
a353d883a0 | ||
|
|
6d59ad7fc9 | ||
|
|
aec44a0b89 | ||
|
|
516af86575 | ||
|
|
465ef0b772 | ||
|
|
814aca6d69 | ||
|
|
4446de00d3 | ||
|
|
5ea8b16f84 | ||
|
|
d61bddf56c | ||
|
|
a129b017b9 | ||
|
|
34118078bf | ||
|
|
d5ba916978 | ||
|
|
78d1a29bc1 | ||
|
|
6fa2dbc042 | ||
|
|
8a6722ba97 | ||
|
|
afcfc60637 | ||
|
|
c015eeb5dd | ||
|
|
826c3dc9cc | ||
|
|
d5ceff2691 | ||
|
|
553beae848 | ||
|
|
a038e4911b | ||
|
|
ff44734774 | ||
|
|
0ee7cc8c47 | ||
|
|
541ce04ebc | ||
|
|
8eccd8a5e4 | ||
|
|
cffc14a46a | ||
|
|
3723904512 | ||
|
|
fbb95ca342 | ||
|
|
093e276908 | ||
|
|
c83b8f2768 | ||
|
|
80fe5ce182 | ||
|
|
24a0d8b073 | ||
|
|
870d9ecdbf | ||
|
|
3b7a9358c3 | ||
|
|
afbeee9e82 | ||
|
|
8c12a69570 | ||
|
|
4f89319b40 | ||
|
|
bbfd320ed4 | ||
|
|
c793d8a69e | ||
|
|
1acf969c6e | ||
|
|
2c4bf91a61 | ||
|
|
eb09aba747 | ||
|
|
fdd4c02461 | ||
|
|
eda33e092f | ||
|
|
a25689fabf | ||
|
|
cfe46fd9f5 | ||
|
|
fb818ead60 | ||
|
|
a4b2552540 | ||
|
|
da5b07e913 | ||
|
|
c4a1ae9375 | ||
|
|
d6fe5d9a63 | ||
|
|
0d04018622 | ||
|
|
6f280b1ccc | ||
|
|
68e7c297e0 | ||
|
|
732332a3c5 | ||
|
|
8fc7f980aa | ||
|
|
4110e71e84 | ||
|
|
939811e9b5 | ||
|
|
a4d88612c1 | ||
|
|
55be90cdd2 | ||
|
|
1c7a562880 | ||
|
|
57a36967bf | ||
|
|
c7c32a7467 | ||
|
|
9d33d8c0fa | ||
|
|
a94062469a | ||
|
|
38090b2553 | ||
|
|
a53178cab9 | ||
|
|
59edb99f04 | ||
|
|
abd6f58aee | ||
|
|
f413dc62cd | ||
|
|
1d7d51a7ab | ||
|
|
22f6db0a6b | ||
|
|
55a23eae25 | ||
|
|
7e277e9381 | ||
|
|
fa7b40ab86 | ||
|
|
5e5fe4b7af | ||
|
|
d3c0111121 | ||
|
|
b9966eb3d4 | ||
|
|
427575e958 | ||
|
|
59032a06d5 | ||
|
|
9262aea7a5 | ||
|
|
ff26b19d2b | ||
|
|
3cfe5aabe5 | ||
|
|
0afe66ac39 | ||
|
|
afe91a2633 | ||
|
|
2b12a5514e | ||
|
|
093eb473cb | ||
|
|
bd3b590153 | ||
|
|
d042e073cc | ||
|
|
d23f2849d4 | ||
|
|
d4e5c95711 |
2
.github/workflows/dockerize.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- src/interface/web/**
|
||||
- pyproject.toml
|
||||
- Dockerfile
|
||||
- prod.Dockerfile
|
||||
@@ -30,6 +31,7 @@ on:
|
||||
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' }}
|
||||
|
||||
|
||||
28
.github/workflows/pypi.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- src/interface/web/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/pypi.yml
|
||||
pull_request:
|
||||
@@ -15,8 +16,10 @@ on:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- src/interface/web/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/pypi.yml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -25,7 +28,7 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -34,9 +37,20 @@ jobs:
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: ⬇️ Install Application
|
||||
- name: ⬇️ Install Server
|
||||
run: python -m pip install --upgrade pip && pip install --upgrade .
|
||||
|
||||
- name: ⬇️ Install Web Client
|
||||
run: |
|
||||
yarn install
|
||||
yarn pypiciexport
|
||||
working-directory: src/interface/web
|
||||
|
||||
- name: 📂 Copy Generated Files
|
||||
run: |
|
||||
mkdir -p src/khoj/interface/compiled
|
||||
cp -r /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/khoj/interface/compiled/* src/khoj/interface/compiled/
|
||||
|
||||
- name: ⚙️ Build Python Package
|
||||
run: |
|
||||
# Setup Environment for Reproducible Builds
|
||||
@@ -44,7 +58,7 @@ jobs:
|
||||
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
|
||||
rm -rf dist
|
||||
|
||||
# Build PyPi Package
|
||||
# Build PyPI Package
|
||||
pipx run build
|
||||
|
||||
- name: 🌡️ Validate Python Package
|
||||
@@ -54,11 +68,13 @@ jobs:
|
||||
pipx run twine check dist/*
|
||||
|
||||
- name: ⏫ Upload Python Package Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: khoj-assistant
|
||||
path: dist/*.whl
|
||||
name: khoj
|
||||
path: dist/khoj-*.whl
|
||||
|
||||
- name: 📦 Publish Python Package to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/master'
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
1
.github/workflows/test.yml
vendored
@@ -29,7 +29,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python_version:
|
||||
- '3.9'
|
||||
- '3.10'
|
||||
- '3.11'
|
||||
- '3.12'
|
||||
|
||||
6
.gitignore
vendored
@@ -16,13 +16,15 @@ todesktop.json
|
||||
|
||||
# Build artifacts
|
||||
/src/khoj/interface/web/images
|
||||
/src/khoj/interface/built/
|
||||
/src/khoj/interface/compiled/404.html
|
||||
/build/
|
||||
/dist/
|
||||
khoj_assistant.egg-info
|
||||
/config/khoj*.yml
|
||||
.pytest_cache
|
||||
*.log
|
||||
static
|
||||
/src/khoj/static
|
||||
|
||||
# Obsidian plugin artifacts
|
||||
# ---
|
||||
@@ -35,6 +37,8 @@ src/interface/obsidian/main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
# IntelliJ
|
||||
.idea
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
22
Dockerfile
@@ -1,13 +1,24 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ubuntu:jammy
|
||||
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
|
||||
LABEL homepage="https://khoj.dev"
|
||||
LABEL repository="https://github.com/khoj-ai/khoj"
|
||||
LABEL org.opencontainers.image.source="https://github.com/khoj-ai/khoj"
|
||||
|
||||
# Install System Dependencies
|
||||
RUN apt update -y && apt -y install python3-pip swig
|
||||
RUN apt update -y && apt -y install python3-pip swig curl
|
||||
|
||||
WORKDIR /app
|
||||
# Install Node.js and Yarn
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
|
||||
RUN apt -y install nodejs
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN apt update && apt -y install yarn
|
||||
|
||||
# Install RapidOCR dependencies
|
||||
RUN apt -y install libgl1 libgl1-mesa-glx libglib2.0-0
|
||||
|
||||
# Install Application
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml .
|
||||
COPY README.md .
|
||||
ARG VERSION=0.0.0
|
||||
@@ -20,6 +31,11 @@ COPY . .
|
||||
# Set the PYTHONPATH environment variable in order for it to find the Django app.
|
||||
ENV PYTHONPATH=/app/src:$PYTHONPATH
|
||||
|
||||
# Go to the directory src/interface/web and export the built Next.js assets
|
||||
WORKDIR /app/src/interface/web
|
||||
RUN bash -c "yarn cache clean && yarn install --verbose && yarn ciexport"
|
||||
WORKDIR /app
|
||||
|
||||
# Run the Application
|
||||
# There are more arguments required for the application to run,
|
||||
# but these should be passed in through the docker-compose.yml file.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[](https://github.com/khoj-ai/khoj/actions/workflows/test.yml)
|
||||
[](https://github.com/khoj-ai/khoj/pkgs/container/khoj)
|
||||
[](https://pypi.org/project/khoj-assistant/)
|
||||
[](https://pypi.org/project/khoj/)
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,8 @@ services:
|
||||
- KHOJ_DEBUG=False
|
||||
- KHOJ_ADMIN_EMAIL=username@example.com
|
||||
- KHOJ_ADMIN_PASSWORD=password
|
||||
# Uncomment the following lines to make your instance publicly accessible. Replace the domain with your domain. Proceed with caution, especially if you are using anonymous mode.
|
||||
# Uncomment the following lines to make your instance publicly accessible.
|
||||
# Replace the domain with your domain. Proceed with caution, especially if you are using anonymous mode.
|
||||
# - KHOJ_NO_HTTPS=True
|
||||
# - KHOJ_DOMAIN=192.168.0.104
|
||||
command: --host="0.0.0.0" --port=42110 -vv --anonymous-mode
|
||||
|
||||
BIN
documentation/assets/img/admin_get_emali_login.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
documentation/assets/img/admin_successful_login_url.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
BIN
documentation/assets/img/magic_link.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
documentation/assets/img/mic_chat_icon.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
documentation/assets/img/speaker_icon.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
documentation/assets/img/summarize.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
8
documentation/docs/advanced/_category_.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Advanced Self Hosting",
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Advanced setup for Self Hosting Khoj server"
|
||||
}
|
||||
}
|
||||
52
documentation/docs/advanced/authentication.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Authenticate
|
||||
|
||||
:::info
|
||||
This is only helpful for self-hosted users or teams. If you're using [Khoj Cloud](https://app.khoj.dev), both Magic Links and Google OAuth work.
|
||||
:::
|
||||
|
||||
By default, most of the instructions for self-hosting Khoj assume a single user, and so the default configuration is to run in anonymous mode. However, if you want to enable authentication, you can do so either with with [Magic Links](#using-magic-links) or [Google OAuth](#using-google-oauth) as shown below. This can be helpful to make Khoj securely accessible to you and your team.
|
||||
|
||||
:::tip[Note]
|
||||
Remove the `--anonymous-mode` flag in your start up command to enable authentication.
|
||||
:::
|
||||
|
||||
## Using Magic Links
|
||||
The most secure way to do this is to integrate with [Resend](https://resend.com) by setting up an account and adding an environment variable for `RESEND_API_KEY`. You can get your API key [here](https://resend.com/api-keys). This will allow you to automatically send sign-in links to users who want to log in.
|
||||
|
||||
It's still possible to use the magic links feature without Resend, but you'll need to manually send the magic links to users who want to log in.
|
||||
|
||||
## Manually sending magic links
|
||||
|
||||
1. The user will have to enter their email address in the login form.
|
||||
They'll click `Send Magic 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"/>
|
||||
|
||||
2. You can get their magic link using the admin panel
|
||||
Go to the [admin panel](http://localhost:42110/server/admin/database/khojuser/). You'll see a list of users. Search for the user you want to send a magic link to. Tick the checkbox next to their row, and use the action drop down at the top to 'Get email login URL'. This will generate a magic link that you can send to the user, which will appear at the top of the admin interface.
|
||||
|
||||
| Get email login URL | Retrieved login URL |
|
||||
|---------------------|---------------------|
|
||||
| <img src="/img/admin_get_emali_login.png" alt="Get user magic sign in link" width="400" />| <img src="/img/admin_successful_login_url.png" alt="Successfully retrieved a login URL" width="400" />|
|
||||
|
||||
3. Send the magic link to the user. They can click on it to log in.
|
||||
|
||||
Once they click on the link, they'll automatically be logged in. They'll have to repeat this process for every new device they want to log in from, but they shouldn't have to repeat it on the same device.
|
||||
|
||||
A given magic link can only be used once. If the user tries to use it again, they'll be redirected to the login page to get a new magic link.
|
||||
|
||||
## Using Google OAuth
|
||||
|
||||
To set up your self-hosted Khoj with Google Auth, you need to create a project in the Google Cloud Console and enable the Google Auth API.
|
||||
|
||||
To implement this, you'll need to:
|
||||
1. You must use the `python` package or build from source, because you'll need to install additional packages for the google auth libraries (`prod`). The syntax to install the right packages is
|
||||
```
|
||||
pip install khoj[prod]
|
||||
```
|
||||
2. [Create authorization credentials](https://developers.google.com/identity/sign-in/web/sign-in) for your application.
|
||||
3. Open your [Google cloud console](https://console.developers.google.com/apis/credentials) and create a configuration like below for the relevant `OAuth 2.0 Client IDs` project:
|
||||

|
||||
|
||||
4. Configure these environment variables: `GOOGLE_CLIENT_SECRET`, and `GOOGLE_CLIENT_ID`. You can find these values in the Google cloud console, in the same place where you configured the authorized origins and redirect URIs.
|
||||
|
||||
That's it! That should be all you have to do. Now, when you reload Khoj without `--anonymous-mode`, you should be able to use your Google account to sign in.
|
||||
37
documentation/docs/advanced/litellm.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# LiteLLM
|
||||
:::info
|
||||
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
|
||||
:::
|
||||
|
||||
:::info
|
||||
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
|
||||
:::
|
||||
|
||||
[LiteLLM](https://docs.litellm.ai/docs/proxy/quick_start) exposes an OpenAI compatible API that proxies requests to other LLM API services. This provides a standardized API to interact with both open-source and commercial LLMs.
|
||||
|
||||
Using LiteLLM with Khoj makes it possible to turn any LLM behind an API into your personal AI agent.
|
||||
|
||||
## Setup
|
||||
1. Install LiteLLM
|
||||
```bash
|
||||
pip install litellm[proxy]
|
||||
```
|
||||
2. Start LiteLLM and use Mistral tiny via Mistral API
|
||||
```
|
||||
export MISTRAL_API_KEY=<MISTRAL_API_KEY>
|
||||
litellm --model mistral/mistral-tiny --drop_params
|
||||
```
|
||||
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
|
||||
- Name: `proxy-name`
|
||||
- Api Key: `any string`
|
||||
- Api Base Url: **URL of your Openai Proxy API**
|
||||
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
|
||||
- Name: `llama3.1` (replace with the name of your local model)
|
||||
- Model Type: `Openai`
|
||||
- Openai Config: `<the proxy config you created in step 3>`
|
||||
- Max prompt size: `20000` (replace with the max prompt size of your model)
|
||||
- Tokenizer: *Do not set for OpenAI, Mistral, Llama3 based models*
|
||||
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
|
||||
- Default model: `<name of chat model option you created in step 4>`
|
||||
- Summarizer model: `<name of chat model option you created in step 4>`
|
||||
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
|
||||
30
documentation/docs/advanced/lmstudio.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# LM Studio
|
||||
:::info
|
||||
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
|
||||
:::
|
||||
|
||||
:::info
|
||||
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
|
||||
:::
|
||||
|
||||
[LM Studio](https://lmstudio.ai/) is a desktop app to chat with open-source LLMs on your local machine. LM Studio provides a neat interface for folks comfortable with a GUI.
|
||||
|
||||
LM Studio can expose an [OpenAI API compatible server](https://lmstudio.ai/docs/local-server). This makes it possible to turn chat models from LM Studio into your personal AI agents with Khoj.
|
||||
|
||||
## Setup
|
||||
1. Install [LM Studio](https://lmstudio.ai/) and download your preferred Chat Model
|
||||
2. Go to the Server Tab on LM Studio, Select your preferred Chat Model and Click the green Start Server button
|
||||
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
|
||||
- Name: `proxy-name`
|
||||
- Api Key: `any string`
|
||||
- Api Base Url: `http://localhost:1234/v1/` (default for LMStudio)
|
||||
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
|
||||
- Name: `llama3.1` (replace with the name of your local model)
|
||||
- Model Type: `Openai`
|
||||
- Openai Config: `<the proxy config you created in step 3>`
|
||||
- Max prompt size: `20000` (replace with the max prompt size of your model)
|
||||
- Tokenizer: *Do not set for OpenAI, mistral, llama3 based models*
|
||||
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
|
||||
- Default model: `<name of chat model option you created in step 4>`
|
||||
- Summarizer model: `<name of chat model option you created in step 4>`
|
||||
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
|
||||
36
documentation/docs/advanced/ollama.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Ollama
|
||||
:::info
|
||||
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
|
||||
:::
|
||||
|
||||
:::info
|
||||
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
|
||||
:::
|
||||
|
||||
Ollama allows you to run [many popular open-source LLMs](https://ollama.com/library) locally from your terminal.
|
||||
For folks comfortable with the terminal, Ollama's terminal based flows can ease setup and management of chat models.
|
||||
|
||||
Ollama exposes a local [OpenAI API compatible server](https://github.com/ollama/ollama/blob/main/docs/openai.md#models). This makes it possible to use chat models from Ollama to create your personal AI agents with Khoj.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Setup Ollama: https://ollama.com/
|
||||
2. Start your preferred model with Ollama. For example,
|
||||
```bash
|
||||
ollama run llama3.1
|
||||
```
|
||||
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
|
||||
- Name: `ollama`
|
||||
- Api Key: `any string`
|
||||
- Api Base Url: `http://localhost:11434/v1/` (default for Ollama)
|
||||
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
|
||||
- Name: `llama3.1` (replace with the name of your local model)
|
||||
- Model Type: `Openai`
|
||||
- Openai Config: `<the ollama config you created in step 3>`
|
||||
- Max prompt size: `20000` (replace with the max prompt size of your model)
|
||||
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
|
||||
- Default model: `<name of chat model option you created in step 4>`
|
||||
- Summarizer model: `<name of chat model option you created in step 4>`
|
||||
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
|
||||
|
||||
That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model.
|
||||
17
documentation/docs/advanced/support-multilingual-docs.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Support Multilingual Docs
|
||||
Khoj uses an embedding model to understand documents. Multilingual embedding models improve the search quality for documents not in English. This affects both search and chat with docs experiences across Khoj.
|
||||
|
||||
To improve search and chat quality for non-english documents you can use a [multilingual model](https://www.sbert.net/docs/pretrained_models.html#multi-lingual-models).<br />
|
||||
For example, the [paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) supports [50+ languages](https://www.sbert.net/docs/pretrained_models.html#:~:text=we%20used%20the%20following%2050%2B%20languages), has decent search quality and speed for a consumer machine.
|
||||
To use it:
|
||||
1. Open [the search config](http://localhost:42110/server/admin/database/searchmodelconfig/) on your server's admin settings page. Either create a new search model, if none exists, or update the existing one. For example,
|
||||
- Set the `bi_encoder` field to `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`
|
||||
- Set the `cross_encoder` field to `mixedbread-ai/mxbai-rerank-xsmall-v1`
|
||||
2. Regenerate your content index from all the relevant clients. This step is very important, as you'll need to re-encode all your content with the new model.
|
||||
|
||||
:::info[Note]
|
||||
Modern search/embedding model like [mixedbread-ai/mxbai-embed-large-v1](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1) expect a prefix to the query (or docs) string to improve encoding. Update the `bi_encoder_query_encode_config` field of your [embedding model](http://localhost:42110/server/admin/database/searchmodelconfig/) with `{prompt: <prefix-prompt>}` to improve the search quality of these models.
|
||||
|
||||
E.g. `{prompt: "Represent this query for searching documents"}`. You can pass any valid JSON object that the SentenceTransformer `encode` function accepts
|
||||
|
||||
:::
|
||||
37
documentation/docs/advanced/use-openai-proxy.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Use OpenAI Proxy
|
||||
:::info
|
||||
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
|
||||
:::
|
||||
|
||||
:::info
|
||||
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
|
||||
:::
|
||||
|
||||
Khoj can use any OpenAI API compatible server including [Ollama](/advanced/ollama), [LMStudio](/advanced/lmstudio) and [LiteLLM](/advanced/litellm).
|
||||
Configuring this allows you to use non-standard, open or commercial, local or hosted LLM models for Khoj
|
||||
|
||||
Combine them with Khoj can turn your favorite LLM into an AI agent. Allowing you to chat with your docs, find answers from the internet, build custom agents and run automations.
|
||||
|
||||
For specific integrations, see our [Ollama](/advanced/ollama), [LMStudio](/advanced/lmstudio) and [LiteLLM](/advanced/litellm) setup docs. For general instructions to setup Khoj with an OpenAI API proxy see below.
|
||||
|
||||
## General Setup
|
||||
|
||||
1. Start your preferred OpenAI API compatible app
|
||||
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
|
||||
- Name: `proxy-name`
|
||||
- Api Key: `any string`
|
||||
- Api Base Url: **URL of your Openai Proxy API**
|
||||
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
|
||||
- Name: `llama3` (replace with the name of your local model)
|
||||
- Model Type: `Openai`
|
||||
- Openai Config: `<the proxy config you created in step 3>`
|
||||
- Max prompt size: `2000` (replace with the max prompt size of your model)
|
||||
- Tokenizer: *Do not set for OpenAI, mistral, llama3 based models*
|
||||
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
|
||||
- Default model: `<name of chat model option you created in step 4>`
|
||||
- Summarizer model: `<name of chat model option you created in step 4>`
|
||||
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
|
||||
@@ -7,14 +7,15 @@ sidebar_position: 1
|
||||
> Query your Second Brain from your machine
|
||||
|
||||
Use the Desktop app to chat and search with Khoj.
|
||||
You can also sync any relevant files with Khoj using the app.
|
||||
Khoj will use these files to provide contextual responses when you search or chat.
|
||||
You can also share your files, folders with Khoj using the app.
|
||||
Khoj will keep these files in sync to provide contextual responses when you search or chat.
|
||||
|
||||
## Features
|
||||
- **Chat**
|
||||
- **Faster answers**: Find answers quickly, from your private notes or the public internet
|
||||
- **Assisted creativity**: Smoothly weave across retrieving answers and generating content
|
||||
- **Iterative discovery**: Iteratively explore and re-discover your notes
|
||||
- **Quick access**: Use [Khoj Mini](/features/khoj_mini) on the desktop to quickly pull up a mini chat module for quicker answers
|
||||
- **Search**
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
@@ -22,9 +23,10 @@ Khoj will use these files to provide contextual responses when you search or cha
|
||||
## Setup
|
||||
|
||||
1. Install the [Khoj Desktop app](https://khoj.dev/downloads) for your OS
|
||||
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
|
||||
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
|
||||
3. Set your Khoj API Key on the *Settings* page of the Khoj Desktop app
|
||||
4. [Optional] Add any files, folders you'd like Khoj to be aware of on the *Settings* page and Click *Save*
|
||||
These files and folders will be automatically kept in sync for you
|
||||
|
||||
## Interface
|
||||
| Chat | Search |
|
||||
|
||||
@@ -30,7 +30,7 @@ sidebar_position: 2
|
||||
|  |  |
|
||||
|
||||
## Setup
|
||||
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
|
||||
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
|
||||
2. Add below snippet to your Emacs config file, usually at `~/.emacs.d/init.el`
|
||||
|
||||
|
||||
@@ -90,13 +90,13 @@ M-x package-install khoj
|
||||
See [Khoj Search](/features/search) for details
|
||||
1. Hit `C-c s s` (or `M-x khoj RET s`) to open khoj search
|
||||
2. Enter your query in natural language<br/>
|
||||
E.g *"What is the meaning of life?"*, *"My life goals for 2023"*
|
||||
E.g. *"What is the meaning of life?"*, *"My life goals for 2023"*
|
||||
|
||||
### Chat
|
||||
See [Khoj Chat](/features/chat) for details
|
||||
1. Hit `C-c s c` (or `M-x khoj RET c`) to open khoj chat
|
||||
2. Ask questions in a natural, conversational style<br/>
|
||||
E.g *"When did I file my taxes last year?"*
|
||||
E.g. *"When did I file my taxes last year?"*
|
||||
|
||||
### Find Similar Entries
|
||||
This feature finds entries similar to the one you are currently on.
|
||||
@@ -105,7 +105,7 @@ This feature finds entries similar to the one you are currently on.
|
||||
|
||||
### Advanced Usage
|
||||
- Add [query filters](https://github.com/khoj-ai/khoj/#query-filters) during search to narrow down results further
|
||||
e.g `What is the meaning of life? -"god" +"none" dt>"last week"`
|
||||
e.g. `What is the meaning of life? -"god" +"none" dt>"last week"`
|
||||
|
||||
- Use `C-c C-o 2` to open the current result at cursor in its source org file
|
||||
- This calls `M-x org-open-at-point` on the current entry and opens the second link in the entry.
|
||||
|
||||
@@ -23,7 +23,7 @@ sidebar_position: 3
|
||||
|
||||
1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel
|
||||
2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian
|
||||
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
|
||||
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
|
||||
4. Set your Khoj API Key in the Khoj plugin settings in Obsidian
|
||||
|
||||
See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for more details on installing Obsidian plugins.
|
||||
@@ -31,7 +31,7 @@ See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsid
|
||||
## Use
|
||||
### Chat
|
||||
Click the *Khoj chat* icon 💬 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Chat* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette) and ask questions in a natural, conversational style.<br />
|
||||
E.g *"When did I file my taxes last year?"*
|
||||
E.g. *"When did I file my taxes last year?"*
|
||||
|
||||
See [Khoj Chat](/features/chat) for more details
|
||||
|
||||
|
||||
@@ -6,14 +6,16 @@ sidebar_position: 5
|
||||
|
||||
> Query your Second Brain from WhatsApp
|
||||
|
||||
Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan [this QR code](https://khoj.dev/whatsapp) on your phone to chat with Khoj on WhatsApp.
|
||||
Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan the QQ code below on your phone to chat with Khoj on WhatsApp.
|
||||
|
||||
Without any desktop clients, you can start chatting with Khoj on WhatsApp. Bear in mind you do need one of the desktop clients in order to share and sync your data with Khoj. The WhatsApp AI bot will work right away for answering generic queries and using Khoj in default mode.
|
||||
|
||||
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/config).
|
||||
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/settings).
|
||||
|
||||
If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://khoj.dev/pricing) on Khoj Cloud.
|
||||
|
||||
<img src="https://khoj-web-bucket.s3.amazonaws.com/khojwhatsapp.png" alt="WhatsApp QR Code" width="300" height="300" />
|
||||
|
||||
## Features
|
||||
|
||||
- **Slash Commands**: Use slash commands to quickly access Khoj features
|
||||
@@ -23,6 +25,6 @@ If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://k
|
||||
|
||||
We have more commands under development, including `/share` to uploading documents directly to your Khoj account from WhatsApp, and `/speak` in order to get a speech response from Khoj. Feel free to [raise an issue](https://github.com/khoj-ai/flint/issues) if you have any suggestions for new commands.
|
||||
|
||||
## Nerdy Details
|
||||
## Source Code
|
||||
|
||||
You can find all of the code for the WhatsApp bot in the the [flint repository](https://github.com/khoj-ai/flint). As all of our code, it is open source and you can contribute to it.
|
||||
|
||||
@@ -186,7 +186,7 @@ In whichever clients you're using for testing, you'll need to update the server
|
||||
### Before Making Changes
|
||||
1. Install Git Hooks for Validation
|
||||
```shell
|
||||
pre-commit install -t pre-push -t pre-commit
|
||||
./scripts/dev_setup.sh
|
||||
```
|
||||
- This ensures standard code formatting fixes and other checks run automatically on every commit and push
|
||||
- Note 1: If [pre-commit](https://pre-commit.com/#intro) didn't already get installed, [install it](https://pre-commit.com/#install) via `pip install pre-commit`
|
||||
@@ -229,7 +229,7 @@ The core code for the Obsidian plugin is under `src/interface/obsidian`. The fil
|
||||
4. Open the `khoj` folder in the file explorer that opens. You'll see a file called `main.js` in this folder. To test your changes, replace this file with the `main.js` file that was generated by the development server in the previous section.
|
||||
|
||||
## Create Khoj Release (Only for Maintainers)
|
||||
Follow the steps below to [release](https://github.com/debanjum/khoj/releases/) Khoj. This will create a stable release of Khoj on [Pypi](https://pypi.org/project/khoj-assistant/), [Melpa](https://stable.melpa.org/#%252Fkhoj) and [Obsidian](https://obsidian.md/plugins?id%253Dkhoj). It will also create desktop apps of Khoj and attach them to the latest release.
|
||||
Follow the steps below to [release](https://github.com/debanjum/khoj/releases/) Khoj. This will create a stable release of Khoj on [Pypi](https://pypi.org/project/khoj/), [Melpa](https://stable.melpa.org/#%252Fkhoj) and [Obsidian](https://obsidian.md/plugins?id%253Dkhoj). It will also create desktop apps of Khoj and attach them to the latest release.
|
||||
|
||||
1. Create and tag release commit by running the bump_version script. The release commit sets version number in required metadata files.
|
||||
```shell
|
||||
|
||||
@@ -4,11 +4,11 @@ The Github integration allows you to index as many repositories as you want. It'
|
||||
|
||||
# Configure your settings
|
||||
|
||||
1. Go to [https://app.khoj.dev/config](https://app.khoj.dev/config) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
|
||||
1. Go to [https://app.khoj.dev/settings](https://app.khoj.dev/settings) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
|
||||
|
||||
## Use the Github plugin
|
||||
|
||||
1. Generate a [classic PAT (personal access token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) from [Github](https://github.com/settings/tokens) with `repo` and `admin:org` scopes at least.
|
||||
2. Navigate to [https://app.khoj.dev/config/content-source/github](https://app.khoj.dev/config/content-source/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
|
||||
2. Navigate to [https://app.khoj.dev/settings#github](https://app.khoj.dev/settings#github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
|
||||
3. Click `Save`. Go back to the settings page and click `Configure`.
|
||||
4. Go to [https://app.khoj.dev/](https://app.khoj.dev/) and start searching!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The Notion integration allows you to search/chat with your Notion workspaces. [Notion](https://notion.so/) is a platform people use for taking notes, especially for collaboration.
|
||||
|
||||
Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
|
||||
Go to https://app.khoj.dev/settings to connect your Notion workspace(s) to Khoj.
|
||||
|
||||

|
||||
|
||||
@@ -13,7 +13,7 @@ Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
|
||||

|
||||
3. Share all the workspaces that you want to integrate with the Khoj integration you just made in the previous step
|
||||

|
||||
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at http://localhost:42110/config/content-source/notion. Click `Save`.
|
||||
5. Click `Configure` in http://localhost:42110/config to index your Notion workspace(s).
|
||||
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at [http://localhost:42110/settings#notion](http://localhost:42110/settings#notion). Click `Save`.
|
||||
5. Click `Configure` in http://localhost:42110/settings to index your Notion workspace(s).
|
||||
|
||||
That's it! You should be ready to start searching and chatting. Make sure you've configured your [chat settings](/get-started/setup#2-configure).
|
||||
|
||||
@@ -29,6 +29,7 @@ Khoj is available as a [Desktop app](/clients/desktop), [Emacs package](/clients
|
||||

|
||||
|
||||
### Supported Data Sources
|
||||
Khoj can understand your org-mode, markdown, PDF, plaintext files, [Github projects](/data-sources/github_integration) and [Notion pages](/data-sources/notion_integration).
|
||||
Khoj can understand your word, PDF, org-mode, markdown, plaintext files, [Github projects](/data-sources/github_integration) and [Notion pages](/data-sources/notion_integration).
|
||||
|
||||
|
||||

|
||||
@@ -5,5 +5,5 @@
|
||||
Khoj will use your local time zone to determine the scheduling localization. You can go back and configure the prompt any time you want from the automations page. You can also delete the automation if you no longer need it.
|
||||
|
||||
:::danger[Note]
|
||||
Automations will not deliver emails to self-hosted users out of the box. You'll have to have Resend and [Google Auth](/miscellaneous/google_auth) setup to send emails.
|
||||
Automations will not deliver emails to self-hosted users out of the box. You'll have to have Resend and [Authentication](/advanced/authentication) setup to send emails.
|
||||
:::
|
||||
|
||||
@@ -25,7 +25,7 @@ Offline chat stays completely private and can work without internet using open-s
|
||||
> - An Nvidia, AMD GPU or a Mac M1+ machine would significantly speed up chat response times
|
||||
|
||||
1. Open your [Khoj offline settings](http://localhost:42110/server/admin/database/offlinechatprocessorconversationconfig/) and click *Enable* on the Offline Chat configuration.
|
||||
2. Open your [Chat model options settings](http://localhost:42110/server/admin/database/chatmodeloptions/) and add any [GGUF chat model](https://huggingface.co/models?library=gguf) to use for offline chat. Make sure to use `Offline` as its type. For a balanced chat model that runs well on standard consumer hardware we recommend using [Hermes-2-Pro-Mistral-7B by NousResearch](https://huggingface.co/NousResearch/Hermes-2-Pro-Mistral-7B-GGUF) by default.
|
||||
2. Open your [Chat model options settings](http://localhost:42110/server/admin/database/chatmodeloptions/) and add any [GGUF chat model](https://huggingface.co/models?library=gguf) to use for offline chat. Make sure to use `Offline` as its type. For a balanced chat model that runs well on standard consumer hardware we recommend using [Llama 3.1 by Meta](https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF) by default. For machines with no or small GPU we recommend using [Gemma 2 2B](https://huggingface.co/bartowski/gemma-2-2b-it-GGUF) or [Phi 3.5 mini](https://huggingface.co/bartowski/Phi-3.5-mini-instruct-GGUF)
|
||||
|
||||
|
||||
:::tip[Note]
|
||||
@@ -68,3 +68,4 @@ Slash commands allows you to change what Khoj uses to respond to your query
|
||||
- **/online**: Use online information and incorporate it in the prompt to the LLM to send you a response.
|
||||
- **/image**: Generate an image in response to your query.
|
||||
- **/help**: Use /help to get all available commands and general information about Khoj
|
||||
- **/summarize**: Can be used to summarize 1 selected file filter for that conversation. Refer to [File Summarization](summarization) for details.
|
||||
|
||||
9
documentation/docs/features/khoj_mini.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Desktop Quick Chat (Khoj Mini)
|
||||
|
||||
Once you have the Khoj [desktop application](https://khoj.dev/downloads) installed, you can use the desktop shortcut to quickly pull up a mini chat module for quicker answers. See the desktop setup instructions [in the docs](/clients/desktop.md) for more information.
|
||||
|
||||
To use it, you just have to copy the text you want to inject into your query, and then run `Ctrl + Shift + K` (or `Cmd + Shift + K` on Mac) to open the mini chat module. The text you copied will be automatically pasted into the chat module, and you can then hit enter to get the answer. You can edit the text before hitting enter if you want to refine your query.
|
||||
|
||||
The desktop shortcut is a great way to quickly get answers to your questions without having to switch between windows or tabs. It's especially useful when you're working on a project and need to quickly look up something without losing your focus.
|
||||
|
||||

|
||||
@@ -1,17 +1,21 @@
|
||||
# Online Search
|
||||
|
||||
By default, Khoj will try to infer which information-sourcing tools are required to answer your question. Sometimes, you'll have a need for outside questions that the LLM's knowledge doesn't cover. In that case, it will use the `online` search feature.
|
||||
Khoj will research on the internet to ground its responses, when it determines that it would need fresh information outside its existing knowledge to answer the query. It will always show any online references it used to respond to your requests.
|
||||
|
||||
For example, these queries would trigger an online search:
|
||||
By default, Khoj will try to infer which information sources, it needs to read to answer your question. This can include reading your documents or researching information online. You can also explicitly trigger an online search by adding the `/online` prefix to your chat query.
|
||||
|
||||
Example queries that should trigger an online search:
|
||||
- What's the latest news about the Israel-Palestine war?
|
||||
- Where can I find the best pizza in New York City?
|
||||
- Deadline for filing taxes 2024.
|
||||
- /online Deadline for filing taxes 2024.
|
||||
- Give me a summary of this article: https://en.wikipedia.org/wiki/Haitian_Revolution
|
||||
|
||||
Try it out yourself! https://app.khoj.dev
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
The general online search function currently requires an API key from Serper.dev. You can grab one here: https://serper.dev/, and then add it as an environment variable with the name `SERPER_DEV_API_KEY`.
|
||||
Online search works out of the box even when self-hosting. Khoj uses [JinaAI's reader API](https://jina.ai/reader/) to search online and read webpages by default. No API key setup is necessary.
|
||||
|
||||
Without any API keys, Khoj will use the `requests` library to directly read any webpages you give it a link to. This means that you can use Khoj to read any webpage that you have access in your local network.
|
||||
To improve 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.
|
||||
|
||||
For advanced webpage reading, set the `OLOSTEP_API_KEY` environment variable to your [Olostep](https://www.olostep.com/) API key. This has a higher success rate at reading webpages than the default webpage reader.
|
||||
|
||||
26
documentation/docs/features/summarization.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# File Summarization
|
||||
|
||||
You can use the `/summarize` command to get Khoj to generate context driven summaries of your documents.
|
||||
Simply select a single file filter on the left panel menu and then use `/summarize [any context]` and Khoj
|
||||
will produce a tailored summary of the text.
|
||||
|
||||
You can also try a natural language query which include the intent for summary without explicitly using the `/summarize` command.
|
||||
|
||||
## Design Diagram
|
||||
|
||||
<img src="/img/summarize.jpg" alt="Chat on Web" style={{width: '800px'}}/>
|
||||
|
||||
## Example Usage
|
||||
|
||||
* `/summarize in a way that can be used as practice questions for a test`
|
||||
* `/summarize in a way a toddler can understand`
|
||||
* `/summarize in one paragraph`
|
||||
|
||||
Without using the `/summarize` command:
|
||||
* `create a summary of the document in a way that can be used as practice questions for a test`
|
||||
* `summarize the document in a way a toddler can understand`
|
||||
* `generate a one paragraph summary of the document`
|
||||
27
documentation/docs/features/voice-chat.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Voice
|
||||
|
||||
You can talk to Khoj using your voice. Khoj will respond to your queries using the same models as the chat feature. You can use voice chat on the web, Desktop, and Obsidian apps.
|
||||
|
||||

|
||||
|
||||
Click on the little mic icon to send your voice message to Khoj. It will send back what it heard via text. You'll have some time to edit it before sending it, if required. Try it at https://app.khoj.dev/.
|
||||
|
||||
## Voice Response
|
||||
|
||||
If you send a voice message, Khoj will automatically respond back with a voice message.
|
||||
You can also click on the speaker icon next to any message to hear it out loud. The voice response feature is available only on the web view right now.
|
||||
|
||||

|
||||
|
||||
## Setup (Self-Hosting)
|
||||
|
||||
Voice chat will automatically be configured when you initialize the application. The default configuration will run locally. If you want to use the OpenAI whisper API for voice chat, you can set it up by following these steps:
|
||||
|
||||
1. Setup your OpenAI API key. See instructions [here](/get-started/setup#2-configure).
|
||||
2. Create a new configuration at http://localhost:42110/server/admin/database/speechtotextmodeloptions/. We recommend the value `whisper-1` and model type `Openai`.
|
||||
|
||||
If you want to use the Text to Speech feature, you can set it up by following these steps:
|
||||
|
||||
1. Setup your account on [ElevenLabs.io](https://elevenlabs.io/).
|
||||
2. Configure your API key in your environment variables with the key `ELEVEN_LABS_API_KEY`.
|
||||
2. (Optional) Create a new [Voice model option](http://localhost:42110/server/admin/database/voicemodeloption/) with a specific voice ID from whichever voice you want to use. You can explore the options [here](https://elevenlabs.io/app/voice-library).
|
||||
@@ -1,14 +0,0 @@
|
||||
# Voice
|
||||
|
||||
You can talk to Khoj using your voice. Khoj will respond to your queries using the same models as the chat feature. You can use voice chat on the web, Desktop, and Obsidian apps. Click on the little mic icon to send your voice message to Khoj. It will send back what it heard via text. You'll have some time to edit it before sending it, if required. Try it at https://app.khoj.dev/.
|
||||
|
||||
:::info[Voice Response]
|
||||
Khoj doesn't yet respond with voice, but it will send back a text response. Let us know if you're interested in voice responses at team at khoj.dev.
|
||||
:::
|
||||
|
||||
## Setup (Self-Hosting)
|
||||
|
||||
Voice chat will automatically be configured when you initialize the application. The default configuration will run locally. If you want to use the OpenAI whisper API for voice chat, you can set it up by following these steps:
|
||||
|
||||
1. Setup your OpenAI API key. See instructions [here](/get-started/setup#2-configure).
|
||||
2. Create a new configuration at http://localhost:42110/server/admin/database/speechtotextmodeloptions/. We recommend the value `whisper-1` and model type `Openai`.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Demos
|
||||
|
||||
Check out a couple of demos and screenshots of Khoj in action.
|
||||
|
||||
### Screenshots
|
||||
|
||||
| Web | Obsidian | Emacs |
|
||||
|:---:|:--------:|:-----:|
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
|
||||
### Videos
|
||||
#### Khoj in Obsidian
|
||||
[Link to Video](https://github-production-user-asset-6210df.s3.amazonaws.com/6413477/240061700-3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b.mp4)
|
||||
|
||||
##### Installation
|
||||
|
||||
1. Install Khoj via `pip` and start Khoj backend in a terminal (Run `khoj`)
|
||||
```bash
|
||||
python -m pip install khoj-assistant
|
||||
khoj
|
||||
```
|
||||
2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
|
||||
- Check the new Khoj plugin settings
|
||||
- Let Khoj backend index the markdown, pdf, Github markdown files in the current Vault
|
||||
- Open Khoj plugin on Obsidian via Search button on Left Pane
|
||||
- Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
|
||||
- Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
|
||||
|
||||
#### Khoj in Emacs, Browser
|
||||
[Link to Video](https://user-images.githubusercontent.com/6413477/184735169-92c78bf1-d827-4663-9087-a1ea194b8f4b.mp4)
|
||||
|
||||
##### Installation
|
||||
|
||||
- Install Khoj via pip
|
||||
- Start Khoj app
|
||||
- Add this readme and [khoj.el readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs) as org-mode for Khoj to index
|
||||
- Search \"*Setup editor*\" on the Web and Emacs. Re-rank the results for better accuracy
|
||||
- Top result is what we are looking for, the [section to Install Khoj.el on Emacs](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#2-Install-Khojel)
|
||||
|
||||
##### Analysis
|
||||
|
||||
- The results do not have any words used in the query
|
||||
- *Based on the top result it seems the re-ranking model understands that Emacs is an editor?*
|
||||
- The results incrementally update as the query is entered
|
||||
- The results are re-ranked, for better accuracy, once user hits enter
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
slug: /
|
||||
keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain ai", "ai search engine"]
|
||||
keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain", "personal ai", "ai search engine"]
|
||||
---
|
||||
|
||||
# Overview
|
||||
@@ -9,7 +9,7 @@ keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features"
|
||||
<p align="center"><img src="/img/khoj-logo-sideways-500.png" width="200" alt="Khoj Logo"></img></p>
|
||||
|
||||
<div align="center">
|
||||
<b>An AI copilot for your Second Brain</b>
|
||||
<b>Your Second Brain</b>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
@@ -27,10 +27,10 @@ keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features"
|
||||
Welcome to the Khoj Docs! This is the best place to get setup and explore Khoj's features.
|
||||
|
||||
- Khoj is an open source, personal AI
|
||||
- You can [chat](/features/chat) with it about anything. It'll use files you shared with it to respond, when relevant
|
||||
- You can [chat](/features/chat) with it about anything. It'll use files you shared with it to respond, when relevant. It can also access information from the public internet.
|
||||
- Quickly [find](/features/search) relevant notes and documents using natural language
|
||||
- It understands pdf, plaintext, markdown, org-mode files, [notion pages](/data-sources/notion_integration) and [github repositories](/data-sources/github_integration)
|
||||
- Access it from your [Emacs](/clients/emacs), [Obsidian](/clients/obsidian), [Web browser](/clients/web) or the [Khoj Desktop app](/clients/desktop)
|
||||
- Access it from your [Emacs](/clients/emacs), [Obsidian](/clients/obsidian), the [Khoj desktop app](/clients/desktop), or [any web browser](/clients/web)
|
||||
- Use [cloud](https://app.khoj.dev/login) to access your Khoj anytime from anywhere, [self-host](/get-started/setup) on consumer hardware for privacy
|
||||
|
||||
## Quickstart
|
||||
@@ -39,13 +39,3 @@ Welcome to the Khoj Docs! This is the best place to get setup and explore Khoj's
|
||||
|
||||
## At a Glance
|
||||

|
||||
|
||||
#### [Search](/features/search)
|
||||
- **Natural**: Use natural language queries to quickly find relevant notes and documents.
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
|
||||
#### [Chat](/features/chat)
|
||||
- **Faster answers**: Find answers faster, smoother than search. No need to manually scan through your notes to find answers.
|
||||
- **Iterative discovery**: Iteratively explore and (re-)discover your notes
|
||||
- **Assisted creativity**: Smoothly weave across answers retrieval and content generation
|
||||
- **Online or Offline**: Choose online or offline chat depending on your requirements
|
||||
|
||||
@@ -14,7 +14,7 @@ Here's what to consider if you're using Khoj, whether self-hosted or on our clou
|
||||
1. We collect completely anonymized usage telemetry and send it to [PostHog](https://posthog.com/). This includes data like unique chat requests, unique search requests, unique requests to index data. Usage data is collected to help us understand how people are using Khoj, and to help us prioritize features.
|
||||
- We do not log your IP address, nor upload any of your personal data to PostHog.
|
||||
- You can see our telemetry aggregation code [here](https://github.com/khoj-ai/khoj/blob/master/src/khoj/routers/helpers.py#L71) and see our telemetry server [here](https://github.com/khoj-ai/khoj/blob/master/src/telemetry/telemetry.py).
|
||||
- If you're self-hosting, you can opt out of telemetry by following [these instructions](./miscellaneous/telemetry).
|
||||
- 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:
|
||||
@@ -22,6 +22,8 @@ Self-hosting isn't for everyone, so we've still taken steps to make Khoj privacy
|
||||
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.
|
||||
|
||||
You can see our full privacy policy [here](https://khoj.dev/privacy-policy).
|
||||
|
||||
|
||||
:::tip[Info]
|
||||
Your data is yours. We do not sell your data or use it for training models. Khoj is a sustainable, open-source alternative to closed-source, commercial personal AI. We have no interest in selling your data to make a quick buck.
|
||||
@@ -16,20 +16,13 @@ import TabItem from '@theme/TabItem';
|
||||
|
||||
## Setup
|
||||
These are the general setup instructions for self-hosted Khoj.
|
||||
|
||||
- Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine
|
||||
- Check the [Khoj Emacs docs](/clients/emacs#setup) to setup Khoj with Emacs<br />
|
||||
It's simpler as it can skip the server *install*, *run* and *configure* step below.
|
||||
- Check the [Khoj Obsidian docs](/clients/obsidian#setup) to setup Khoj with Obsidian<br />
|
||||
Its simpler as it can skip the *configure* step below.
|
||||
|
||||
For Installation, you can either use Docker or install the Khoj server locally.
|
||||
You can install the Khoj server using either Docker or Pip.
|
||||
|
||||
:::info[Offline Model + GPU]
|
||||
If you want to use the offline chat model and you have a GPU, you should use Installation Option 2 - local setup via the Python package directly. Our Docker image doesn't currently support running the offline chat model on GPU, making inference times really slow.
|
||||
:::
|
||||
|
||||
### Installation Option 1 (Docker)
|
||||
### 1A. Install Method 1: Docker
|
||||
|
||||
#### Prerequisites
|
||||
1. Install Docker Engine. See [official instructions](https://docs.docker.com/engine/install/).
|
||||
@@ -37,21 +30,23 @@ If you want to use the offline chat model and you have a GPU, you should use Ins
|
||||
|
||||
#### Setup
|
||||
|
||||
Use the sample docker-compose [in Github](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml) to run Khoj in Docker. Start by configuring all the environment variables to your choosing. Your admin account will automatically be created based on the admin credentials in that file, so pay attention to those. To start the container, run the following command in the same directory as the docker-compose.yml file. This will automatically setup the database and run the Khoj server.
|
||||
1. Get the sample docker-compose file [from Github](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml).
|
||||
2. Configure the environment variables in the docker-compose.yml to your choosing.<br />
|
||||
Note: *Your admin account will automatically be created based on the admin credentials in that file, so pay attention to those.*
|
||||
3. Now start the container by running the following command in the same directory as your docker-compose.yml file. This will automatically setup the database and run the Khoj server.
|
||||
```shell
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
```shell
|
||||
docker-compose up
|
||||
```
|
||||
Khoj should now be running at http://localhost:42110! You can see the web UI in your browser.
|
||||
|
||||
Khoj should now be running at http://localhost:42110. You can see the web UI in your browser.
|
||||
|
||||
### Installation Option 2 (Local)
|
||||
### 1B. Install Method 2: Pip
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
##### Install Postgres (with PgVector)
|
||||
|
||||
Khoj uses the `pgvector` package to store embeddings of your index in a Postgres database. In order to use this, you need to have Postgres installed.
|
||||
Khoj uses Postgres DB for all server configuration and to scale to multi-user setups. It uses the pgvector package in Postgres to manage your document embeddings. Both Postgres and pgvector need to be installed for Khoj to work.
|
||||
|
||||
```mdx-code-block
|
||||
<Tabs groupId="operating-systems">
|
||||
@@ -97,24 +92,23 @@ sudo -u postgres createdb khoj --password
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
#### Install Khoj server
|
||||
|
||||
#### Install package
|
||||
|
||||
##### Local Server Setup
|
||||
##### Install Khoj Server
|
||||
- *Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine*
|
||||
- Check [llama-cpp-python setup](https://python.langchain.com/docs/integrations/llms/llamacpp#installation) if you hit any llama-cpp issues with the installation
|
||||
|
||||
Run the following command in your terminal to install the Khoj backend.
|
||||
Run the following command in your terminal to install the Khoj server.
|
||||
|
||||
```mdx-code-block
|
||||
<Tabs groupId="operating-systems">
|
||||
<TabItem value="macos" label="MacOS">
|
||||
```shell
|
||||
# ARM/M1+ Machines
|
||||
MAKE_ARGS="-DLLAMA_METAL=on" python -m pip install khoj-assistant
|
||||
MAKE_ARGS="-DLLAMA_METAL=on" python -m pip install khoj
|
||||
|
||||
# Intel Machines
|
||||
python -m pip install khoj-assistant
|
||||
python -m pip install khoj
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="win" label="Windows">
|
||||
@@ -128,25 +122,25 @@ python -m pip install khoj-assistant
|
||||
$env:CMAKE_ARGS = "-DLLAMA_VULKAN=on"
|
||||
|
||||
# 2. Install Khoj
|
||||
py -m pip install khoj-assistant
|
||||
py -m pip install khoj
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="unix" label="Linux">
|
||||
```shell
|
||||
# CPU
|
||||
python -m pip install khoj-assistant
|
||||
python -m pip install khoj
|
||||
# NVIDIA (CUDA) GPU
|
||||
CMAKE_ARGS="DLLAMA_CUDA=on" FORCE_CMAKE=1 python -m pip install khoj-assistant
|
||||
CMAKE_ARGS="DLLAMA_CUDA=on" FORCE_CMAKE=1 python -m pip install khoj
|
||||
# AMD (ROCm) GPU
|
||||
CMAKE_ARGS="-DLLAMA_HIPBLAS=on" FORCE_CMAKE=1 python -m pip install khoj-assistant
|
||||
CMAKE_ARGS="-DLLAMA_HIPBLAS=on" FORCE_CMAKE=1 python -m pip install khoj
|
||||
# VULCAN GPU
|
||||
CMAKE_ARGS="-DLLAMA_VULKAN=on" FORCE_CMAKE=1 python -m pip install khoj-assistant
|
||||
CMAKE_ARGS="-DLLAMA_VULKAN=on" FORCE_CMAKE=1 python -m pip install khoj
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
##### Local Server Start
|
||||
##### Start Khoj Server
|
||||
|
||||
Before getting started, configure the following environment variables in your terminal for the first run
|
||||
|
||||
@@ -186,26 +180,37 @@ On the first run, you will be prompted to input credentials for your admin accou
|
||||
|
||||
Khoj should now be running at http://localhost:42110. You can see the web UI in your browser.
|
||||
|
||||
Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g with `@reboot khoj`)
|
||||
Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g. with `@reboot khoj`)
|
||||
|
||||
|
||||
### Setup Notes
|
||||
|
||||
You can use Khoj with a custom domain as well. To do so, you need to set the `KHOJ_DOMAIN` environment variable to your domain (e.g., `export KHOJ_DOMAIN=my-khoj-domain.com` or add it to your `docker-compose.yml`). By default, the Khoj server you set up will not be accessible outside of `localhost` or `127.0.0.1`.
|
||||
|
||||
:::warning[Without HTTPS certificate]
|
||||
To expose Khoj on a custom domain over the public internet, use of an SSL certificate is strongly recommended. You can use [Let's Encrypt](https://letsencrypt.org/) to get a free SSL certificate for your domain.
|
||||
|
||||
To disable HTTPS, set the `KHOJ_NO_HTTPS` environment variable to `True`. This can be useful if Khoj is only accessible behind a secure, private network.
|
||||
:::
|
||||
|
||||
### 2. Configure
|
||||
1. Go to http://localhost:42110/server/admin and login with your admin credentials.
|
||||
#### Login to the Khoj Admin Panel
|
||||
Go to http://localhost:42110/server/admin and login with the admin credentials you setup during installation.
|
||||
|
||||
:::info[CSRF Error]
|
||||
Ensure you are using **localhost, not 127.0.0.1**, to access the admin panel to avoid the CSRF error.
|
||||
:::
|
||||
|
||||
:::info[DISALLOWED HOST Error]
|
||||
You may hit this if you try access Khoj exposed on a custom domain (e.g. 192.168.12.3 or example.com) or over HTTP.
|
||||
Set the environment variables KHOJ_DOMAIN=your-domain and KHOJ_NO_HTTPS=false if required to avoid this error.
|
||||
:::
|
||||
|
||||
:::tip[Note]
|
||||
Using Safari on Mac? You might not be able to login to the admin panel. Try using Chrome or Firefox instead.
|
||||
:::
|
||||
|
||||
#### Configure Chat Model
|
||||
##### Configure OpenAI or a custom OpenAI-compatible proxy server
|
||||
Setup which chat model you'd want to use. Khoj supports local and online chat models.
|
||||
|
||||
:::tip[Multiple Chat Models]
|
||||
Add a `ServerChatSettings` with `Default` and `Summarizer` fields set to your preferred chat model via [the admin panel](http://localhost:42110/server/admin/database/serverchatsettings/add/). Otherwise Khoj defaults to use the first chat model in your [ChatModelOptions](http://localhost:42110/server/admin/database/chatmodeloptions/) for all non chat response generation tasks.
|
||||
:::
|
||||
|
||||
##### Configure OpenAI Chat
|
||||
|
||||
:::info[Ollama Integration]
|
||||
Using Ollama? See the [Ollama Integration](/miscellaneous/ollama) section for more custom setup instructions.
|
||||
Using Ollama? See the [Ollama Integration](/advanced/ollama) section for more custom setup instructions.
|
||||
:::
|
||||
|
||||
1. Go to the [OpenAI settings](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/) in the server admin settings to add an OpenAI processor conversation config. This is where you set your API key and server API base URL. The API base URL is optional - it's only relevant if you're using another OpenAI-compatible proxy server.
|
||||
@@ -214,55 +219,45 @@ Using Ollama? See the [Ollama Integration](/miscellaneous/ollama) section for mo
|
||||
- The `tokenizer` and `max-prompt-size` fields are optional. Set them only if you're sure of the tokenizer or token limit for the model you're using. Contact us if you're unsure what to do here.
|
||||
|
||||
##### Configure Offline Chat
|
||||
Any chat model on Huggingface in GGUF format can be used for local chat. Here's how you can set it up:
|
||||
|
||||
1. No need to setup a conversation processor config!
|
||||
2. Go over to configure your [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/). Set the `chat-model` field to a supported chat model[^1] of your choice. For example, we recommend `NousResearch/Hermes-2-Pro-Mistral-7B-GGUF`, but [any gguf model on huggingface](https://huggingface.co/models?library=gguf) should work.
|
||||
2. Go over to configure your [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/). Set the `chat-model` field to a supported chat model[^1] of your choice. For example, we recommend `bartowski/Meta-Llama-3.1-8B-Instruct-GGUF`, but [any gguf model on huggingface](https://huggingface.co/models?library=gguf) should work.
|
||||
- Make sure to set the `model-type` to `Offline`. Do not set `openai config`.
|
||||
- The `tokenizer` and `max-prompt-size` fields are optional. Set them only when using a non-standard model (i.e not mistral, gpt or llama2 model) when you know the token limit.
|
||||
- The `tokenizer` and `max-prompt-size` fields are optional. You can set these for non-standard models (i.e not Mistral or Llama based models) or when you know the token limit of the model to improve context stuffing.
|
||||
|
||||
#### Share your data
|
||||
1. Select files and folders to index [using the desktop client](/get-started/setup#2-download-the-desktop-client). When you click 'Save', the files will be sent to your server for indexing.
|
||||
- Select Notion workspaces and Github repositories to index using the web interface.
|
||||
You can sync your files and folders with Khoj using the [Desktop](/clients/desktop#setup), [Obsidian](/clients/obsidian#setup), or [Emacs](/clients/emacs#setup) clients or just drag and drop specific files on the [website](/clients/web#upload-documents). You can also directly sync your [Notion workspace](/data-sources/notion_integration).
|
||||
|
||||
[^1]: Khoj, by default, can use [OpenAI GPT3.5+ chat models](https://platform.openai.com/docs/models/overview) or [GGUF chat models](https://huggingface.co/models?library=gguf). See [this section](/miscellaneous/advanced#use-openai-compatible-llm-api-server-self-hosting) on how to locally use OpenAI-format compatible proxy servers.
|
||||
[^1]: Khoj, by default, can use [OpenAI GPT3.5+ chat models](https://platform.openai.com/docs/models/overview) or [GGUF chat models](https://huggingface.co/models?library=gguf). See [this section](/advanced/use-openai-proxy) on how to locally use OpenAI-format compatible proxy servers.
|
||||
|
||||
:::tip[Note]
|
||||
Using Safari on Mac? You might not be able to login to the admin panel. Try using Chrome or Firefox instead.
|
||||
:::
|
||||
### 3. Use Khoj 🚀
|
||||
|
||||
### 3. Download the desktop client (Optional)
|
||||
Now open http://localhost:42110 to start interacting with Khoj!
|
||||
|
||||
You can use our desktop executables to select file paths and folders to index. You can simply select the folders or files, and they'll be automatically uploaded to the server. Once you specify a file or file path, you don't need to update the configuration again; it will grab any data diffs dynamically over time.
|
||||
|
||||
**To download the latest desktop client, go to https://download.khoj.dev** and the correct executable for your OS will automatically start downloading. You can also go to https://khoj.dev/downloads to explicitly download your image of choice. Once downloaded, you can configure your folders for indexing using the settings tab. To set your chat configuration, you'll have to use the web interface for the Khoj server you setup in the previous step.
|
||||
|
||||
To use the desktop client, you need to go to your Khoj server's settings page (http://localhost:42110/config) and copy the API key. Then, paste it into the desktop client's settings page. Once you've done that, you can select files and folders to index. Set the desktop client settings to use `http://127.0.0.1:42110` as the host URL.
|
||||
|
||||
|
||||
### 4. Install Client Plugins (Optional)
|
||||
### 4. Install Khoj Clients (Optional)
|
||||
Khoj exposes a web interface to search, chat and configure by default.<br />
|
||||
The optional steps below allow using Khoj from within an existing application like Obsidian or Emacs.
|
||||
You can install a Khoj client to sync your documents or to easily access Khoj from within Obsidian, Emacs or your OS.
|
||||
|
||||
- **Khoj Desktop**:<br />
|
||||
[Install](/clients/desktop#setup) the Khoj Desktop app.
|
||||
|
||||
- **Khoj Obsidian**:<br />
|
||||
[Install](/clients/obsidian#setup) the Khoj Obsidian plugin
|
||||
[Install](/clients/obsidian#setup) the Khoj Obsidian plugin.
|
||||
|
||||
- **Khoj Emacs**:<br />
|
||||
[Install](/clients/emacs#setup) khoj.el
|
||||
|
||||
#### Setup host URL
|
||||
To configure your host URL on your clients when self-hosting, use `http://127.0.0.1:42110`. This is the default port for the Khoj server. Note that `localhost` will not work.
|
||||
|
||||
### 5. Use Khoj 🚀
|
||||
|
||||
You can head to http://localhost:42110 to use the web interface. You can also use the desktop client to search and chat.
|
||||
Set the host URL on your clients settings page to your Khoj server URL. By default, use `http://127.0.0.1:42110` or `http://localhost:42110`. Note that `localhost` may not work in all cases.
|
||||
|
||||
## Upgrade
|
||||
### Upgrade Khoj Server
|
||||
|
||||
```mdx-code-block
|
||||
<Tabs groupId="environment">
|
||||
<TabItem value="localsetup" label="Local Setup">
|
||||
```shell
|
||||
pip install --upgrade khoj-assistant
|
||||
pip install --upgrade khoj
|
||||
```
|
||||
*Note: To upgrade to the latest pre-release version of the khoj server run below command*
|
||||
</TabItem>
|
||||
@@ -284,14 +279,13 @@ You can head to http://localhost:42110 to use the web interface. You can also us
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
### Uninstall Khoj Server
|
||||
|
||||
```mdx-code-block
|
||||
<Tabs groupId="environment">
|
||||
<TabItem value="localsetup" label="Local Setup">
|
||||
```shell
|
||||
# uninstall khoj server
|
||||
pip uninstall khoj-assistant
|
||||
pip uninstall khoj
|
||||
|
||||
# delete khoj postgres db
|
||||
dropdb khoj -U postgres
|
||||
@@ -324,13 +318,13 @@ You can head to http://localhost:42110 to use the web interface. You can also us
|
||||
1. Install [pipx](https://pypa.github.io/pipx/#install-pipx)
|
||||
2. Use `pipx` to install Khoj to avoid dependency conflicts with other python packages.
|
||||
```shell
|
||||
pipx install khoj-assistant
|
||||
pipx install khoj
|
||||
```
|
||||
3. Now start `khoj` using the standard steps described earlier
|
||||
|
||||
|
||||
#### Install fails while building Tokenizer dependency
|
||||
- **Details**: `pip install khoj-assistant` fails while building the `tokenizers` dependency. Complains about Rust.
|
||||
- **Details**: `pip install khoj` fails while building the `tokenizers` dependency. Complains about Rust.
|
||||
- **Fix**: Install Rust to build the tokenizers package. For example on Mac run:
|
||||
```shell
|
||||
brew install rustup
|
||||
@@ -343,3 +337,14 @@ You can head to http://localhost:42110 to use the web interface. You can also us
|
||||
#### Khoj in Docker errors out with \"Killed\" in error message
|
||||
- **Fix**: Increase RAM available to Docker Containers in Docker Settings
|
||||
- **Refer**: [StackOverflow Solution](https://stackoverflow.com/a/50770267), [Configure Resources on Docker for Mac](https://docs.docker.com/desktop/mac/#resources)
|
||||
|
||||
## Advanced
|
||||
### Self Host on Custom Domain
|
||||
|
||||
You can self-host Khoj behind a custom domain as well. To do so, you need to set the `KHOJ_DOMAIN` environment variable to your domain (e.g., `export KHOJ_DOMAIN=my-khoj-domain.com` or add it to your `docker-compose.yml`). By default, the Khoj server you set up will not be accessible outside of `localhost` or `127.0.0.1`.
|
||||
|
||||
:::warning[Without HTTPS certificate]
|
||||
To expose Khoj on a custom domain over the public internet, use of an SSL certificate is strongly recommended. You can use [Let's Encrypt](https://letsencrypt.org/) to get a free SSL certificate for your domain.
|
||||
|
||||
To disable HTTPS, set the `KHOJ_NO_HTTPS` environment variable to `True`. This can be useful if Khoj is only accessible behind a secure, private network.
|
||||
:::
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Miscellaneous",
|
||||
"position": 6,
|
||||
"position": 7,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Additional resources for learning about Khoj"
|
||||
|
||||
@@ -4,14 +4,6 @@ sidebar_position: 3
|
||||
|
||||
# Advanced Usage
|
||||
|
||||
## Search across Different Languages (Self-Hosting)
|
||||
To search for notes in multiple, different languages, you can use a [multi-lingual model](https://www.sbert.net/docs/pretrained_models.html#multi-lingual-models).<br />
|
||||
For example, the [paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) supports [50+ languages](https://www.sbert.net/docs/pretrained_models.html#:~:text=we%20used%20the%20following%2050%2B%20languages), has good search quality and speed. To use it:
|
||||
1. Manually update the search config in server's admin settings page. Go to [the search config](http://localhost:42110/server/admin/database/searchmodelconfig/). Either create a new one, if none exists, or update the existing one. Set the bi_encoder to `sentence-transformers/multi-qa-MiniLM-L6-cos-v1` and the cross_encoder to `mixedbread-ai/mxbai-rerank-xsmall-v1`.
|
||||
2. Regenerate your content index from all the relevant clients. This step is very important, as you'll need to re-encode all your content with the new model.
|
||||
|
||||
Note: If you use a search model that expects a prefix (e.g [mixedbread-ai/mxbai-embed-large-v1](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1)) to the query (or docs) string before encoding. Update the `bi_encoder_query_encode_config` field with `{prompt: <prefix-prompt>}`. Eg. `{prompt: "Represent this query for searching documents"}`. You can pass a valid JSON object that the SentenceTransformer `encode` function accepts
|
||||
|
||||
## Query Filters
|
||||
|
||||
Use structured query syntax to filter entries from your knowledge based used by search results or chat responses.
|
||||
@@ -32,25 +24,3 @@ Use structured query syntax to filter entries from your knowledge based used by
|
||||
- containing dates from the year *1984*
|
||||
- excluding words *"big"* and *"brother"*
|
||||
- that best match the natural language query *"what is the meaning of life?"*
|
||||
|
||||
## Use OpenAI compatible LLM API Server (Self Hosting)
|
||||
Use this if you want to use non-standard, open or commercial, local or hosted LLM models for Khoj chat
|
||||
1. Setup your desired chat LLM by installing an OpenAI compatible LLM API Server like [LiteLLM](https://docs.litellm.ai/docs/proxy/quick_start), [llama-cpp-python](https://github.com/abetlen/llama-cpp-python?tab=readme-ov-file#openai-compatible-web-server)
|
||||
2. Set environment variable `OPENAI_API_BASE="<url-of-your-llm-server>"` before starting Khoj
|
||||
3. Add ChatModelOptions with `model-type` `OpenAI`, and `chat-model` to anything (e.g `gpt-3.5-turbo`) during [Config](/get-started/setup#3-configure)
|
||||
- *(Optional)* Set the `tokenizer` and `max-prompt-size` relevant to the actual chat model you're using
|
||||
|
||||
#### Sample Setup using LiteLLM and Mistral API
|
||||
|
||||
```shell
|
||||
# Install LiteLLM
|
||||
pip install litellm[proxy]
|
||||
|
||||
# Start LiteLLM and use Mistral tiny via Mistral API
|
||||
export MISTRAL_API_KEY=<MISTRAL_API_KEY>
|
||||
litellm --model mistral/mistral-tiny --drop_params
|
||||
|
||||
# Set OpenAI API Base to LiteLLM server URL and start Khoj
|
||||
export OPENAI_API_BASE='http://localhost:8000'
|
||||
khoj --anonymous-mode
|
||||
```
|
||||
|
||||
@@ -5,9 +5,10 @@ sidebar_position: 4
|
||||
# Credits
|
||||
Many Open Source projects are used to power Khoj. Here's a few of them:
|
||||
|
||||
- [Multi-QA MiniLM Model](https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1), [All MiniLM Model](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) for Text Search. See [SBert Documentation](https://www.sbert.net/examples/applications/retrieve_rerank/README.html)
|
||||
- [OpenAI CLIP Model](https://github.com/openai/CLIP) for Image Search. See [SBert Documentation](https://www.sbert.net/examples/applications/image-search/README.html)
|
||||
- [Llama.cpp](https://github.com/ggerganov/llama.cpp) to chat with local LLM
|
||||
- [SentenceTransformer](https://www.sbert.net/examples/applications/retrieve_rerank/README.html) for Text Search
|
||||
- [HuggingFace](https://huggingface.co/) for hosting open-source chat and search models
|
||||
- Charles Cave for [OrgNode Parser](http://members.optusnet.com.au/~charles57/GTD/orgnode.html)
|
||||
- [Org.js](https://mooz.github.io/org-js/) to render Org-mode results on the Web interface
|
||||
- [Markdown-it](https://github.com/markdown-it/markdown-it) to render Markdown results on the Web interface
|
||||
- [Llama.cpp](https://github.com/ggerganov/llama.cpp) to chat with local LLM
|
||||
- [Katex](https://katex.org/) to render math
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Setting up Google Auth
|
||||
|
||||
To set up your self-hosted Khoj with Google Auth, you need to create a project in the Google Cloud Console and enable the Google Auth API.
|
||||
|
||||
|
||||
To implement this, you'll need to:
|
||||
1. You must use the `python` package or build from source, because you'll need to install additional packages for the google auth libraries (`prod`). The syntax to install the right packages is
|
||||
```
|
||||
pip install khoj-assistant[prod]
|
||||
```
|
||||
2. [Create authorization credentials](https://developers.google.com/identity/sign-in/web/sign-in) for your application.
|
||||
3. Go to your [Google cloud console](https://console.developers.google.com/apis/credentials) and create a configuration like below for the relevant `OAuth 2.0 Client IDs` project:
|
||||

|
||||
|
||||
4. Configure these environment variables: `GOOGLE_CLIENT_SECRET`, and `GOOGLE_CLIENT_ID`. You can find these values in the Google cloud console, in the same place where you configured the authorized origins and redirect URIs.
|
||||
|
||||
That's it! That should be all you have to do. Now, when you reload Khoj without `--anonymous-mode`, you should be able to use your Google account to sign in.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Ollama / Khoj
|
||||
|
||||
You can run your own open source models locally with Ollama and use them with Khoj.
|
||||
|
||||
:::info[Ollama Integration]
|
||||
This is only going to be helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
|
||||
:::
|
||||
|
||||
Khoj supports any OpenAI-API compatible server, which includes [Ollama](http://ollama.ai/). Ollama allows you to start a local server with [several popular open-source LLMs](https://ollama.com/library) directly on your own computer. Combined with Khoj, you can chat with these LLMs and use them to search your notes and documents.
|
||||
|
||||
While Khoj also supports local-hosted LLMs downloaded from Hugging Face, the Ollama integration is particularly useful for its ease of setup and multi-model support, especially if you're already using Ollama.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Setup Ollama: https://ollama.com/
|
||||
2. Start your preferred model with Ollama. For example,
|
||||
```bash
|
||||
ollama run llama3
|
||||
```
|
||||
3. Go to Khoj settings at [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/)
|
||||
4. Create a new config.
|
||||
- Name: `ollama`
|
||||
- Api Key: `any string`
|
||||
- Api Base Url: `http://localhost:11434/v1/` (default for Ollama)
|
||||
5. Go to [Chat Model Options](http://localhost:42110/server/admin/database/chatmodeloptions/)
|
||||
6. Create a new config.
|
||||
- Name: `llama3` (replace with the name of your local model)
|
||||
- Model Type: `Openai`
|
||||
- Openai Config: `<the ollama config you created in step 4>`
|
||||
- Max prompt size: `1000` (replace with the max prompt size of your model)
|
||||
7. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
|
||||
|
||||
That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model.
|
||||
@@ -10,7 +10,7 @@ Here are some top-level performance metrics for Khoj. These are rough estimates
|
||||
|
||||
- Semantic search using the bi-encoder is fairly fast at \<100 ms across all content types
|
||||
- Reranking using the cross-encoder is slower at \<2s on 15 results. Tweak `top_k` to tradeoff speed for accuracy of results
|
||||
- Filters in query (e.g by file, word or date) usually add \<20ms to query latency
|
||||
- Filters in query (e.g. by file, word or date) usually add \<20ms to query latency
|
||||
|
||||
### Indexing performance
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {themes as prismThemes} from 'prism-react-renderer';
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Khoj AI',
|
||||
tagline: 'An AI copilot for your Second Brain',
|
||||
tagline: 'Your Second Brain',
|
||||
|
||||
staticDirectories: ['assets'],
|
||||
|
||||
@@ -75,7 +75,6 @@ const config = {
|
||||
({
|
||||
image: 'img/khoj-logo-sideways-500.png',
|
||||
metadata: [
|
||||
{name: 'keywords', content: 'khoj, khoj ai, chatgpt, open ai, open source, productivity'},
|
||||
{name: 'og:title', content: 'Khoj Documentation'},
|
||||
{name: 'og:type', content: 'website'},
|
||||
{name: 'og:site_name', content: 'Khoj Documentation'},
|
||||
@@ -129,18 +128,18 @@ const config = {
|
||||
},
|
||||
{
|
||||
label: 'Features',
|
||||
to: '/features/all_features',
|
||||
to: '/features/all-features',
|
||||
},
|
||||
{
|
||||
label: 'Client Apps',
|
||||
to: '/category/clients',
|
||||
},
|
||||
{
|
||||
label: 'Self-Hosting',
|
||||
label: 'Self-Host',
|
||||
to: '/get-started/setup',
|
||||
},
|
||||
{
|
||||
label: 'Contributing',
|
||||
label: 'Contribute',
|
||||
to: '/contributing/development',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "1.14.0",
|
||||
"version": "1.21.3",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Khoj Inc.",
|
||||
"authorUrl": "https://github.com/khoj-ai",
|
||||
"isDesktopOnly": false
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
FROM ubuntu:jammy
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
|
||||
LABEL org.opencontainers.image.source="https://github.com/khoj-ai/khoj"
|
||||
|
||||
# Install System Dependencies
|
||||
RUN apt update -y && apt -y install python3-pip libsqlite3-0 ffmpeg libsm6 libxext6
|
||||
RUN apt update -y && apt -y install python3-pip libsqlite3-0 ffmpeg libsm6 libxext6 swig curl
|
||||
|
||||
# Install Node.js and Yarn
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
|
||||
RUN apt -y install nodejs
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN apt update && apt -y install yarn
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -20,6 +27,11 @@ COPY . .
|
||||
# Set the PYTHONPATH environment variable in order for it to find the Django app.
|
||||
ENV PYTHONPATH=/app/src:$PYTHONPATH
|
||||
|
||||
# Go to the directory src/interface/web and export the built Next.js assets
|
||||
WORKDIR /app/src/interface/web
|
||||
RUN bash -c "yarn cache clean && yarn install --verbose && yarn ciexport"
|
||||
WORKDIR /app
|
||||
|
||||
# Run the Application
|
||||
# There are more arguments required for the application to run,
|
||||
# but these should be passed in through the docker-compose.yml file.
|
||||
|
||||
@@ -3,11 +3,11 @@ requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "khoj-assistant"
|
||||
description = "An AI copilot for your Second Brain"
|
||||
name = "khoj"
|
||||
description = "Your Second Brain"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-or-later"
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Debanjum Singh Solanky, Saba Imran" },
|
||||
]
|
||||
@@ -27,7 +27,6 @@ classifiers = [
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
@@ -47,12 +46,13 @@ dependencies = [
|
||||
"tiktoken >= 0.3.2",
|
||||
"tenacity >= 8.2.2",
|
||||
"magika ~= 0.5.1",
|
||||
"pillow ~= 9.5.0",
|
||||
"pydantic >= 2.0.0",
|
||||
"pillow ~= 10.0.0",
|
||||
"pydantic[email] >= 2.0.0",
|
||||
"pyyaml ~= 6.0",
|
||||
"rich >= 13.3.1",
|
||||
"schedule == 1.1.0",
|
||||
"sentence-transformers == 2.5.1",
|
||||
"sentence-transformers == 3.0.1",
|
||||
"einops == 0.8.0",
|
||||
"transformers >= 4.28.0",
|
||||
"torch == 2.2.2",
|
||||
"uvicorn == 0.17.6",
|
||||
@@ -64,16 +64,16 @@ dependencies = [
|
||||
"tenacity == 8.3.0",
|
||||
"anyio == 3.7.1",
|
||||
"pymupdf >= 1.23.5",
|
||||
"django == 4.2.11",
|
||||
"django == 5.0.7",
|
||||
"authlib == 1.2.1",
|
||||
"llama-cpp-python == 0.2.76",
|
||||
"llama-cpp-python == 0.2.88",
|
||||
"itsdangerous == 2.1.2",
|
||||
"httpx == 0.25.0",
|
||||
"pgvector == 0.2.4",
|
||||
"psycopg2-binary == 2.9.9",
|
||||
"lxml == 4.9.3",
|
||||
"tzdata == 2023.3",
|
||||
"rapidocr-onnxruntime == 1.3.11; python_version<'3.12'",
|
||||
"rapidocr-onnxruntime == 1.3.22",
|
||||
"openai-whisper >= 20231117",
|
||||
"django-phonenumber-field == 7.3.0",
|
||||
"phonenumbers == 8.13.27",
|
||||
@@ -87,6 +87,7 @@ dependencies = [
|
||||
"cron-descriptor == 1.4.3",
|
||||
"django_apscheduler == 0.6.2",
|
||||
"anthropic == 0.26.1",
|
||||
"docx2txt == 0.8"
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
@@ -108,7 +109,7 @@ prod = [
|
||||
"resend == 1.0.1",
|
||||
]
|
||||
dev = [
|
||||
"khoj-assistant[prod]",
|
||||
"khoj[prod]",
|
||||
"pytest >= 7.1.2",
|
||||
"pytest-xdist[psutil]",
|
||||
"pytest-django == 4.5.2",
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
|
||||
project_root=$PWD
|
||||
|
||||
while getopts 'nc:' opt;
|
||||
while getopts 'nc:t:' opt;
|
||||
do
|
||||
case "${opt}" in
|
||||
c)
|
||||
# Get current project version
|
||||
current_version=$OPTARG
|
||||
t)
|
||||
# Get version type to bump. Options: major, minor, patch
|
||||
version_type=$OPTARG
|
||||
|
||||
# Bump Web app to current version
|
||||
cd $project_root/src/interface/web
|
||||
yarn version --$version_type --no-git-tag-version
|
||||
|
||||
# Bump Desktop app to current version
|
||||
cd $project_root/src/interface/desktop
|
||||
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$current_version\",/" package.json
|
||||
rm *.bak
|
||||
yarn version --$version_type --no-git-tag-version
|
||||
|
||||
# Get bumped project version
|
||||
current_version=$(grep '"version":' package.json | awk -F '"' '{print $4}')
|
||||
|
||||
# Bump Obsidian plugin to current version
|
||||
cd $project_root/src/interface/obsidian
|
||||
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$current_version\",/" package.json
|
||||
sed -E -i.bak "s/version\": \"(.*)\"/version\": \"$current_version\"/" manifest.json
|
||||
yarn build # verify build before bumping version
|
||||
yarn version --$version_type --no-git-tag-version
|
||||
# append current version, min Obsidian app version from manifest to versions json
|
||||
cp $project_root/versions.json .
|
||||
npm run version # append current version
|
||||
rm *.bak
|
||||
yarn run version # run Obsidian version script
|
||||
|
||||
# Bump Emacs package to current version
|
||||
cd ../emacs
|
||||
@@ -38,8 +44,57 @@ do
|
||||
|
||||
# Commit changes and tag commit for 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 master
|
||||
;;
|
||||
c)
|
||||
# Get current project version
|
||||
current_version=$OPTARG
|
||||
|
||||
# Bump Web app to current version
|
||||
cd $project_root/src/interface/web
|
||||
yarn version --new-version $current_version --no-git-tag-version
|
||||
|
||||
# Bump Desktop app to current version
|
||||
cd $project_root/src/interface/desktop
|
||||
yarn version --new-version $current_version --no-git-tag-version
|
||||
|
||||
# Bump Obsidian plugin to current version
|
||||
cd $project_root/src/interface/obsidian
|
||||
yarn version --new-version $current_version --no-git-tag-version
|
||||
# append current version, min Obsidian app version from manifest.json to versions.json
|
||||
cp $project_root/versions.json .
|
||||
yarn run version # run Obsidian version script
|
||||
|
||||
# Bump Emacs package to current 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 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 \
|
||||
@@ -54,17 +109,20 @@ do
|
||||
next_version=$(touch bump.txt && git add bump.txt && hatch version | sed 's/\.dev.*//g')
|
||||
git rm --cached -- bump.txt && rm bump.txt
|
||||
|
||||
# Bump Web app to next version
|
||||
cd $project_root/src/interface/web
|
||||
yarn version --new-version $next_version --no-git-tag-version
|
||||
|
||||
# Bump Desktop app to next version
|
||||
cd $project_root/src/interface/desktop
|
||||
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$current_version\",/" package.json
|
||||
rm *.bak
|
||||
yarn version --new-version $next_version --no-git-tag-version
|
||||
|
||||
# Bump Obsidian plugins to next version
|
||||
cd $project_root/src/interface/obsidian
|
||||
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$next_version\",/" package.json
|
||||
sed -E -i.bak "s/version\": \"(.*)\"/version\": \"$next_version\"/" manifest.json
|
||||
npm run version # updates versions.json
|
||||
rm *.bak
|
||||
yarn version --new-version $next_version --no-git-tag-version
|
||||
# append next version, min Obsidian app version from manifest to versions json
|
||||
git rm --cached -- versions.json
|
||||
yarn run version # run Obsidian version script
|
||||
|
||||
# Bump Emacs package to next version
|
||||
cd $project_root/src/interface/emacs
|
||||
@@ -76,15 +134,17 @@ do
|
||||
|
||||
# Commit changes
|
||||
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
|
||||
git commit -m "Bump Khoj to pre-release version $next_version"
|
||||
;;
|
||||
?)
|
||||
echo -e "Invalid command option.\nUsage: $(basename $0) [-c] [-n]"
|
||||
echo -e "Invalid command option.\nUsage: $(basename $0) [-t] [-c] [-n]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
119
scripts/dev_setup.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
# Initialize the development environment for the project
|
||||
# ---
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
# Install Web App
|
||||
# ---
|
||||
echo "Installing Web App..."
|
||||
cd $PROJECT_ROOT/src/interface/web
|
||||
yarn install
|
||||
|
||||
# Install Obsidian App
|
||||
# ---
|
||||
echo "Installing Obsidian App..."
|
||||
cd $PROJECT_ROOT/src/interface/obsidian
|
||||
yarn install
|
||||
|
||||
# Install Desktop App
|
||||
# ---
|
||||
echo "Installing Desktop App..."
|
||||
cd $PROJECT_ROOT/src/interface/desktop
|
||||
yarn install
|
||||
|
||||
# Install Server App
|
||||
# ---
|
||||
echo "Installing Server App..."
|
||||
cd $PROJECT_ROOT
|
||||
# pip install --user pipenv && pipenv install -e '.[dev]' --skip-lock && pipenv shell
|
||||
python3 -m venv .venv && pip install -e '.[dev]' && . .venv/bin/activate
|
||||
|
||||
# Install pre-commit hooks
|
||||
# ----
|
||||
echo "Installing pre-commit hooks..."
|
||||
|
||||
# Setup pre-commit hooks using the pre-commit package
|
||||
pre-commit install -t pre-push -t pre-commit
|
||||
|
||||
# Run Prettier on web app
|
||||
cat << 'EOF' > temp_pre_commit
|
||||
# Run Prettier for Web App
|
||||
# -------------------------
|
||||
|
||||
# Function to check if color output is possible
|
||||
can_use_color() {
|
||||
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && tput colors >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print colored text if possible
|
||||
print_color() {
|
||||
if can_use_color; then
|
||||
tput setab "$1"
|
||||
printf "%s" "$2"
|
||||
tput sgr0
|
||||
else
|
||||
printf "%s" "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
print_status() {
|
||||
local status="$1"
|
||||
local color="$2"
|
||||
printf "prettier%-64s" "..."
|
||||
print_color "$color" "$status"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel)
|
||||
# Get the list of staged files
|
||||
FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^src/interface/web/' | sed 's| |\\ |g')
|
||||
if [ -z "$FILES" ]; then
|
||||
if [ -t 1 ]; then
|
||||
print_status "Skipped" 6
|
||||
else
|
||||
echo "prettier.....................................................Skipped"
|
||||
fi
|
||||
else
|
||||
# Run prettier on staged files
|
||||
echo "$FILES" | xargs $PROJECT_ROOT/src/interface/web/node_modules/.bin/prettier --ignore-unknown --write
|
||||
|
||||
# Check if any files were modified by prettier
|
||||
MODIFIED=$(git diff --name-only -- $FILES)
|
||||
if [ -n "$MODIFIED" ]; then
|
||||
if [ -t 1 ]; then
|
||||
print_status "Modified" 1
|
||||
else
|
||||
echo "prettier.....................................................Modified"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add back the modified/prettified files to staging
|
||||
# echo "$FILES" | xargs git add
|
||||
|
||||
# Show the user if changes were made
|
||||
if [ -t 1 ]; then
|
||||
print_status "Passed" 2
|
||||
else
|
||||
echo "prettier.....................................................Passed"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Prepend the new content to the existing pre-commit file
|
||||
cat temp_pre_commit "$(git rev-parse --git-dir)/hooks/pre-commit" > temp_combined_pre_commit
|
||||
|
||||
# Replace the old pre-commit file with the new combined one
|
||||
mv temp_combined_pre_commit "$(git rev-parse --git-dir)/hooks/pre-commit"
|
||||
|
||||
# Clean up
|
||||
# ---
|
||||
|
||||
# Remove the temporary pre-commit file
|
||||
rm temp_pre_commit
|
||||
|
||||
# Make sure the pre-commit hook is executable
|
||||
chmod +x "$(git rev-parse --git-dir)/hooks/pre-commit"
|
||||
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 651 B |
BIN
src/interface/desktop/assets/icons/favicon-256x256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/interface/desktop/assets/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 170 KiB |
@@ -13,8 +13,25 @@
|
||||
<link rel="stylesheet" href="https://assets.khoj.dev/higlightjs/solarized-dark.min.css">
|
||||
<script src="https://assets.khoj.dev/higlightjs/highlight.min.js"></script>
|
||||
<script src="./utils.js"></script>
|
||||
|
||||
<script src="chatutils.js"></script>
|
||||
<script>
|
||||
// Add keyboard shortcuts to the chat view
|
||||
window.addEventListener("keydown", function(event) {
|
||||
// If enter key is pressed, send the message
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
chat();
|
||||
// If ^O then Open the chat sessions panel
|
||||
} else if (event.key === "o" && event.ctrlKey) {
|
||||
handleCollapseSidePanel();
|
||||
// If ^N then Create a new conversation
|
||||
} else if (event.key === "n" && event.ctrlKey) {
|
||||
createNewConversation();
|
||||
// If ^D then Delete the conversation history
|
||||
} else if (event.key === "d" && event.ctrlKey) {
|
||||
clearConversationHistory();
|
||||
}
|
||||
});
|
||||
let chatOptions = [];
|
||||
function createCopyParentText(message) {
|
||||
return function(event) {
|
||||
@@ -44,6 +61,14 @@
|
||||
let city = null;
|
||||
let countryName = null;
|
||||
let timezone = null;
|
||||
let chatMessageState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
isVoice: false,
|
||||
}
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
.then(response => response.json())
|
||||
@@ -58,356 +83,9 @@
|
||||
return;
|
||||
});
|
||||
|
||||
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) {
|
||||
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
|
||||
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
|
||||
|
||||
// Escape reference for HTML rendering
|
||||
let escaped_ref = reference.replaceAll('"', '"');
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
let short_ref = escaped_ref.slice(0, 100);
|
||||
short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref;
|
||||
let referenceButton = document.createElement('button');
|
||||
referenceButton.textContent = short_ref;
|
||||
referenceButton.id = `ref-${index}`;
|
||||
referenceButton.classList.add("reference-button");
|
||||
referenceButton.classList.add("collapsed");
|
||||
referenceButton.tabIndex = 0;
|
||||
|
||||
// Add event listener to toggle full reference on click
|
||||
referenceButton.addEventListener('click', function() {
|
||||
if (this.classList.contains("collapsed")) {
|
||||
this.classList.remove("collapsed");
|
||||
this.classList.add("expanded");
|
||||
this.textContent = escaped_ref;
|
||||
} else {
|
||||
this.classList.add("collapsed");
|
||||
this.classList.remove("expanded");
|
||||
this.textContent = short_ref;
|
||||
}
|
||||
});
|
||||
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function generateOnlineReference(reference, index) {
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
let title = reference.title || reference.link;
|
||||
let link = reference.link;
|
||||
let snippet = reference.snippet;
|
||||
let question = reference.question;
|
||||
if (question) {
|
||||
question = `<b>Question:</b> ${question}<br><br>`;
|
||||
} else {
|
||||
question = "";
|
||||
}
|
||||
|
||||
let linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', link);
|
||||
linkElement.setAttribute('target', '_blank');
|
||||
linkElement.setAttribute('rel', 'noopener noreferrer');
|
||||
linkElement.classList.add("inline-chat-link");
|
||||
linkElement.classList.add("reference-link");
|
||||
linkElement.setAttribute('title', title);
|
||||
linkElement.textContent = title;
|
||||
|
||||
let referenceButton = document.createElement('button');
|
||||
referenceButton.innerHTML = linkElement.outerHTML;
|
||||
referenceButton.id = `ref-${index}`;
|
||||
referenceButton.classList.add("reference-button");
|
||||
referenceButton.classList.add("collapsed");
|
||||
referenceButton.tabIndex = 0;
|
||||
|
||||
// Add event listener to toggle full reference on click
|
||||
referenceButton.addEventListener('click', function() {
|
||||
if (this.classList.contains("collapsed")) {
|
||||
this.classList.remove("collapsed");
|
||||
this.classList.add("expanded");
|
||||
this.innerHTML = linkElement.outerHTML + `<br><br>${question + snippet}`;
|
||||
} else {
|
||||
this.classList.add("collapsed");
|
||||
this.classList.remove("expanded");
|
||||
this.innerHTML = linkElement.outerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Create a new div for the chat message
|
||||
let chatMessage = document.createElement('div');
|
||||
chatMessage.className = `chat-message ${by}`;
|
||||
chatMessage.dataset.meta = `${by_name} at ${message_time}`;
|
||||
|
||||
// Create a new div for the chat message text and append it to the chat message
|
||||
let chatMessageText = document.createElement('div');
|
||||
chatMessageText.className = `chat-message-text ${by}`;
|
||||
chatMessageText.appendChild(formattedMessage);
|
||||
chatMessage.appendChild(chatMessageText);
|
||||
|
||||
// Append annotations div to the chat message
|
||||
if (annotations) {
|
||||
chatMessageText.appendChild(annotations);
|
||||
}
|
||||
|
||||
// Append chat message div to chat body
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
if (renderType === "append") {
|
||||
chatBody.appendChild(chatMessage);
|
||||
// Scroll to bottom of chat-body element
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
} else if (renderType === "prepend") {
|
||||
chatBody.insertBefore(chatMessage, chatBody.firstChild);
|
||||
} else if (renderType === "return") {
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
}
|
||||
|
||||
function processOnlineReferences(referenceSection, onlineContext) {
|
||||
let numOnlineReferences = 0;
|
||||
for (let subquery in onlineContext) {
|
||||
let onlineReference = onlineContext[subquery];
|
||||
if (onlineReference.organic && onlineReference.organic.length > 0) {
|
||||
numOnlineReferences += onlineReference.organic.length;
|
||||
for (let index in onlineReference.organic) {
|
||||
let reference = onlineReference.organic[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.knowledgeGraph && onlineReference.knowledgeGraph.length > 0) {
|
||||
numOnlineReferences += onlineReference.knowledgeGraph.length;
|
||||
for (let index in onlineReference.knowledgeGraph) {
|
||||
let reference = onlineReference.knowledgeGraph[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.peopleAlsoAsk && onlineReference.peopleAlsoAsk.length > 0) {
|
||||
numOnlineReferences += onlineReference.peopleAlsoAsk.length;
|
||||
for (let index in onlineReference.peopleAlsoAsk) {
|
||||
let reference = onlineReference.peopleAlsoAsk[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
|
||||
numOnlineReferences += onlineReference.webpages.length;
|
||||
for (let index in onlineReference.webpages) {
|
||||
let reference = onlineReference.webpages[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return numOnlineReferences;
|
||||
}
|
||||
|
||||
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) {
|
||||
let chatEl;
|
||||
if (intentType?.includes("text-to-image")) {
|
||||
let imageMarkdown = generateImageMarkdown(message, intentType, inferredQueries);
|
||||
chatEl = renderMessage(imageMarkdown, by, dt, null, false, "return");
|
||||
} else {
|
||||
chatEl = renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
// If no document or online context is provided, render the message as is
|
||||
if ((context == null || context?.length == 0)
|
||||
&& (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
||||
return chatEl;
|
||||
}
|
||||
|
||||
// If document or online context is provided, render the message with its references
|
||||
let references = {};
|
||||
if (!!context) references["notes"] = context;
|
||||
if (!!onlineContext) references["online"] = onlineContext;
|
||||
let chatMessageEl = chatEl.getElementsByClassName("chat-message-text")[0];
|
||||
chatMessageEl.appendChild(createReferenceSection(references));
|
||||
|
||||
return chatEl;
|
||||
}
|
||||
|
||||
function generateImageMarkdown(message, intentType, inferredQueries=null) {
|
||||
let imageMarkdown;
|
||||
if (intentType === "text-to-image") {
|
||||
imageMarkdown = ``;
|
||||
} else if (intentType === "text-to-image2") {
|
||||
imageMarkdown = ``;
|
||||
} else if (intentType === "text-to-image-v3") {
|
||||
imageMarkdown = ``;
|
||||
}
|
||||
const inferredQuery = inferredQueries?.[0];
|
||||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
return imageMarkdown;
|
||||
}
|
||||
|
||||
function formatHTMLMessage(message, raw=false, willReplace=true) {
|
||||
var md = window.markdownit();
|
||||
let newHTML = message;
|
||||
|
||||
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for the AI chat model.
|
||||
newHTML = newHTML.replace(/<s>\[INST\].+(<\/s>)?/g, '');
|
||||
|
||||
// Customize the rendering of images
|
||||
md.renderer.rules.image = function(tokens, idx, options, env, self) {
|
||||
let token = tokens[idx];
|
||||
|
||||
// Add class="text-to-image" to images
|
||||
token.attrPush(['class', 'text-to-image']);
|
||||
|
||||
// Use the default renderer to render image markdown format
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// Render markdown
|
||||
newHTML = raw ? newHTML : md.render(newHTML);
|
||||
// Sanitize the rendered markdown
|
||||
newHTML = DOMPurify.sanitize(newHTML);
|
||||
// Set rendered markdown to HTML DOM element
|
||||
let element = document.createElement('div');
|
||||
element.innerHTML = newHTML;
|
||||
element.className = "chat-message-text-response";
|
||||
|
||||
// Add a copy button to each chat message
|
||||
if (willReplace === true) {
|
||||
let copyButton = document.createElement('button');
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.title = "Copy Message";
|
||||
let copyIcon = document.createElement("img");
|
||||
copyIcon.src = "./assets/icons/copy-button.svg";
|
||||
copyIcon.classList.add("copy-icon");
|
||||
copyButton.appendChild(copyIcon);
|
||||
copyButton.addEventListener('click', createCopyParentText(message));
|
||||
element.append(copyButton);
|
||||
}
|
||||
|
||||
// Get any elements with a class that starts with "language"
|
||||
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
|
||||
// For each element, add a parent div with the class "programmatic-output"
|
||||
codeBlockElements.forEach((codeElement, key) => {
|
||||
// Create the parent div
|
||||
let parentDiv = document.createElement('div');
|
||||
parentDiv.classList.add("programmatic-output");
|
||||
// Add the parent div before the code element
|
||||
codeElement.parentNode.insertBefore(parentDiv, codeElement);
|
||||
// Move the code element into the parent div
|
||||
parentDiv.appendChild(codeElement);
|
||||
|
||||
// Check if hijs has been loaded
|
||||
if (typeof hljs !== 'undefined') {
|
||||
// Highlight the code block
|
||||
hljs.highlightBlock(codeElement);
|
||||
}
|
||||
|
||||
// Add a copy button to each element
|
||||
if (willReplace === true) {
|
||||
let copyButton = document.createElement('button');
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.title = "Copy Code";
|
||||
let copyIcon = document.createElement("img");
|
||||
copyIcon.src = "./assets/icons/copy-button.svg";
|
||||
copyIcon.classList.add("copy-icon");
|
||||
copyButton.appendChild(copyIcon);
|
||||
copyButton.addEventListener('click', copyParentText);
|
||||
codeElement.prepend(copyButton);
|
||||
}
|
||||
});
|
||||
|
||||
// Get all code elements that have no class.
|
||||
let codeElements = element.querySelectorAll('code:not([class])');
|
||||
codeElements.forEach((codeElement) => {
|
||||
// Add the class "chat-response" to each element
|
||||
codeElement.classList.add("chat-response");
|
||||
});
|
||||
|
||||
let anchorElements = element.querySelectorAll('a');
|
||||
anchorElements.forEach((anchorElement) => {
|
||||
// Tag external links to open in separate window
|
||||
if (
|
||||
!anchorElement.href.startsWith("./") &&
|
||||
!anchorElement.href.startsWith("#") &&
|
||||
!anchorElement.href.startsWith("/")
|
||||
) {
|
||||
anchorElement.setAttribute('target', '_blank');
|
||||
anchorElement.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
// Add the class "inline-chat-link" to each element
|
||||
anchorElement.classList.add("inline-chat-link");
|
||||
});
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
function createReferenceSection(references) {
|
||||
let referenceSection = document.createElement('div');
|
||||
referenceSection.classList.add("reference-section");
|
||||
referenceSection.classList.add("collapsed");
|
||||
|
||||
let numReferences = 0;
|
||||
|
||||
if (references.hasOwnProperty("notes")) {
|
||||
numReferences += references["notes"].length;
|
||||
|
||||
references["notes"].forEach((reference, index) => {
|
||||
let polishedReference = generateReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
});
|
||||
}
|
||||
if (references.hasOwnProperty("online")){
|
||||
numReferences += processOnlineReferences(referenceSection, references["online"]);
|
||||
}
|
||||
|
||||
let referenceExpandButton = document.createElement('button');
|
||||
referenceExpandButton.classList.add("reference-expand-button");
|
||||
referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
|
||||
|
||||
referenceExpandButton.addEventListener('click', function() {
|
||||
if (referenceSection.classList.contains("collapsed")) {
|
||||
referenceSection.classList.remove("collapsed");
|
||||
referenceSection.classList.add("expanded");
|
||||
} else {
|
||||
referenceSection.classList.add("collapsed");
|
||||
referenceSection.classList.remove("expanded");
|
||||
}
|
||||
});
|
||||
|
||||
let referencesDiv = document.createElement('div');
|
||||
referencesDiv.classList.add("references");
|
||||
referencesDiv.appendChild(referenceExpandButton);
|
||||
referencesDiv.appendChild(referenceSection);
|
||||
|
||||
return referencesDiv;
|
||||
}
|
||||
|
||||
async function chat() {
|
||||
// Extract required fields for search from form
|
||||
async function chat(isVoice=false) {
|
||||
// Extract chat message from chat input form
|
||||
let query = document.getElementById("chat-input").value.trim();
|
||||
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
||||
console.log(`Query: ${query}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
@@ -435,9 +113,6 @@
|
||||
await refreshChatSessionsPanel();
|
||||
}
|
||||
|
||||
// Generate backend API URL to execute query
|
||||
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
|
||||
|
||||
let newResponseEl = document.createElement("div");
|
||||
newResponseEl.classList.add("chat-message", "khoj");
|
||||
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||
@@ -448,25 +123,7 @@
|
||||
newResponseEl.appendChild(newResponseTextEl);
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
let loadingEllipsis = document.createElement("div");
|
||||
loadingEllipsis.classList.add("lds-ellipsis");
|
||||
|
||||
let firstEllipsis = document.createElement("div");
|
||||
firstEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let secondEllipsis = document.createElement("div");
|
||||
secondEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let thirdEllipsis = document.createElement("div");
|
||||
thirdEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let fourthEllipsis = document.createElement("div");
|
||||
fourthEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
loadingEllipsis.appendChild(firstEllipsis);
|
||||
loadingEllipsis.appendChild(secondEllipsis);
|
||||
loadingEllipsis.appendChild(thirdEllipsis);
|
||||
loadingEllipsis.appendChild(fourthEllipsis);
|
||||
let loadingEllipsis = createLoadingEllipsis();
|
||||
|
||||
newResponseTextEl.appendChild(loadingEllipsis);
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
@@ -477,107 +134,36 @@
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
chatInput.classList.remove("option-enabled");
|
||||
|
||||
// Setup chat message state
|
||||
chatMessageState = {
|
||||
newResponseTextEl,
|
||||
newResponseEl,
|
||||
loadingEllipsis,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
rawQuery: query,
|
||||
isVoice: isVoice,
|
||||
}
|
||||
|
||||
// Call Khoj chat API
|
||||
let response = await fetch(chatApi, { headers });
|
||||
let rawResponse = "";
|
||||
let references = null;
|
||||
const contentType = response.headers.get("content-type");
|
||||
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
|
||||
chatApi += (!!region && !!city && !!countryName && !!timezone)
|
||||
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
||||
: '';
|
||||
|
||||
if (contentType === "application/json") {
|
||||
// Handle JSON response
|
||||
try {
|
||||
const responseAsJson = await response.json();
|
||||
if (responseAsJson.image) {
|
||||
// If response has image field, response is a generated image.
|
||||
if (responseAsJson.intentType === "text-to-image") {
|
||||
rawResponse += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image2") {
|
||||
rawResponse += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image-v3") {
|
||||
rawResponse += ``;
|
||||
}
|
||||
const inferredQueries = responseAsJson.inferredQueries?.[0];
|
||||
if (inferredQueries) {
|
||||
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQueries}`;
|
||||
}
|
||||
}
|
||||
if (responseAsJson.context) {
|
||||
const rawReferenceAsJson = responseAsJson.context;
|
||||
references = createReferenceSection(rawReferenceAsJson);
|
||||
}
|
||||
if (responseAsJson.detail) {
|
||||
// If response has detail field, response is an error message.
|
||||
rawResponse += responseAsJson.detail;
|
||||
}
|
||||
} catch (error) {
|
||||
// If the chunk is not a JSON object, just display it as is
|
||||
rawResponse += chunk;
|
||||
} finally {
|
||||
newResponseTextEl.innerHTML = "";
|
||||
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
||||
const response = await fetch(chatApi, { headers });
|
||||
|
||||
if (references != null) {
|
||||
newResponseTextEl.appendChild(references);
|
||||
}
|
||||
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
document.getElementById("chat-input").removeAttribute("disabled");
|
||||
}
|
||||
} else {
|
||||
// Handle streamed response of type text/event-stream or text/plain
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let references = {};
|
||||
|
||||
readStream();
|
||||
|
||||
function readStream() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
// Append any references after all the data has been streamed
|
||||
if (references != {}) {
|
||||
newResponseTextEl.appendChild(createReferenceSection(references));
|
||||
}
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
document.getElementById("chat-input").removeAttribute("disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode message chunk from stream
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
if (chunk.includes("### compiled references:")) {
|
||||
const additionalResponse = chunk.split("### compiled references:")[0];
|
||||
rawResponse += additionalResponse;
|
||||
newResponseTextEl.innerHTML = "";
|
||||
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
||||
|
||||
const rawReference = chunk.split("### compiled references:")[1];
|
||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||
if (rawReferenceAsJson instanceof Array) {
|
||||
references["notes"] = rawReferenceAsJson;
|
||||
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
||||
references["online"] = rawReferenceAsJson;
|
||||
}
|
||||
readStream();
|
||||
} else {
|
||||
// Display response from Khoj
|
||||
if (newResponseTextEl.getElementsByClassName("lds-ellipsis").length > 0) {
|
||||
newResponseTextEl.removeChild(loadingEllipsis);
|
||||
}
|
||||
|
||||
// If the chunk is not a JSON object, just display it as is
|
||||
rawResponse += chunk;
|
||||
newResponseTextEl.innerHTML = "";
|
||||
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
||||
|
||||
readStream();
|
||||
}
|
||||
|
||||
// Scroll to bottom of chat window as chat response is streamed
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is empty");
|
||||
// Stream and render chat response
|
||||
await readChatStream(response);
|
||||
} catch (err) {
|
||||
console.error(`Khoj chat response failed with\n${err}`);
|
||||
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
|
||||
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
|
||||
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
|
||||
newResponseTextEl.textContent = errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1892,7 +1478,7 @@
|
||||
div#new-conversation {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
font-size: large;
|
||||
font-size: medium;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--main-text-color);
|
||||
margin: 8px 0;
|
||||
@@ -1910,7 +1496,7 @@
|
||||
border: 1px solid var(--main-text-color);
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
font-size: small;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
cursor: pointer;
|
||||
@@ -1918,7 +1504,6 @@
|
||||
text-align: left;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.three-dot-menu {
|
||||
|
||||
557
src/interface/desktop/chatutils.js
Normal file
@@ -0,0 +1,557 @@
|
||||
function copyParentText(event, message=null) { //same
|
||||
const button = event.currentTarget;
|
||||
const textContent = message ?? button.parentNode.textContent.trim();
|
||||
navigator.clipboard.writeText(textContent).then(() => {
|
||||
button.firstChild.src = "./assets/icons/copy-button-success.svg";
|
||||
setTimeout(() => {
|
||||
button.firstChild.src = "./assets/icons/copy-button.svg";
|
||||
}, 1000);
|
||||
}).catch((error) => {
|
||||
console.error("Error copying text to clipboard:", error);
|
||||
const originalButtonText = button.innerHTML;
|
||||
button.innerHTML = "⛔️";
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalButtonText;
|
||||
button.firstChild.src = "./assets/icons/copy-button.svg";
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function createCopyParentText(message) { //same
|
||||
return function(event) {
|
||||
copyParentText(event, message);
|
||||
}
|
||||
}
|
||||
function formatDate(date) { //same
|
||||
// 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
|
||||
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
|
||||
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
|
||||
|
||||
// Escape reference for HTML rendering
|
||||
let escaped_ref = reference.replaceAll('"', '"');
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
let short_ref = escaped_ref.slice(0, 100);
|
||||
short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref;
|
||||
let referenceButton = document.createElement('button');
|
||||
referenceButton.textContent = short_ref;
|
||||
referenceButton.id = `ref-${index}`;
|
||||
referenceButton.classList.add("reference-button");
|
||||
referenceButton.classList.add("collapsed");
|
||||
referenceButton.tabIndex = 0;
|
||||
|
||||
// Add event listener to toggle full reference on click
|
||||
referenceButton.addEventListener('click', function() {
|
||||
if (this.classList.contains("collapsed")) {
|
||||
this.classList.remove("collapsed");
|
||||
this.classList.add("expanded");
|
||||
this.textContent = escaped_ref;
|
||||
} else {
|
||||
this.classList.add("collapsed");
|
||||
this.classList.remove("expanded");
|
||||
this.textContent = short_ref;
|
||||
}
|
||||
});
|
||||
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function generateOnlineReference(reference, index) { //same
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
let title = reference.title || reference.link;
|
||||
let link = reference.link;
|
||||
let snippet = reference.snippet;
|
||||
let question = reference.question;
|
||||
if (question) {
|
||||
question = `<b>Question:</b> ${question}<br><br>`;
|
||||
} else {
|
||||
question = "";
|
||||
}
|
||||
|
||||
let linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', link);
|
||||
linkElement.setAttribute('target', '_blank');
|
||||
linkElement.setAttribute('rel', 'noopener noreferrer');
|
||||
linkElement.classList.add("inline-chat-link");
|
||||
linkElement.classList.add("reference-link");
|
||||
linkElement.setAttribute('title', title);
|
||||
linkElement.textContent = title;
|
||||
|
||||
let referenceButton = document.createElement('button');
|
||||
referenceButton.innerHTML = linkElement.outerHTML;
|
||||
referenceButton.id = `ref-${index}`;
|
||||
referenceButton.classList.add("reference-button");
|
||||
referenceButton.classList.add("collapsed");
|
||||
referenceButton.tabIndex = 0;
|
||||
|
||||
// Add event listener to toggle full reference on click
|
||||
referenceButton.addEventListener('click', function() {
|
||||
if (this.classList.contains("collapsed")) {
|
||||
this.classList.remove("collapsed");
|
||||
this.classList.add("expanded");
|
||||
this.innerHTML = linkElement.outerHTML + `<br><br>${question + snippet}`;
|
||||
} else {
|
||||
this.classList.add("collapsed");
|
||||
this.classList.remove("expanded");
|
||||
this.innerHTML = linkElement.outerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") { //same
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
let formattedMessage = formatHTMLMessage(message, raw);
|
||||
|
||||
// Create a new div for the chat message
|
||||
let chatMessage = document.createElement('div');
|
||||
chatMessage.className = `chat-message ${by}`;
|
||||
chatMessage.dataset.meta = `${by_name} at ${message_time}`;
|
||||
|
||||
// Create a new div for the chat message text and append it to the chat message
|
||||
let chatMessageText = document.createElement('div');
|
||||
chatMessageText.className = `chat-message-text ${by}`;
|
||||
chatMessageText.appendChild(formattedMessage);
|
||||
chatMessage.appendChild(chatMessageText);
|
||||
|
||||
// Append annotations div to the chat message
|
||||
if (annotations) {
|
||||
chatMessageText.appendChild(annotations);
|
||||
}
|
||||
|
||||
// Append chat message div to chat body
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
let body = document.body;
|
||||
if (renderType === "append") {
|
||||
chatBody.appendChild(chatMessage);
|
||||
// Scroll to bottom of chat-body element
|
||||
body.scrollTop = chatBody.scrollHeight;
|
||||
} else if (renderType === "prepend") {
|
||||
chatBody.insertBefore(chatMessage, chatBody.firstChild);
|
||||
} else if (renderType === "return") {
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
let chatBodyWrapper = document.getElementById("chat-body");
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
}
|
||||
|
||||
function processOnlineReferences(referenceSection, onlineContext) { //same
|
||||
let numOnlineReferences = 0;
|
||||
for (let subquery in onlineContext) {
|
||||
let onlineReference = onlineContext[subquery];
|
||||
if (onlineReference.organic && onlineReference.organic.length > 0) {
|
||||
numOnlineReferences += onlineReference.organic.length;
|
||||
for (let index in onlineReference.organic) {
|
||||
let reference = onlineReference.organic[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.knowledgeGraph && onlineReference.knowledgeGraph.length > 0) {
|
||||
numOnlineReferences += onlineReference.knowledgeGraph.length;
|
||||
for (let index in onlineReference.knowledgeGraph) {
|
||||
let reference = onlineReference.knowledgeGraph[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.peopleAlsoAsk && onlineReference.peopleAlsoAsk.length > 0) {
|
||||
numOnlineReferences += onlineReference.peopleAlsoAsk.length;
|
||||
for (let index in onlineReference.peopleAlsoAsk) {
|
||||
let reference = onlineReference.peopleAlsoAsk[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
|
||||
numOnlineReferences += onlineReference.webpages.length;
|
||||
for (let index in onlineReference.webpages) {
|
||||
let reference = onlineReference.webpages[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return numOnlineReferences;
|
||||
}
|
||||
|
||||
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) { //same
|
||||
let chatEl;
|
||||
if (intentType?.includes("text-to-image")) {
|
||||
let imageMarkdown = generateImageMarkdown(message, intentType, inferredQueries);
|
||||
chatEl = renderMessage(imageMarkdown, by, dt, null, false, "return");
|
||||
} else {
|
||||
chatEl = renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
// If no document or online context is provided, render the message as is
|
||||
if ((context == null || context?.length == 0)
|
||||
&& (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
||||
return chatEl;
|
||||
}
|
||||
|
||||
// If document or online context is provided, render the message with its references
|
||||
let references = {};
|
||||
if (!!context) references["notes"] = context;
|
||||
if (!!onlineContext) references["online"] = onlineContext;
|
||||
let chatMessageEl = chatEl.getElementsByClassName("chat-message-text")[0];
|
||||
chatMessageEl.appendChild(createReferenceSection(references));
|
||||
|
||||
return chatEl;
|
||||
}
|
||||
|
||||
function generateImageMarkdown(message, intentType, inferredQueries=null) { //same
|
||||
let imageMarkdown;
|
||||
if (intentType === "text-to-image") {
|
||||
imageMarkdown = ``;
|
||||
} else if (intentType === "text-to-image2") {
|
||||
imageMarkdown = ``;
|
||||
} else if (intentType === "text-to-image-v3") {
|
||||
imageMarkdown = ``;
|
||||
}
|
||||
const inferredQuery = inferredQueries?.[0];
|
||||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
return imageMarkdown;
|
||||
}
|
||||
|
||||
function formatHTMLMessage(message, raw=false, willReplace=true) { //same
|
||||
var md = window.markdownit();
|
||||
let newHTML = message;
|
||||
|
||||
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for the AI chat model.
|
||||
newHTML = newHTML.replace(/<s>\[INST\].+(<\/s>)?/g, '');
|
||||
|
||||
// Customize the rendering of images
|
||||
md.renderer.rules.image = function(tokens, idx, options, env, self) {
|
||||
let token = tokens[idx];
|
||||
|
||||
// Add class="text-to-image" to images
|
||||
token.attrPush(['class', 'text-to-image']);
|
||||
|
||||
// Use the default renderer to render image markdown format
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// Render markdown
|
||||
newHTML = raw ? newHTML : md.render(newHTML);
|
||||
// Sanitize the rendered markdown
|
||||
newHTML = DOMPurify.sanitize(newHTML);
|
||||
// Set rendered markdown to HTML DOM element
|
||||
let element = document.createElement('div');
|
||||
element.innerHTML = newHTML;
|
||||
element.className = "chat-message-text-response";
|
||||
|
||||
// Add a copy button to each chat message
|
||||
if (willReplace === true) {
|
||||
let copyButton = document.createElement('button');
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.title = "Copy Message";
|
||||
let copyIcon = document.createElement("img");
|
||||
copyIcon.id = "copy-icon";
|
||||
copyIcon.src = "./assets/icons/copy-button.svg";
|
||||
copyIcon.classList.add("copy-icon");
|
||||
copyButton.appendChild(copyIcon);
|
||||
copyButton.addEventListener('click', createCopyParentText(message));
|
||||
element.append(copyButton);
|
||||
}
|
||||
|
||||
// Get any elements with a class that starts with "language"
|
||||
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
|
||||
// For each element, add a parent div with the class "programmatic-output"
|
||||
codeBlockElements.forEach((codeElement, key) => {
|
||||
// Create the parent div
|
||||
let parentDiv = document.createElement('div');
|
||||
parentDiv.classList.add("programmatic-output");
|
||||
// Add the parent div before the code element
|
||||
codeElement.parentNode.insertBefore(parentDiv, codeElement);
|
||||
// Move the code element into the parent div
|
||||
parentDiv.appendChild(codeElement);
|
||||
// Add a copy button to each element
|
||||
});
|
||||
|
||||
// Get all code elements that have no class.
|
||||
let codeElements = element.querySelectorAll('code:not([class])');
|
||||
codeElements.forEach((codeElement) => {
|
||||
// Add the class "chat-response" to each element
|
||||
codeElement.classList.add("chat-response");
|
||||
});
|
||||
|
||||
let anchorElements = element.querySelectorAll('a');
|
||||
anchorElements.forEach((anchorElement) => {
|
||||
// Tag external links to open in separate window
|
||||
if (
|
||||
!anchorElement.href.startsWith("./") &&
|
||||
!anchorElement.href.startsWith("#") &&
|
||||
!anchorElement.href.startsWith("/")
|
||||
) {
|
||||
anchorElement.setAttribute('target', '_blank');
|
||||
anchorElement.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
// Add the class "inline-chat-link" to each element
|
||||
anchorElement.classList.add("inline-chat-link");
|
||||
});
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
function createReferenceSection(references, createLinkerSection=false) {
|
||||
console.log("linker data: ", createLinkerSection);
|
||||
let referenceSection = document.createElement('div');
|
||||
referenceSection.classList.add("reference-section");
|
||||
referenceSection.classList.add("collapsed");
|
||||
|
||||
let numReferences = 0;
|
||||
|
||||
if (references.hasOwnProperty("notes")) {
|
||||
numReferences += references["notes"].length;
|
||||
|
||||
references["notes"].forEach((reference, index) => {
|
||||
let polishedReference = generateReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
});
|
||||
}
|
||||
if (references.hasOwnProperty("online")){
|
||||
numReferences += processOnlineReferences(referenceSection, references["online"]);
|
||||
}
|
||||
|
||||
let referenceExpandButton = document.createElement('button');
|
||||
referenceExpandButton.id = "reference-expand-button";
|
||||
referenceExpandButton.classList.add("reference-expand-button");
|
||||
referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
|
||||
|
||||
referenceExpandButton.addEventListener('click', function() {
|
||||
if (referenceSection.classList.contains("collapsed")) {
|
||||
referenceSection.classList.remove("collapsed");
|
||||
referenceSection.classList.add("expanded");
|
||||
} else {
|
||||
referenceSection.classList.add("collapsed");
|
||||
referenceSection.classList.remove("expanded");
|
||||
}
|
||||
});
|
||||
|
||||
let referencesDiv = document.createElement('div');
|
||||
referencesDiv.classList.add("references");
|
||||
referencesDiv.appendChild(referenceExpandButton);
|
||||
if (createLinkerSection) {
|
||||
//add a linker button back to the desktop application
|
||||
let linkerButton = document.createElement('button');
|
||||
linkerButton.innerHTML = "Continue Conversation";
|
||||
linkerButton.id = "linker-button";
|
||||
linkerButton.addEventListener('click', function() {
|
||||
window.routeBackToMainWindowAPI.sendSignal();
|
||||
});
|
||||
referencesDiv.appendChild(linkerButton);
|
||||
console.log("shortcut window");
|
||||
}
|
||||
referencesDiv.appendChild(referenceSection);
|
||||
|
||||
return referencesDiv;
|
||||
}
|
||||
|
||||
function createLoadingEllipsis() {
|
||||
let loadingEllipsis = document.createElement("div");
|
||||
loadingEllipsis.classList.add("lds-ellipsis");
|
||||
|
||||
let firstEllipsis = document.createElement("div");
|
||||
firstEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let secondEllipsis = document.createElement("div");
|
||||
secondEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let thirdEllipsis = document.createElement("div");
|
||||
thirdEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let fourthEllipsis = document.createElement("div");
|
||||
fourthEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
loadingEllipsis.appendChild(firstEllipsis);
|
||||
loadingEllipsis.appendChild(secondEllipsis);
|
||||
loadingEllipsis.appendChild(thirdEllipsis);
|
||||
loadingEllipsis.appendChild(fourthEllipsis);
|
||||
|
||||
return loadingEllipsis;
|
||||
}
|
||||
|
||||
function handleStreamResponse(newResponseElement, rawResponse, rawQuery, loadingEllipsis, replace=true) {
|
||||
if (!newResponseElement) return;
|
||||
// Remove loading ellipsis if it exists
|
||||
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
|
||||
newResponseElement.removeChild(loadingEllipsis);
|
||||
// Clear the response element if replace is true
|
||||
if (replace) newResponseElement.innerHTML = "";
|
||||
|
||||
// Append response to the response element
|
||||
newResponseElement.appendChild(formatHTMLMessage(rawResponse, false, replace, rawQuery));
|
||||
|
||||
// Append loading ellipsis if it exists
|
||||
if (!replace && loadingEllipsis) newResponseElement.appendChild(loadingEllipsis);
|
||||
// Scroll to bottom of chat view
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
}
|
||||
|
||||
function handleImageResponse(imageJson, rawResponse) {
|
||||
if (imageJson.image) {
|
||||
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
|
||||
|
||||
// If response has image field, response is a generated image.
|
||||
if (imageJson.intentType === "text-to-image") {
|
||||
rawResponse += ``;
|
||||
} else if (imageJson.intentType === "text-to-image2") {
|
||||
rawResponse += ``;
|
||||
} else if (imageJson.intentType === "text-to-image-v3") {
|
||||
rawResponse = ``;
|
||||
}
|
||||
if (inferredQuery) {
|
||||
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If response has detail field, response is an error message.
|
||||
if (imageJson.detail) rawResponse += imageJson.detail;
|
||||
|
||||
return rawResponse;
|
||||
}
|
||||
|
||||
function finalizeChatBodyResponse(references, newResponseElement) {
|
||||
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
|
||||
newResponseElement.appendChild(createReferenceSection(references));
|
||||
}
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
document.getElementById("chat-input")?.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
function convertMessageChunkToJson(rawChunk) {
|
||||
// Split the chunk into lines
|
||||
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
|
||||
try {
|
||||
let jsonChunk = JSON.parse(rawChunk);
|
||||
if (!jsonChunk.type)
|
||||
jsonChunk = {type: 'message', data: jsonChunk};
|
||||
return jsonChunk;
|
||||
} catch (e) {
|
||||
return {type: 'message', data: rawChunk};
|
||||
}
|
||||
} else if (rawChunk.length > 0) {
|
||||
return {type: 'message', data: rawChunk};
|
||||
}
|
||||
}
|
||||
|
||||
function processMessageChunk(rawChunk) {
|
||||
const chunk = convertMessageChunkToJson(rawChunk);
|
||||
console.debug("Chunk:", chunk);
|
||||
if (!chunk || !chunk.type) return;
|
||||
if (chunk.type ==='status') {
|
||||
console.log(`status: ${chunk.data}`);
|
||||
const statusMessage = chunk.data;
|
||||
handleStreamResponse(chatMessageState.newResponseTextEl, statusMessage, chatMessageState.rawQuery, chatMessageState.loadingEllipsis, false);
|
||||
} else if (chunk.type === 'start_llm_response') {
|
||||
console.log("Started streaming", new Date());
|
||||
} else if (chunk.type === 'end_llm_response') {
|
||||
console.log("Stopped streaming", new Date());
|
||||
|
||||
// Automatically respond with voice if the subscribed user has sent voice message
|
||||
if (chatMessageState.isVoice && "{{ is_active }}" == "True")
|
||||
textToSpeech(chatMessageState.rawResponse);
|
||||
|
||||
// Append any references after all the data has been streamed
|
||||
finalizeChatBodyResponse(chatMessageState.references, chatMessageState.newResponseTextEl);
|
||||
|
||||
const liveQuery = chatMessageState.rawQuery;
|
||||
// Reset variables
|
||||
chatMessageState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
rawQuery: liveQuery,
|
||||
isVoice: false,
|
||||
}
|
||||
} else if (chunk.type === "references") {
|
||||
chatMessageState.references = {"notes": chunk.data.context, "online": chunk.data.onlineContext};
|
||||
} else if (chunk.type === 'message') {
|
||||
const chunkData = chunk.data;
|
||||
if (typeof chunkData === 'object' && chunkData !== null) {
|
||||
// If chunkData is already a JSON object
|
||||
handleJsonResponse(chunkData);
|
||||
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
|
||||
// Try process chunk data as if it is a JSON object
|
||||
try {
|
||||
const jsonData = JSON.parse(chunkData.trim());
|
||||
handleJsonResponse(jsonData);
|
||||
} catch (e) {
|
||||
chatMessageState.rawResponse += chunkData;
|
||||
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
||||
}
|
||||
} else {
|
||||
chatMessageState.rawResponse += chunkData;
|
||||
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleJsonResponse(jsonData) {
|
||||
if (jsonData.image || jsonData.detail) {
|
||||
chatMessageState.rawResponse = handleImageResponse(jsonData, chatMessageState.rawResponse);
|
||||
} else if (jsonData.response) {
|
||||
chatMessageState.rawResponse = jsonData.response;
|
||||
}
|
||||
|
||||
if (chatMessageState.newResponseTextEl) {
|
||||
chatMessageState.newResponseTextEl.innerHTML = "";
|
||||
chatMessageState.newResponseTextEl.appendChild(formatHTMLMessage(chatMessageState.rawResponse));
|
||||
}
|
||||
}
|
||||
|
||||
async function readChatStream(response) {
|
||||
if (!response.body) return;
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const eventDelimiter = '␃🔚␗';
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
// If the stream is done
|
||||
if (done) {
|
||||
// Process the last chunk
|
||||
processMessageChunk(buffer);
|
||||
buffer = '';
|
||||
break;
|
||||
}
|
||||
|
||||
// Read chunk from stream and append it to the buffer
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
console.debug("Raw Chunk:", chunk)
|
||||
// Start buffering chunks until complete event is received
|
||||
buffer += chunk;
|
||||
|
||||
// Once the buffer contains a complete event
|
||||
let newEventIndex;
|
||||
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
|
||||
// Extract the event from the buffer
|
||||
const event = buffer.slice(0, newEventIndex);
|
||||
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
||||
|
||||
// Process the event
|
||||
if (event) processMessageChunk(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const textFileTypes = [
|
||||
'org', 'md', 'markdown', 'txt', 'html', 'xml',
|
||||
// Other valid text file extensions from https://google.github.io/magika/model/config.json
|
||||
'appleplist', 'asm', 'asp', 'batch', 'c', 'cs', 'css', 'csv', 'eml', 'go', 'html', 'ini', 'internetshortcut', 'java', 'javascript', 'json', 'latex', 'lisp', 'makefile', 'markdown', 'mht', 'mum', 'pem', 'perl', 'php', 'powershell', 'python', 'rdf', 'rst', 'rtf', 'ruby', 'rust', 'scala', 'shell', 'smali', 'sql', 'svg', 'symlinktext', 'txt', 'vba', 'winregistry', 'xml', 'yaml']
|
||||
const binaryFileTypes = ['pdf']
|
||||
const binaryFileTypes = ['pdf', 'jpg', 'jpeg', 'png']
|
||||
const validFileTypes = textFileTypes.concat(binaryFileTypes);
|
||||
|
||||
const schema = {
|
||||
@@ -233,11 +233,15 @@ function pushDataToKhoj (regenerate = false) {
|
||||
|
||||
// Request indexing files on server. With upto 1000 files in each request
|
||||
for (let i = 0; i < filesDataToPush.length; i += 1000) {
|
||||
const syncUrl = `${hostURL}/api/content?client=desktop`;
|
||||
const filesDataGroup = filesDataToPush.slice(i, i + 1000);
|
||||
const formData = new FormData();
|
||||
filesDataGroup.forEach(fileData => { formData.append('files', fileData.blob, fileData.path) });
|
||||
let request = axios.post(`${hostURL}/api/v1/index/update?force=${regenerate}&client=desktop`, formData, { headers });
|
||||
requests.push(request);
|
||||
requests.push(
|
||||
regenerate
|
||||
? axios.put(syncUrl, formData, { headers })
|
||||
: axios.patch(syncUrl, formData, { headers })
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for requests batch to finish
|
||||
@@ -253,7 +257,7 @@ function pushDataToKhoj (regenerate = false) {
|
||||
console.error(error);
|
||||
state["completed"] = false;
|
||||
if (error?.response?.status === 429 && (BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')))) {
|
||||
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/config">Upgrade your plan</a> to unlock more space.`;
|
||||
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/settings#subscription">Upgrade your plan</a> to unlock more space.`;
|
||||
const win = BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config'));
|
||||
if (win) win.webContents.send('needsSubscription', true);
|
||||
} else if (error?.code === 'ECONNREFUSED') {
|
||||
@@ -431,6 +435,9 @@ function addCSPHeaderToSession () {
|
||||
let firstRun = true;
|
||||
let win = null;
|
||||
let titleBarStyle = process.platform === 'win32' ? 'default' : 'hidden';
|
||||
const {globalShortcut, clipboard} = require('electron'); // global shortcut and clipboard dependencies for shortcut window
|
||||
const openShortcutWindowKeyBind = 'CommandOrControl+Shift+K'
|
||||
|
||||
const createWindow = (tab = 'chat.html') => {
|
||||
win = new BrowserWindow({
|
||||
width: 800,
|
||||
@@ -506,6 +513,48 @@ const createWindow = (tab = 'chat.html') => {
|
||||
}
|
||||
}
|
||||
|
||||
const createShortcutWindow = (tab = 'shortcut.html') => {
|
||||
var shortcutWin = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 600,
|
||||
show: false,
|
||||
titleBarStyle: titleBarStyle,
|
||||
autoHideMenuBar: true,
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
}
|
||||
});
|
||||
shortcutWin.setMenuBarVisibility(false);
|
||||
shortcutWin.setResizable(false);
|
||||
shortcutWin.setOpacity(0.95);
|
||||
shortcutWin.setBackgroundColor('#f5f4f3');
|
||||
shortcutWin.setHasShadow(true);
|
||||
shortcutWin.setVibrancy('popover');
|
||||
|
||||
shortcutWin.loadFile(tab);
|
||||
shortcutWin.once('ready-to-show', () => {
|
||||
shortcutWin.show();
|
||||
});
|
||||
|
||||
shortcutWin.on('closed', () => {
|
||||
shortcutWin = null;
|
||||
});
|
||||
|
||||
return shortcutWin;
|
||||
};
|
||||
|
||||
function isShortcutWindowOpen() {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
for (let i = 0; i < windows.length; i++) {
|
||||
if (windows[i].webContents.getURL().endsWith('shortcut.html')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
addCSPHeaderToSession();
|
||||
|
||||
@@ -551,14 +600,13 @@ app.whenReady().then(() => {
|
||||
});
|
||||
ipcMain.handle('deleteAllFiles', deleteAllFiles);
|
||||
|
||||
createWindow();
|
||||
|
||||
const mainWindow = createWindow();
|
||||
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: "Khoj",
|
||||
applicationVersion: khojPackage.version,
|
||||
version: khojPackage.version,
|
||||
authors: "Saba Imran, Debanjum Singh Solanky and contributors",
|
||||
authors: "Khoj AI",
|
||||
website: "https://khoj.dev",
|
||||
copyright: "GPL v3",
|
||||
iconPath: path.join(__dirname, 'assets', 'icons', 'favicon-128x128.png')
|
||||
@@ -575,9 +623,43 @@ app.whenReady().then(() => {
|
||||
console.warn("Desktop app update check failed:", e);
|
||||
}
|
||||
})
|
||||
globalShortcut.register(openShortcutWindowKeyBind, () => {
|
||||
console.log("Shortcut key pressed")
|
||||
if(isShortcutWindowOpen()) return;
|
||||
|
||||
const shortcutWin = createShortcutWindow(); // Create a new shortcut window each time the shortcut is triggered
|
||||
shortcutWin.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
const clipboardText = clipboard.readText();
|
||||
console.log('Sending clipboard text:', clipboardText); // Debug log
|
||||
shortcutWin.webContents.once('dom-ready', () => {
|
||||
shortcutWin.webContents.send('clip', clipboardText);
|
||||
console.log('Message sent to window'); // Debug log
|
||||
});
|
||||
|
||||
// Register a global shortcut for the Escape key for the shortcutWin
|
||||
globalShortcut.register('Escape', () => {
|
||||
if (shortcutWin) {
|
||||
shortcutWin.close();
|
||||
}
|
||||
// Unregister the Escape key shortcut
|
||||
globalShortcut.unregister('Escape');
|
||||
});
|
||||
|
||||
shortcutWin.on('closed', () => {
|
||||
// Unregister the Escape key shortcut
|
||||
globalShortcut.unregister('Escape');
|
||||
});
|
||||
ipcMain.on('continue-conversation-button-clicked', () => {
|
||||
openWindow('chat.html');
|
||||
if (shortcutWin && !shortcutWin.isDestroyed()) {
|
||||
shortcutWin.close();
|
||||
}
|
||||
// Unregister the Escape key shortcut
|
||||
globalShortcut.unregister('Escape');
|
||||
});
|
||||
});
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.14.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Saba Imran, Debanjum Singh Solanky <team@khoj.dev>",
|
||||
"version": "1.21.3",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Khoj Inc. <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"homepage": "https://khoj.dev",
|
||||
"repository": "\"https://github.com/khoj-ai/khoj\"",
|
||||
@@ -16,8 +16,8 @@
|
||||
"start": "yarn electron ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@todesktop/runtime": "^1.3.0",
|
||||
"axios": "^1.6.4",
|
||||
"@todesktop/runtime": "^1.6.4",
|
||||
"axios": "^1.7.4",
|
||||
"cron": "^2.4.3",
|
||||
"electron-store": "^8.1.0"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
setTitle: (title) => ipcRenderer.send('set-title', title)
|
||||
})
|
||||
|
||||
contextBridge.exposeInMainWorld('clipboardAPI', {
|
||||
sendClipboardText: (callback) => {
|
||||
ipcRenderer.on('clip', (event, message) => {
|
||||
callback(message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('routeBackToMainWindowAPI', {
|
||||
sendSignal: () => {
|
||||
ipcRenderer.send('continue-conversation-button-clicked'); // Custom event name
|
||||
}
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('storeValueAPI', {
|
||||
handleFileOpen: (key) => ipcRenderer.invoke('handleFileOpen', key)
|
||||
})
|
||||
|
||||
@@ -182,7 +182,7 @@ window.updateStateAPI.onUpdateState((event, state) => {
|
||||
window.needsSubscriptionAPI.onNeedsSubscription((event, needsSubscription) => {
|
||||
console.log("needs subscription", needsSubscription);
|
||||
if (needsSubscription) {
|
||||
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/config");
|
||||
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/settings#subscription");
|
||||
needsSubscriptionElement.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -212,12 +212,12 @@
|
||||
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||
|
||||
// Populate type dropdown field with enabled content types only
|
||||
fetch(`${hostURL}/api/config/types`, { headers })
|
||||
fetch(`${hostURL}/api/content/types`, { headers })
|
||||
.then(response => response.json())
|
||||
.then(enabled_types => {
|
||||
// Show warning if no content types are enabled
|
||||
if (enabled_types.detail) {
|
||||
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>.</div>";
|
||||
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/settings'>settings page</a>.</div>";
|
||||
document.getElementById("query").setAttribute("disabled", "disabled");
|
||||
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
|
||||
return [];
|
||||
|
||||
428
src/interface/desktop/shortcut.html
Normal file
@@ -0,0 +1,428 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Khoj Mini</title>
|
||||
<style>
|
||||
#title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #f9f5de;
|
||||
color: black;
|
||||
-webkit-app-region: drag;
|
||||
z-index: 9999;
|
||||
margin: 0;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
#loading-dots {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#styled-input {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
height: 100px;
|
||||
min-height: 50px;
|
||||
background-color: #475569; /* Blue background */
|
||||
color: #dcdfe4; /* White text */
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
border: none;
|
||||
resize: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chat-input {
|
||||
margin-top: 10px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 5;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#input-container {
|
||||
background-color: #475569;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
#send-button {
|
||||
border: none;
|
||||
border-radius: 5px 5px 5px 5px;
|
||||
margin-top: 5px;
|
||||
background-color: #5a6b84;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
/* font-weight: bold; */
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
}
|
||||
#send-button:hover {
|
||||
background: #7489a9;
|
||||
}
|
||||
|
||||
#edit-button {
|
||||
border: none;
|
||||
border-radius: 5px 5px 5px 5px;
|
||||
margin-top: 5px;
|
||||
background-color: #5a6b84;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
/* font-weight: bold; */
|
||||
position: relative;
|
||||
}
|
||||
#edit-button:hover {
|
||||
background: #7489a9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px; /* Width of the scrollbar */
|
||||
height: 5px;
|
||||
}
|
||||
/* * {
|
||||
outline: 1px solid rgb(255, 255, 255);
|
||||
} */
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1; /* Background of the scrollbar track */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2d2d2d; /* Color of the scrollbar thumb */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #000000; /* Color of the scrollbar thumb on hover */
|
||||
}
|
||||
|
||||
#copy-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
#reference-expand-button{
|
||||
background-color: #000000;
|
||||
color: #dcdfe4; /* White text */
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#linker-button{
|
||||
background-color: #fee285;
|
||||
color: black; /* White text */
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
#linker-button:hover {
|
||||
background-color: #f9f5de;
|
||||
}
|
||||
|
||||
|
||||
div.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.expanded {
|
||||
display: block;
|
||||
}
|
||||
/* CSS for the container */
|
||||
.logo-container {
|
||||
/* display: flex; Use flexbox to align items */
|
||||
align-items: center; /* Align items vertically */
|
||||
justify-content: flex;
|
||||
padding: 10px; /* Add padding to the container */
|
||||
}
|
||||
|
||||
/* CSS for the image */
|
||||
img {
|
||||
width: 100px; /* Set the desired width */
|
||||
height: auto; /* Allows the image to scale proportionally */
|
||||
}
|
||||
.clipboardText {
|
||||
font-size: 14px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
background-color: #475569;
|
||||
color: #dcdfe4;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.reference-button {
|
||||
background-color: #dde5f0;
|
||||
color: #dcdfe4;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
#chat-body {
|
||||
width: 100%;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
word-wrap: break-word;
|
||||
line-height: 20px;
|
||||
}
|
||||
b {
|
||||
font-size: 14px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
font-family:'Noto Sans', sans-serif;
|
||||
font-weight: 400;
|
||||
scrollbar-width: 5px; /* "auto" or "thin" */
|
||||
scrollbar-color: white white;/* thumb color and track color */
|
||||
height: 300px;
|
||||
overflow: scroll;
|
||||
}
|
||||
#chat-body-wrapper {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
#loading-dots span {
|
||||
display: inline-block;
|
||||
animation: bounce 0.9s infinite alternate;
|
||||
}
|
||||
|
||||
.dot1 {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.dot2 {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.dot3 {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="title-bar">Khoj (Esc to Quit)</div>
|
||||
<div id="chat-body-wrapper">
|
||||
<div id="input-container">
|
||||
<textarea id="styled-input" name="styled-input">Hello World!</textarea>
|
||||
<script>
|
||||
try {
|
||||
if (!window.clipboardAPI) {
|
||||
throw new Error('clipboardAPI is not available');
|
||||
}
|
||||
|
||||
window.clipboardAPI.sendClipboardText((clipboardText) => {
|
||||
try {
|
||||
const styledInput = document.getElementById('styled-input');
|
||||
if (!styledInput) {
|
||||
throw new Error('styled-input element not found');
|
||||
}
|
||||
styledInput.value = clipboardText;
|
||||
console.log("success: ", clipboardText);
|
||||
} catch (error) {
|
||||
console.error('Error handling clipboard text:', error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up clipboard listener:', error);
|
||||
}
|
||||
</script>
|
||||
<div style="display: flex;">
|
||||
<button id="send-button" onclick="chat()">
|
||||
Send
|
||||
<svg style="margin-left: 3px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send">
|
||||
<path d="M5 12l10 0-5-5m5 5-5 5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="edit-button" onclick="edit()">
|
||||
Edit
|
||||
<svg style="margin-left: 6px; margin-right: 6px;" fill="#fff" height="11px" width="11px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512 512">
|
||||
<g>
|
||||
<g>
|
||||
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loading-dots" style="display: none;"></div>
|
||||
<div id="chat-body"></div>
|
||||
</div>
|
||||
<script src="main.js"></script>
|
||||
<script type="text/javascript" src="./assets/purify.min.js?v={{ khoj_version }}"></script>
|
||||
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
|
||||
<script src="./utils.js"></script>
|
||||
<script src="./chatutils.js"></script>
|
||||
<script>
|
||||
|
||||
let region = null;
|
||||
let city = null;
|
||||
let countryName = null;
|
||||
let timezone = null;
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
region = data.region;
|
||||
city = data.city;
|
||||
countryName = data.country_name;
|
||||
timezone = data.timezone;
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return;
|
||||
});
|
||||
|
||||
function toggleLoading() {
|
||||
var dots = document.getElementById('loading-dots');
|
||||
if (dots.style.display === 'none') {
|
||||
dots.innerHTML = 'Loading<span class="dot1">.</span><span class="dot2">.</span><span class="dot3">.</span>';
|
||||
dots.style.display = 'inline-block';
|
||||
} else {
|
||||
dots.innerHTML = '';
|
||||
dots.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function edit() {
|
||||
//enable input for text area
|
||||
let inp = document.getElementById("styled-input");
|
||||
inp.removeAttribute('readonly');
|
||||
//put focus on text area
|
||||
inp.focus();
|
||||
}
|
||||
|
||||
async function chat(isVoice=false) {
|
||||
//set chat body to empty
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
toggleLoading();
|
||||
let inp = document.getElementById("styled-input");
|
||||
query = inp.value;
|
||||
inp.setAttribute('readonly', true);
|
||||
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
||||
console.log(`Query: ${query}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
if (query.length === 0)
|
||||
return;
|
||||
|
||||
let chat_body = document.getElementById("chat-body");
|
||||
|
||||
let conversationID = chat_body.dataset.conversationId;
|
||||
let hostURL = await window.hostURLAPI.getURL();
|
||||
const khojToken = await window.tokenAPI.getToken();
|
||||
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||
|
||||
if (!conversationID) {
|
||||
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers });
|
||||
let data = await response.json();
|
||||
conversationID = data.conversation_id;
|
||||
chat_body.dataset.conversationId = conversationID;
|
||||
}
|
||||
|
||||
let newResponseEl = document.createElement("div");
|
||||
newResponseEl.classList.add("chat-message", "khoj");
|
||||
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||
chat_body.appendChild(newResponseEl);
|
||||
|
||||
let newResponseTextEl = document.createElement("div");
|
||||
newResponseTextEl.classList.add("chat-message-text", "khoj");
|
||||
newResponseEl.appendChild(newResponseTextEl);
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
let loadingEllipsis = createLoadingEllipsis();
|
||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
|
||||
toggleLoading();
|
||||
|
||||
// Setup chat message state
|
||||
chatMessageState = {
|
||||
newResponseTextEl,
|
||||
newResponseEl,
|
||||
loadingEllipsis,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
rawQuery: query,
|
||||
isVoice: isVoice,
|
||||
}
|
||||
|
||||
// Construct API URL to execute chat query
|
||||
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
|
||||
chatApi += (!!region && !!city && !!countryName && !!timezone)
|
||||
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
||||
: '';
|
||||
|
||||
const response = await fetch(chatApi, { headers });
|
||||
|
||||
try {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is empty");
|
||||
// Stream and render chat response
|
||||
await readChatStream(response);
|
||||
} catch (err) {
|
||||
console.error(`Khoj chat response failed with\n${err}`);
|
||||
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
|
||||
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
|
||||
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
|
||||
newResponseTextEl.textContent = errorMsg;
|
||||
}
|
||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "",
|
||||
"icon": "./assets/icons/favicon-128x128.png",
|
||||
"icon": "./assets/icons/favicon.icns",
|
||||
"appPath": ".",
|
||||
"schemaVersion": 1
|
||||
"schemaVersion": 1,
|
||||
"windows": {
|
||||
"icon": "./assets/icons/favicon-128x128.png"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ function toggleNavMenu() {
|
||||
document.addEventListener('click', function(event) {
|
||||
let menu = document.getElementById("khoj-nav-menu");
|
||||
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
||||
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
||||
if (isClickOnMenu === false && menu.classList.contains("show")) {
|
||||
let isClickOnMenu = menuContainer?.contains(event.target) || menuContainer === event.target;
|
||||
if (menu && isClickOnMenu === false && menu.classList.contains("show")) {
|
||||
menu.classList.remove("show");
|
||||
}
|
||||
});
|
||||
@@ -85,7 +85,7 @@ async function populateHeaderPane() {
|
||||
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
|
||||
<div class="khoj-nav-username"> ${username} </div>
|
||||
<a id="github-nav" class="khoj-nav" href="https://github.com/khoj-ai/khoj">GitHub</a>
|
||||
<a id="settings-nav" class="khoj-nav" href="./config.html">⚙️ Settings</a>
|
||||
<a id="settings-nav" class="khoj-nav" href="./settings.html">⚙️ Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
|
||||
"@electron/get@^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.2.tgz#ae2a967b22075e9c25aaf00d5941cd79c21efd7e"
|
||||
integrity sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960"
|
||||
integrity sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
env-paths "^2.2.0"
|
||||
@@ -50,10 +50,10 @@
|
||||
dependencies:
|
||||
defer-to-connect "^2.0.0"
|
||||
|
||||
"@todesktop/runtime@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@todesktop/runtime/-/runtime-1.3.0.tgz#7baa64fd5c2e4daa591bda96270a0e39947ec3c7"
|
||||
integrity sha512-a5USs4VxnqvtNqFR6F3bCaQ56W6WFO4VOPPaXefCYiCxcsFMYb4IulXGkYjvcpkU/MFGWzmVnzba6UwK7eQMUQ==
|
||||
"@todesktop/runtime@^1.6.4":
|
||||
version "1.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@todesktop/runtime/-/runtime-1.6.4.tgz#a9d62a021cf2647c51371c892bfb1d4c5a29ed7e"
|
||||
integrity sha512-n6dOxhrKKsXMM+i2u9iRvoJSR2KCWw0orYK+FT9RbWNPykhuFIYd0yy8dYgYy/OuClKGyGl4SJFi2757FLhWDA==
|
||||
dependencies:
|
||||
del "^6.0.0"
|
||||
electron-updater "^4.6.1"
|
||||
@@ -73,9 +73,9 @@
|
||||
"@types/responselike" "^1.0.0"
|
||||
|
||||
"@types/http-cache-semantics@*":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
||||
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
||||
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==
|
||||
|
||||
"@types/keyv@^3.1.4":
|
||||
version "3.1.4"
|
||||
@@ -85,36 +85,40 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/luxon@~3.3.0":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.1.tgz#08727da7d81ee6a6c702b9dc6c8f86be010eb4dc"
|
||||
integrity sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==
|
||||
version "3.3.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.8.tgz#84dbf2d020a9209a272058725e168f21d331a67e"
|
||||
integrity sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==
|
||||
|
||||
"@types/node@*":
|
||||
version "20.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377"
|
||||
integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==
|
||||
version "20.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.8.tgz#45c26a2a5de26c3534a9504530ddb3b27ce031ac"
|
||||
integrity sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.12.tgz#c6bd7413a13e6ad9cfb7e97dd5c4e904c1821e50"
|
||||
integrity sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==
|
||||
version "18.19.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.39.tgz#c316340a5b4adca3aee9dcbf05de385978590593"
|
||||
integrity sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/responselike@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
|
||||
integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50"
|
||||
integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/semver@^7.3.6":
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367"
|
||||
integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==
|
||||
version "7.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
||||
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
|
||||
integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
|
||||
version "2.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
|
||||
integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@@ -134,14 +138,14 @@ ajv-formats@^2.1.1:
|
||||
ajv "^8.0.0"
|
||||
|
||||
ajv@^8.0.0, ajv@^8.6.3:
|
||||
version "8.12.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
|
||||
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4"
|
||||
integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-deep-equal "^3.1.3"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
uri-js "^4.4.1"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -163,12 +167,12 @@ atomically@^1.7.0:
|
||||
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
|
||||
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
|
||||
|
||||
axios@^1.6.4:
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8"
|
||||
integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==
|
||||
axios@^1.7.4:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2"
|
||||
integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.4"
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
@@ -190,12 +194,12 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
fill-range "^7.1.1"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
@@ -269,9 +273,9 @@ conf@^10.2.0:
|
||||
semver "^7.3.5"
|
||||
|
||||
cron@^2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/cron/-/cron-2.4.3.tgz#4e43d8d9a6373b8f28d876c4e9a47c14422d8652"
|
||||
integrity sha512-YBvExkQYF7w0PxyeFLRyr817YVDhGxaCi5/uRRMqa4aWD3IFKRd+uNbpW1VWMdqQy8PZ7CElc+accXJcauPKzQ==
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/cron/-/cron-2.4.4.tgz#988c1757b3f288d1dfcc360ee6d80087448916dc"
|
||||
integrity sha512-MHlPImXJj3K7x7lyUHjtKEOl69CSlTOWxS89jiFgNkzXfvhVjhMz/nc7/EIfN9vgooZp8XTtXJ1FREdmbyXOiQ==
|
||||
dependencies:
|
||||
"@types/luxon" "~3.3.0"
|
||||
luxon "~3.3.0"
|
||||
@@ -293,9 +297,9 @@ debounce-fn@^4.0.0:
|
||||
mimic-fn "^3.0.0"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e"
|
||||
integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
@@ -311,11 +315,21 @@ defer-to-connect@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
|
||||
integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==
|
||||
|
||||
define-properties@^1.1.3:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5"
|
||||
integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==
|
||||
define-data-property@^1.0.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
|
||||
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
|
||||
dependencies:
|
||||
es-define-property "^1.0.0"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.0.1"
|
||||
|
||||
define-properties@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
|
||||
integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
|
||||
dependencies:
|
||||
define-data-property "^1.0.1"
|
||||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
@@ -358,9 +372,9 @@ dot-prop@^6.0.1:
|
||||
is-obj "^2.0.0"
|
||||
|
||||
electron-store@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.1.0.tgz#46a398f2bd9aa83c4a9daaae28380e2b3b9c7597"
|
||||
integrity sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.2.0.tgz#114e6e453e8bb746ab4ccb542424d8c881ad2ca1"
|
||||
integrity sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==
|
||||
dependencies:
|
||||
conf "^10.2.0"
|
||||
type-fest "^2.17.0"
|
||||
@@ -400,6 +414,18 @@ env-paths@^2.2.0, env-paths@^2.2.1:
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
|
||||
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
|
||||
|
||||
es-define-property@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
|
||||
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
|
||||
dependencies:
|
||||
get-intrinsic "^1.2.4"
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es6-error@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
||||
@@ -441,15 +467,15 @@ extract-zip@^2.0.1:
|
||||
optionalDependencies:
|
||||
"@types/yauzl" "^2.9.1"
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
|
||||
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
@@ -458,9 +484,9 @@ fast-glob@^3.2.9:
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
|
||||
integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
|
||||
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -471,10 +497,10 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
@@ -485,10 +511,10 @@ find-up@^3.0.0:
|
||||
dependencies:
|
||||
locate-path "^3.0.0"
|
||||
|
||||
follow-redirects@^1.15.4:
|
||||
version "1.15.5"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
|
||||
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.6"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
|
||||
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -522,20 +548,21 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.1.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
|
||||
integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
|
||||
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
|
||||
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
has-proto "^1.0.1"
|
||||
has-symbols "^1.0.3"
|
||||
hasown "^2.0.0"
|
||||
|
||||
get-stream@^5.1.0:
|
||||
version "5.2.0"
|
||||
@@ -581,11 +608,12 @@ global-agent@^3.0.0:
|
||||
serialize-error "^7.0.1"
|
||||
|
||||
globalthis@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
|
||||
integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
|
||||
integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
define-properties "^1.2.1"
|
||||
gopd "^1.0.1"
|
||||
|
||||
globby@^11.0.1:
|
||||
version "11.1.0"
|
||||
@@ -599,6 +627,13 @@ globby@^11.0.1:
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
gopd@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
||||
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
got@^11.8.5:
|
||||
version "11.8.6"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
|
||||
@@ -622,28 +657,28 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
has-property-descriptors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
|
||||
integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
|
||||
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.1"
|
||||
es-define-property "^1.0.0"
|
||||
|
||||
has-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
|
||||
integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
|
||||
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
|
||||
|
||||
has-symbols@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
||||
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
||||
|
||||
has@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
|
||||
hasown@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
http-cache-semantics@^4.0.0:
|
||||
version "4.1.1"
|
||||
@@ -664,9 +699,9 @@ human-signals@^2.1.0:
|
||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
|
||||
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
||||
|
||||
indent-string@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -772,9 +807,9 @@ jsonfile@^6.0.1:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
keyv@^4.0.0:
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25"
|
||||
integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
@@ -811,13 +846,6 @@ lowercase-keys@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
|
||||
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
luxon@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
|
||||
@@ -841,11 +869,11 @@ merge2@^1.3.0, merge2@^1.4.1:
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
|
||||
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
|
||||
dependencies:
|
||||
braces "^3.0.2"
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0:
|
||||
@@ -1010,9 +1038,9 @@ pump@^3.0.0:
|
||||
once "^1.3.1"
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
|
||||
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
@@ -1073,9 +1101,9 @@ run-parallel@^1.1.9:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
sax@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
|
||||
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -1088,11 +1116,9 @@ semver@^6.2.0:
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.2, semver@^7.3.5:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
version "7.6.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
|
||||
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
|
||||
|
||||
serialize-error@^7.0.1:
|
||||
version "7.0.1"
|
||||
@@ -1124,9 +1150,9 @@ slash@^3.0.0:
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
sprintf-js@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
|
||||
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
|
||||
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
|
||||
|
||||
strip-final-newline@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -1157,17 +1183,22 @@ 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==
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||
|
||||
uri-js@^4.2.2:
|
||||
uri-js@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
|
||||
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
|
||||
@@ -1186,11 +1217,6 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
;;; khoj.el --- AI copilot for your Second Brain -*- lexical-binding: t -*-
|
||||
;;; khoj.el --- Your Second Brain -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2021-2023 Khoj Inc.
|
||||
|
||||
;; Author: Debanjum Singh Solanky <debanjum@khoj.dev>
|
||||
;; Saba Imran <saba@khoj.dev>
|
||||
;; Description: An AI copilot for your Second Brain
|
||||
;; Keywords: search, chat, org-mode, outlines, markdown, pdf, image
|
||||
;; Version: 1.14.0
|
||||
;; Description: Your Second Brain
|
||||
;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image
|
||||
;; Version: 1.21.3
|
||||
;; 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
|
||||
|
||||
@@ -29,19 +29,20 @@
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Create an AI copilot to your `org-mode', `markdown' notes,
|
||||
;; PDFs and images. The copilot exposes 2 modes, search and chat:
|
||||
;; Bootstrap your Second Brain from your `org-mode', `markdown' notes,
|
||||
;; PDFs and images. Khoj exposes 2 modes, search and chat:
|
||||
;;
|
||||
;; Chat provides faster answers, iterative discovery and assisted
|
||||
;; creativity. It requires your OpenAI API key to access GPT models
|
||||
;; creativity.
|
||||
;;
|
||||
;; Search allows natural language, incremental and local search.
|
||||
;; It relies on AI models that run locally on your machine.
|
||||
;; Search allows natural language, incremental search.
|
||||
;;
|
||||
;; Quickstart
|
||||
;; -------------
|
||||
;; 1. Install khoj.el from MELPA Stable
|
||||
;; (use-package khoj :pin melpa-stable :bind ("C-c s" . 'khoj))
|
||||
;; 2. Set API key from https://app.khoj.dev/settings#clients (if not self-hosting)
|
||||
;; (setq khoj-api-key "YOUR_KHOJ_API_KEY")
|
||||
;; 2. Start khoj from Emacs
|
||||
;; C-c s or M-x khoj
|
||||
;;
|
||||
@@ -83,7 +84,7 @@
|
||||
:group 'khoj
|
||||
:type 'integer)
|
||||
|
||||
(defcustom khoj-results-count 5
|
||||
(defcustom khoj-results-count 8
|
||||
"Number of results to show in search and use for chat responses."
|
||||
:group 'khoj
|
||||
:type 'integer)
|
||||
@@ -93,8 +94,13 @@
|
||||
:group 'khoj
|
||||
:type 'number)
|
||||
|
||||
(defcustom khoj-auto-find-similar t
|
||||
"Should try find similar notes automatically."
|
||||
:group 'khoj
|
||||
:type 'boolean)
|
||||
|
||||
(defcustom khoj-api-key nil
|
||||
"API Key to your Khoj. Default at https://app.khoj.dev/config#clients."
|
||||
"API Key to your Khoj. Default at https://app.khoj.dev/settings#clients."
|
||||
:group 'khoj
|
||||
:type 'string)
|
||||
|
||||
@@ -158,28 +164,18 @@ NO-PAGING FILTER))
|
||||
|
||||
(defun khoj--keybindings-info-message ()
|
||||
"Show available khoj keybindings in-context, when khoj invoked."
|
||||
(let ((enabled-content-types (khoj--get-enabled-content-types)))
|
||||
(concat
|
||||
"
|
||||
(concat
|
||||
"
|
||||
Set Content Type
|
||||
-------------------------\n"
|
||||
("C-c RET | improve sort \n")
|
||||
(when (member 'markdown enabled-content-types)
|
||||
"C-x m | markdown\n")
|
||||
(when (member 'org enabled-content-types)
|
||||
"C-x o | org-mode\n")
|
||||
(when (member 'image enabled-content-types)
|
||||
"C-x i | image\n")
|
||||
(when (member 'pdf enabled-content-types)
|
||||
"C-x p | pdf\n"))))
|
||||
"C-c RET | improve sort \n"))
|
||||
|
||||
(defvar khoj--rerank nil "Track when re-rank of results triggered.")
|
||||
(defvar khoj--reference-count 0 "Track number of references currently in chat bufffer.")
|
||||
(defun khoj--improve-sort () "Use cross-encoder to improve sorting of search results." (interactive) (khoj--incremental-search t))
|
||||
(defun khoj--make-search-keymap (&optional existing-keymap)
|
||||
"Setup keymap to configure Khoj search. Build of EXISTING-KEYMAP when passed."
|
||||
(let ((enabled-content-types (khoj--get-enabled-content-types))
|
||||
(kmap (or existing-keymap (make-sparse-keymap))))
|
||||
(let ((kmap (or existing-keymap (make-sparse-keymap))))
|
||||
(define-key kmap (kbd "C-c RET") #'khoj--improve-sort)
|
||||
kmap))
|
||||
|
||||
@@ -194,6 +190,9 @@ Use `which-key` if available, else display simple message in echo area"
|
||||
nil t t))
|
||||
(message "%s" (khoj--keybindings-info-message))))
|
||||
|
||||
(defvar khoj--last-heading-pos nil
|
||||
"The last heading position point was in.")
|
||||
|
||||
|
||||
;; ----------------
|
||||
;; Khoj Setup
|
||||
@@ -249,12 +248,12 @@ for example), set this to the full interpreter path."
|
||||
(make-obsolete-variable 'khoj-org-files 'khoj-index-files "1.2.0" 'set)
|
||||
|
||||
(defcustom khoj-index-files (org-agenda-files t t)
|
||||
"List of org, markdown, pdf and other plaintext to index on khoj server."
|
||||
"List of org, md, text, pdf to index on khoj server."
|
||||
:type '(repeat string)
|
||||
:group 'khoj)
|
||||
|
||||
(defcustom khoj-index-directories nil
|
||||
"List of directories with org, markdown, pdf and other plaintext files to index on khoj server."
|
||||
"List of directories with org, md, text, pdf to index on khoj server."
|
||||
:type '(repeat string)
|
||||
:group 'khoj)
|
||||
|
||||
@@ -285,9 +284,9 @@ Auto invokes setup steps on calling main entrypoint."
|
||||
(if (/= (apply #'call-process khoj-server-python-command
|
||||
nil t nil
|
||||
"-m" "pip" "install" "--upgrade"
|
||||
'("khoj-assistant"))
|
||||
'("khoj"))
|
||||
0)
|
||||
(message "khoj.el: Failed to install Khoj server. Please install it manually using pip install `khoj-assistant'.\n%s" (buffer-string))
|
||||
(message "khoj.el: Failed to install Khoj server. Please install it manually using pip install `khoj'.\n%s" (buffer-string))
|
||||
(message "khoj.el: Installed and upgraded Khoj server version: %s" (khoj--server-get-version)))))
|
||||
|
||||
(defun khoj--server-start ()
|
||||
@@ -407,8 +406,10 @@ Auto invokes setup steps on calling main entrypoint."
|
||||
;; This is a temporary change. `khoj-org-directories', `khoj-org-files' are deprecated. They will be removed in a future release
|
||||
(content-directories (or khoj-index-directories khoj-org-directories))
|
||||
(content-files (or khoj-index-files khoj-org-files))
|
||||
(files-to-index (or file-paths
|
||||
(append (mapcan (lambda (dir) (directory-files-recursively dir "\\.\\(org\\|md\\|markdown\\|pdf\\|txt\\|rst\\|xml\\|htm\\|html\\)$")) content-directories) content-files)))
|
||||
(files-to-index (mapcar
|
||||
#'expand-file-name
|
||||
(or file-paths
|
||||
(append (mapcan (lambda (dir) (directory-files-recursively dir "\\.\\(org\\|md\\|markdown\\|pdf\\|txt\\|rst\\|xml\\|htm\\|html\\)$")) content-directories) content-files))))
|
||||
(type-query (if (or (equal content-type "all") (not content-type)) "" (format "t=%s" content-type)))
|
||||
(delete-files (-difference khoj--indexed-files files-to-index))
|
||||
(inhibit-message t)
|
||||
@@ -424,12 +425,12 @@ Auto invokes setup steps on calling main entrypoint."
|
||||
"Send multi-part form `BODY' of `CONTENT-TYPE' in request to khoj server.
|
||||
Append 'TYPE-QUERY' as query parameter in request url.
|
||||
Specify `BOUNDARY' used to separate files in request header."
|
||||
(let ((url-request-method "POST")
|
||||
(let ((url-request-method (if force "PUT" "PATCH"))
|
||||
(url-request-data body)
|
||||
(url-request-extra-headers `(("content-type" . ,(format "multipart/form-data; boundary=%s" boundary))
|
||||
("Authorization" . ,(format "Bearer %s" khoj-api-key)))))
|
||||
(with-current-buffer
|
||||
(url-retrieve (format "%s/api/v1/index/update?%s&force=%s&client=emacs" khoj-server-url type-query (or force "false"))
|
||||
(url-retrieve (format "%s/api/content?%s&client=emacs" khoj-server-url type-query)
|
||||
;; render response from indexing API endpoint on server
|
||||
(lambda (status)
|
||||
(if (not (plist-get status :error))
|
||||
@@ -501,11 +502,19 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
;; -------------------------------------------
|
||||
;; Render Response from Khoj server for Emacs
|
||||
;; -------------------------------------------
|
||||
(defun khoj--construct-find-similar-title (query)
|
||||
"Construct title for find-similar QUERY."
|
||||
(format "Similar to: %s"
|
||||
(replace-regexp-in-string "^[#\\*]* " "" (car (split-string query "\n")))))
|
||||
|
||||
(defun khoj--extract-entries-as-markdown (json-response query)
|
||||
"Convert JSON-RESPONSE, QUERY from API to markdown entries."
|
||||
(defun khoj--extract-entries-as-markdown (json-response query is-find-similar)
|
||||
"Convert JSON-RESPONSE, QUERY from API to markdown entries.
|
||||
Use IS-FIND-SIMILAR bool to filter out first result.
|
||||
As first result is the current entry at point."
|
||||
(thread-last
|
||||
json-response
|
||||
;; filter our first result if is find similar as it'll be the current entry at point
|
||||
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
|
||||
;; Extract and render each markdown entry from response
|
||||
(mapcar (lambda (json-response-item)
|
||||
(thread-last
|
||||
@@ -516,14 +525,18 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
;; Standardize results to 2nd level heading for consistent rendering
|
||||
(replace-regexp-in-string "^\#+" "##"))))
|
||||
;; Render entries into markdown formatted string with query set as as top level heading
|
||||
(format "# %s\n%s" query)
|
||||
(format "# %s\n%s" (if is-find-similar (khoj--construct-find-similar-title query) query))
|
||||
;; remove leading (, ) or SPC from extracted entries string
|
||||
(replace-regexp-in-string "^[\(\) ]" "")))
|
||||
|
||||
(defun khoj--extract-entries-as-org (json-response query)
|
||||
"Convert JSON-RESPONSE, QUERY from API to `org-mode' entries."
|
||||
(defun khoj--extract-entries-as-org (json-response query is-find-similar)
|
||||
"Convert JSON-RESPONSE, QUERY from API to `org-mode' entries.
|
||||
Use IS-FIND-SIMILAR bool to filter out first result.
|
||||
As first result is the current entry at point."
|
||||
(thread-last
|
||||
json-response
|
||||
;; filter our first result if is find similar as it'll be the current entry at point
|
||||
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
|
||||
;; Extract and render each org-mode entry from response
|
||||
(mapcar (lambda (json-response-item)
|
||||
(thread-last
|
||||
@@ -534,14 +547,18 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
;; Standardize results to 2nd level heading for consistent rendering
|
||||
(replace-regexp-in-string "^\*+" "**"))))
|
||||
;; Render entries into org formatted string with query set as as top level heading
|
||||
(format "* %s\n%s\n" query)
|
||||
(format "* %s\n%s\n" (if is-find-similar (khoj--construct-find-similar-title query) query))
|
||||
;; remove leading (, ) or SPC from extracted entries string
|
||||
(replace-regexp-in-string "^[\(\) ]" "")))
|
||||
|
||||
(defun khoj--extract-entries-as-pdf (json-response query)
|
||||
"Convert QUERY, JSON-RESPONSE from API with PDF results to `org-mode' entries."
|
||||
(defun khoj--extract-entries-as-pdf (json-response query is-find-similar)
|
||||
"Convert JSON-RESPONSE, QUERY from API to PDF entries.
|
||||
Use IS-FIND-SIMILAR bool to filter out first result.
|
||||
As first result is the current entry at point."
|
||||
(thread-last
|
||||
json-response
|
||||
;; filter our first result if is find similar as it'll be the current entry at point
|
||||
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
|
||||
;; Extract and render each pdf entry from response
|
||||
(mapcar (lambda (json-response-item)
|
||||
(thread-last
|
||||
@@ -550,7 +567,7 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
;; Format pdf entry as a org entry string
|
||||
(format "** %s\n\n"))))
|
||||
;; Render entries into org formatted string with query set as as top level heading
|
||||
(format "* %s\n%s\n" query)
|
||||
(format "* %s\n%s\n" (if is-find-similar (khoj--construct-find-similar-title query) query))
|
||||
;; remove leading (, ) or SPC from extracted entries string
|
||||
(replace-regexp-in-string "^[\(\) ]" "")))
|
||||
|
||||
@@ -582,9 +599,13 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
;; remove trailing (, ) or SPC from extracted entries string
|
||||
(replace-regexp-in-string "[\(\) ]$" ""))))
|
||||
|
||||
(defun khoj--extract-entries (json-response query)
|
||||
"Convert JSON-RESPONSE, QUERY from API to text entries."
|
||||
(defun khoj--extract-entries (json-response query is-find-similar)
|
||||
"Convert JSON-RESPONSE, QUERY from API to text entries.
|
||||
Use IS-FIND-SIMILAR bool to filter out first result.
|
||||
As first result is the current entry at point."
|
||||
(thread-last json-response
|
||||
;; filter our first result if is find similar as it'll be the current entry at point
|
||||
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
|
||||
;; extract and render entries from API response
|
||||
(mapcar (lambda (json-response-item)
|
||||
(thread-last
|
||||
@@ -598,7 +619,7 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
;; Format entries as org entry string
|
||||
(format "** %s"))))
|
||||
;; Set query as heading in rendered results buffer
|
||||
(format "* %s\n%s\n" query)
|
||||
(format "* %s\n%s\n" (if is-find-similar (khoj--construct-find-similar-title query) query))
|
||||
;; remove leading (, ) or SPC from extracted entries string
|
||||
(replace-regexp-in-string "^[\(\) ]" "")
|
||||
;; remove trailing (, ) or SPC from extracted entries string
|
||||
@@ -614,13 +635,30 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
|
||||
((and (member 'markdown enabled-content-types) (or (equal file-extension "markdown") (equal file-extension "md"))) "markdown")
|
||||
(t khoj-default-content-type))))
|
||||
|
||||
|
||||
(defun khoj--org-cycle-content (&optional arg)
|
||||
"Show all headlines in the buffer, like a table of contents.
|
||||
With numerical argument ARG, show content up to level ARG.
|
||||
|
||||
Simplified fork of `org-cycle-content' from Emacs 29.1 to work with >=27.1."
|
||||
(interactive "p")
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(let ((regexp (if (and (wholenump arg) (> arg 0))
|
||||
(format "^\\*\\{1,%d\\} " arg)
|
||||
"^\\*+ "))
|
||||
(last (point)))
|
||||
(while (re-search-backward regexp nil t)
|
||||
(org-fold-region (line-end-position) last t 'outline)
|
||||
(setq last (line-end-position 0))))))
|
||||
|
||||
|
||||
;; --------------
|
||||
;; Query Khoj API
|
||||
;; --------------
|
||||
(defun khoj--call-api (path &optional method params callback &rest cbargs)
|
||||
"Sync call API at PATH with METHOD and query PARAMS as kv assoc list.
|
||||
Return json parsed response as alist."
|
||||
Optionally apply CALLBACK with JSON parsed response and CBARGS."
|
||||
(let* ((url-request-method (or method "GET"))
|
||||
(url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key))))
|
||||
(param-string (if params (url-build-query-string params) ""))
|
||||
@@ -639,7 +677,7 @@ Return json parsed response as alist."
|
||||
|
||||
(defun khoj--call-api-async (path &optional method params callback &rest cbargs)
|
||||
"Async call to API at PATH with METHOD and query PARAMS as kv assoc list.
|
||||
Return json parsed response as alist."
|
||||
Optionally apply CALLBACK with JSON parsed response and CBARGS."
|
||||
(let* ((url-request-method (or method "GET"))
|
||||
(url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key))))
|
||||
(param-string (if params (url-build-query-string params) ""))
|
||||
@@ -660,42 +698,44 @@ Return json parsed response as alist."
|
||||
|
||||
(defun khoj--get-enabled-content-types ()
|
||||
"Get content types enabled for search from API."
|
||||
(khoj--call-api "/api/config/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
|
||||
(khoj--call-api "/api/content/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
|
||||
|
||||
(defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank title)
|
||||
"Query Khoj Search API with QUERY, CONTENT-TYPE and (optional) RERANK as query params
|
||||
Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional) TITLE."
|
||||
(let ((title (or title query))
|
||||
(rerank (or rerank "false"))
|
||||
(params `((q ,query) (t ,content-type) (r ,rerank) (n ,khoj-results-count)))
|
||||
(path "/api/search"))
|
||||
(defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank is-find-similar)
|
||||
"Query Khoj Search API with QUERY, CONTENT-TYPE and RERANK as query params.
|
||||
Render search results in BUFFER-NAME using CONTENT-TYPE and QUERY.
|
||||
Filter out first similar result if IS-FIND-SIMILAR set."
|
||||
(let* ((rerank (or rerank "false"))
|
||||
(params `((q ,query) (t ,content-type) (r ,rerank) (n ,khoj-results-count)))
|
||||
(path "/api/search"))
|
||||
(khoj--call-api-async path
|
||||
"GET"
|
||||
params
|
||||
'khoj--render-search-results
|
||||
content-type title buffer-name)))
|
||||
content-type query buffer-name is-find-similar)))
|
||||
|
||||
(defun khoj--render-search-results (json-response content-type query buffer-name)
|
||||
(defun khoj--render-search-results (json-response content-type query buffer-name &optional is-find-similar)
|
||||
"Render search results in BUFFER-NAME using JSON-RESPONSE, CONTENT-TYPE, QUERY.
|
||||
Filter out first similar result if IS-FIND-SIMILAR set."
|
||||
;; render json response into formatted entries
|
||||
(with-current-buffer buffer-name
|
||||
(let ((inhibit-read-only t))
|
||||
(let ((is-find-similar (or is-find-similar nil))
|
||||
(inhibit-read-only t))
|
||||
(erase-buffer)
|
||||
(insert
|
||||
(cond ((equal content-type "org") (khoj--extract-entries-as-org json-response query))
|
||||
((equal content-type "markdown") (khoj--extract-entries-as-markdown json-response query))
|
||||
((equal content-type "pdf") (khoj--extract-entries-as-pdf json-response query))
|
||||
(cond ((equal content-type "org") (khoj--extract-entries-as-org json-response query is-find-similar))
|
||||
((equal content-type "markdown") (khoj--extract-entries-as-markdown json-response query is-find-similar))
|
||||
((equal content-type "pdf") (khoj--extract-entries-as-pdf json-response query is-find-similar))
|
||||
((equal content-type "image") (khoj--extract-entries-as-images json-response query))
|
||||
(t (khoj--extract-entries json-response query))))
|
||||
(t (khoj--extract-entries json-response query is-find-similar))))
|
||||
(cond ((or (equal content-type "all")
|
||||
(equal content-type "pdf")
|
||||
(equal content-type "org"))
|
||||
(progn (visual-line-mode)
|
||||
(org-mode)
|
||||
(setq-local
|
||||
org-startup-folded "showall"
|
||||
org-hide-leading-stars t
|
||||
org-startup-with-inline-images t)
|
||||
(org-set-startup-visibility)))
|
||||
(khoj--org-cycle-content 2)))
|
||||
((equal content-type "markdown") (progn (markdown-mode)
|
||||
(visual-line-mode)))
|
||||
((equal content-type "image") (progn (shr-render-region (point-min) (point-max))
|
||||
@@ -712,60 +752,61 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
|
||||
;; Khoj Chat
|
||||
;; ----------------
|
||||
|
||||
(defun khoj--chat ()
|
||||
"Chat with Khoj."
|
||||
(defun khoj--chat (&optional session-id)
|
||||
"Chat with Khoj in session with SESSION-ID."
|
||||
(interactive)
|
||||
(when (not (get-buffer khoj--chat-buffer-name))
|
||||
(khoj--load-chat-session khoj--chat-buffer-name))
|
||||
(khoj--open-side-pane khoj--chat-buffer-name)
|
||||
(when (or session-id (not (get-buffer khoj--chat-buffer-name)))
|
||||
(khoj--load-chat-session khoj--chat-buffer-name session-id))
|
||||
(let ((query (read-string "Query: ")))
|
||||
(when (not (string-empty-p query))
|
||||
(khoj--query-chat-api-and-render-messages query khoj--chat-buffer-name))))
|
||||
(khoj--query-chat-api-and-render-messages query khoj--chat-buffer-name session-id))))
|
||||
|
||||
(defun khoj--open-side-pane (buffer-name)
|
||||
"Open Khoj BUFFER-NAME in right side pane."
|
||||
(if (get-buffer-window-list buffer-name)
|
||||
;; if window is already open, switch to it
|
||||
(progn
|
||||
(select-window (get-buffer-window buffer-name))
|
||||
(switch-to-buffer buffer-name))
|
||||
;; else if window is not open, open it as a right-side window pane
|
||||
(let ((bottomright-window (some-window (lambda (window) (and (window-at-side-p window 'right) (window-at-side-p window 'bottom))))))
|
||||
(progn
|
||||
;; Select the right-most window
|
||||
(select-window bottomright-window)
|
||||
;; if bottom-right window is not a vertical pane, split it vertically, else use the existing bottom-right vertical window
|
||||
(let ((khoj-window (if (window-at-side-p bottomright-window 'left)
|
||||
(split-window-right)
|
||||
bottomright-window)))
|
||||
;; Set the buffer in the khoj window
|
||||
(set-window-buffer khoj-window buffer-name)
|
||||
;; Switch to the khoj window
|
||||
(select-window khoj-window)
|
||||
;; Resize the window to 1/3 of the frame width
|
||||
(window-resize khoj-window
|
||||
(- (truncate (* 0.33 (frame-width))) (window-width))
|
||||
t))))))
|
||||
(save-selected-window
|
||||
(if (get-buffer-window-list buffer-name)
|
||||
;; if window is already open, switch to it
|
||||
(progn
|
||||
(select-window (get-buffer-window buffer-name))
|
||||
(switch-to-buffer buffer-name))
|
||||
;; else if window is not open, open it as a right-side window pane
|
||||
(let ((bottomright-window (some-window (lambda (window) (and (window-at-side-p window 'right) (window-at-side-p window 'bottom))))))
|
||||
(progn
|
||||
;; Select the right-most window
|
||||
(select-window bottomright-window)
|
||||
;; if bottom-right window is not a vertical pane, split it vertically, else use the existing bottom-right vertical window
|
||||
(let ((khoj-window (if (window-at-side-p bottomright-window 'left)
|
||||
(split-window-right)
|
||||
bottomright-window)))
|
||||
;; Set the buffer in the khoj window
|
||||
(set-window-buffer khoj-window buffer-name)
|
||||
;; Switch to the khoj window
|
||||
(select-window khoj-window)
|
||||
;; Resize the window to 1/3 of the frame width
|
||||
(window-resize khoj-window
|
||||
(- (truncate (* 0.33 (frame-width))) (window-width))
|
||||
t)))))
|
||||
(goto-char (point-max))))
|
||||
|
||||
(defun khoj--load-chat-session (buffer-name &optional session-id)
|
||||
"Load Khoj Chat conversation history into BUFFER-NAME."
|
||||
"Load Khoj Chat conversation history from SESSION-ID into BUFFER-NAME."
|
||||
(setq khoj--reference-count 0)
|
||||
(let ((inhibit-read-only t)
|
||||
(json-response (cdr (assoc 'chat (cdr (assoc 'response (khoj--get-chat-session session-id)))))))
|
||||
(with-current-buffer (get-buffer-create buffer-name)
|
||||
(erase-buffer)
|
||||
(insert "* Khoj Chat\n")
|
||||
(when json-response
|
||||
(thread-last
|
||||
json-response
|
||||
;; generate chat messages from Khoj Chat API response
|
||||
(mapcar #'khoj--format-chat-response)
|
||||
;; insert chat messages into Khoj Chat Buffer
|
||||
(mapc #'insert)))
|
||||
(progn
|
||||
(erase-buffer)
|
||||
(insert "* Khoj Chat\n")
|
||||
(when json-response
|
||||
(thread-last
|
||||
json-response
|
||||
;; generate chat messages from Khoj Chat API response
|
||||
(mapcar #'khoj--format-chat-response)
|
||||
;; insert chat messages into Khoj Chat Buffer
|
||||
(mapc #'insert)))
|
||||
(org-mode)
|
||||
(khoj--add-hover-text-to-footnote-refs (point-min))
|
||||
|
||||
;; commented add-hover-text func due to perf issues with the implementation
|
||||
;;(khoj--add-hover-text-to-footnote-refs (point-min))
|
||||
;; render reference footnotes as superscript
|
||||
(setq-local
|
||||
org-startup-folded "showall"
|
||||
@@ -783,10 +824,11 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
|
||||
|
||||
;; enable minor modes for khoj chat
|
||||
(visual-line-mode)
|
||||
(read-only-mode t)))))
|
||||
(read-only-mode t)))
|
||||
(khoj--open-side-pane buffer-name)))
|
||||
|
||||
(defun khoj--close ()
|
||||
"Kill Khoj buffer and window"
|
||||
"Kill Khoj buffer and window."
|
||||
(interactive)
|
||||
(progn
|
||||
(kill-buffer (current-buffer))
|
||||
@@ -816,8 +858,8 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
|
||||
;; show definition on hover on footnote reference
|
||||
(overlay-put overlay 'help-echo it)))))))
|
||||
|
||||
(defun khoj--query-chat-api-and-render-messages (query buffer-name)
|
||||
"Send QUERY to Khoj Chat. Render the chat messages from exchange in BUFFER-NAME."
|
||||
(defun khoj--query-chat-api-and-render-messages (query buffer-name &optional session-id)
|
||||
"Send QUERY to Chat SESSION-ID. Render the chat messages in BUFFER-NAME."
|
||||
;; render json response into formatted chat messages
|
||||
(with-current-buffer (get-buffer buffer-name)
|
||||
(let ((inhibit-read-only t)
|
||||
@@ -826,16 +868,19 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
|
||||
(insert
|
||||
(khoj--render-chat-message query "you" query-time))
|
||||
(khoj--query-chat-api query
|
||||
session-id
|
||||
#'khoj--format-chat-response
|
||||
#'khoj--render-chat-response buffer-name))))
|
||||
|
||||
(defun khoj--query-chat-api (query callback &rest cbargs)
|
||||
"Send QUERY to Khoj Chat API and call CALLBACK with the response.
|
||||
CBARGS are optional additional arguments to pass to CALLBACK."
|
||||
(khoj--call-api-async "/api/chat"
|
||||
"GET"
|
||||
`(("q" ,query) ("n" ,khoj-results-count))
|
||||
callback cbargs))
|
||||
(defun khoj--query-chat-api (query session-id callback &rest cbargs)
|
||||
"Send QUERY for SESSION-ID to Khoj Chat API.
|
||||
Call CALLBACK func with response and CBARGS."
|
||||
(let ((params `(("q" ,query) ("n" ,khoj-results-count))))
|
||||
(when session-id (push `("conversation_id" ,session-id) params))
|
||||
(khoj--call-api-async "/api/chat"
|
||||
"GET"
|
||||
params
|
||||
callback cbargs)))
|
||||
|
||||
(defun khoj--get-chat-sessions ()
|
||||
"Get all chat sessions from Khoj server."
|
||||
@@ -863,8 +908,7 @@ CBARGS are optional additional arguments to pass to CALLBACK."
|
||||
(defun khoj--open-conversation-session ()
|
||||
"Menu to select Khoj conversation session to open."
|
||||
(let ((selected-session-id (khoj--select-conversation-session "Open")))
|
||||
(khoj--load-chat-session khoj--chat-buffer-name selected-session-id)
|
||||
(khoj--open-side-pane khoj--chat-buffer-name)))
|
||||
(khoj--load-chat-session khoj--chat-buffer-name selected-session-id)))
|
||||
|
||||
(defun khoj--create-chat-session ()
|
||||
"Create new chat session."
|
||||
@@ -872,21 +916,21 @@ CBARGS are optional additional arguments to pass to CALLBACK."
|
||||
|
||||
(defun khoj--new-conversation-session ()
|
||||
"Create new Khoj conversation session."
|
||||
(let* ((session (khoj--create-chat-session))
|
||||
(new-session-id (cdr (assoc 'conversation_id session))))
|
||||
(khoj--load-chat-session khoj--chat-buffer-name new-session-id)
|
||||
(khoj--open-side-pane khoj--chat-buffer-name)))
|
||||
(thread-last
|
||||
(khoj--create-chat-session)
|
||||
(assoc 'conversation_id)
|
||||
(cdr)
|
||||
(khoj--chat)))
|
||||
|
||||
(defun khoj--delete-chat-session (session-id)
|
||||
"Delete new chat session."
|
||||
"Delete chat session with SESSION-ID."
|
||||
(khoj--call-api "/api/chat/history" "DELETE" `(("conversation_id" ,session-id))))
|
||||
|
||||
(defun khoj--delete-conversation-session ()
|
||||
"Delete new Khoj conversation session."
|
||||
(let* ((selected-session-id (khoj--select-conversation-session "Delete"))
|
||||
(session (khoj--delete-chat-session selected-session-id)))
|
||||
(khoj--load-chat-session khoj--chat-buffer-name)
|
||||
(khoj--open-side-pane khoj--chat-buffer-name)))
|
||||
(thread-last
|
||||
(khoj--select-conversation-session "Delete")
|
||||
(khoj--delete-chat-session)))
|
||||
|
||||
(defun khoj--render-chat-message (message sender &optional receive-date)
|
||||
"Render chat messages as `org-mode' list item.
|
||||
@@ -923,10 +967,11 @@ RECEIVE-DATE is the message receive date."
|
||||
(format "\n[fn:%x] %s" khoj--reference-count)))))
|
||||
|
||||
(defun khoj--generate-online-reference (reference)
|
||||
"Create `org-mode' footnotes for online REFERENCE."
|
||||
(setq khoj--reference-count (1+ khoj--reference-count))
|
||||
(let ((link (cdr (assoc 'link reference)))
|
||||
(title (cdr (assoc 'title reference)))
|
||||
(description (cdr (assoc 'description reference))))
|
||||
(let* ((link (cdr (assoc 'link reference)))
|
||||
(title (or (cdr (assoc 'title reference)) link))
|
||||
(description (or (cdr (assoc 'description reference)) title)))
|
||||
(cons
|
||||
(propertize (format "^{ [fn:%x]}" khoj--reference-count) 'help-echo (format "%s\n%s" link description))
|
||||
(thread-last
|
||||
@@ -935,8 +980,8 @@ RECEIVE-DATE is the message receive date."
|
||||
(replace-regexp-in-string "\n\n" "\n")
|
||||
(format "\n[fn:%x] [[%s][%s]]\n%s\n" khoj--reference-count link title)))))
|
||||
|
||||
(defun khoj--extract-online-references (result-types searches)
|
||||
"Extract link, title, and description of specified RESULT-TYPES from SEARCHES."
|
||||
(defun khoj--extract-online-references (result-types query-result-pairs)
|
||||
"Extract link, title and description from RESULT-TYPES in QUERY-RESULT-PAIRS."
|
||||
(let ((result '()))
|
||||
(-map
|
||||
(lambda (search)
|
||||
@@ -949,19 +994,22 @@ RECEIVE-DATE is the message receive date."
|
||||
(lambda (search-result)
|
||||
(-map
|
||||
(lambda (entry)
|
||||
(let ((link (cdr (or (assoc 'link entry) (assoc 'descriptionLink entry))))
|
||||
(title (cdr (or (assoc 'title entry) '(title . ,link))))
|
||||
(description (cdr (or (assoc 'snippet entry) (assoc 'description entry)))))
|
||||
(let* ((link (cdr (or (assoc 'link entry) (assoc 'descriptionLink entry))))
|
||||
(title (cdr (or (assoc 'title entry) `(title . ,link))))
|
||||
(description (cdr (or (assoc 'snippet entry) (assoc 'description entry)))))
|
||||
(setq result (append result `(((title . ,title) (link . ,link) (description . ,description) (search . ,search-q)))))))
|
||||
;; wrap search results in a list if it is not already a list
|
||||
(if (or (equal 'knowledgeGraph (car search-result)) (equal 'webpages (car search-result)))
|
||||
(list (cdr search-result))
|
||||
(if (arrayp (cdr search-result))
|
||||
(list (elt (cdr search-result) 0))
|
||||
(list (cdr search-result)))
|
||||
(cdr search-result))))
|
||||
search-results)))
|
||||
searches)
|
||||
query-result-pairs)
|
||||
result))
|
||||
|
||||
(defun khoj--render-chat-response (response buffer-name)
|
||||
"Insert chat message from RESPONSE into BUFFER-NAME."
|
||||
(with-current-buffer (get-buffer buffer-name)
|
||||
(let ((start-pos (point))
|
||||
(inhibit-read-only t))
|
||||
@@ -975,7 +1023,8 @@ RECEIVE-DATE is the message receive date."
|
||||
(re-search-backward "^\*+ 🏮" nil t)))))
|
||||
|
||||
(defun khoj--format-chat-response (json-response &optional callback &rest cbargs)
|
||||
"Render chat message using JSON-RESPONSE from Khoj Chat API."
|
||||
"Format chat message using JSON-RESPONSE from Khoj Chat API.
|
||||
Run CALLBACK with CBARGS on formatted message."
|
||||
(let* ((message (cdr (or (assoc 'response json-response) (assoc 'message json-response))))
|
||||
(sender (cdr (assoc 'by json-response)))
|
||||
(receive-date (cdr (assoc 'created json-response)))
|
||||
@@ -1087,6 +1136,16 @@ RECEIVE-DATE is the message receive date."
|
||||
;; Similar Search
|
||||
;; --------------
|
||||
|
||||
(defun khoj--get-current-outline-entry-pos ()
|
||||
"Get heading position of current outline section."
|
||||
;; get heading position of current outline entry
|
||||
(cond
|
||||
;; when at heading of entry
|
||||
((looking-at outline-regexp)
|
||||
(point))
|
||||
;; when within entry
|
||||
(t (save-excursion (outline-previous-heading) (point)))))
|
||||
|
||||
(defun khoj--get-current-outline-entry-text ()
|
||||
"Get text under current outline section."
|
||||
(string-trim
|
||||
@@ -1130,10 +1189,6 @@ Paragraph only starts at first text after blank line."
|
||||
;; get paragraph, if in text mode
|
||||
(t
|
||||
(khoj--get-current-paragraph-text))))
|
||||
;; extract heading to show in result buffer from query
|
||||
(query-title
|
||||
(format "Similar to: %s"
|
||||
(replace-regexp-in-string "^[#\\*]* " "" (car (split-string query "\n")))))
|
||||
(buffer-name (get-buffer-create khoj--search-buffer-name)))
|
||||
(progn
|
||||
(khoj--query-search-api-and-render-results
|
||||
@@ -1141,9 +1196,35 @@ Paragraph only starts at first text after blank line."
|
||||
content-type
|
||||
buffer-name
|
||||
rerank
|
||||
query-title)
|
||||
(khoj--open-side-pane buffer-name)
|
||||
(goto-char (point-min)))))
|
||||
t)
|
||||
(khoj--open-side-pane buffer-name))))
|
||||
|
||||
(defun khoj--auto-find-similar ()
|
||||
"Call find similar on current element, if point has moved to a new element."
|
||||
;; Call find similar
|
||||
(when (and (derived-mode-p 'org-mode)
|
||||
(org-element-at-point)
|
||||
(not (string= (buffer-name (current-buffer)) khoj--search-buffer-name))
|
||||
(get-buffer-window khoj--search-buffer-name))
|
||||
(let ((current-heading-pos (khoj--get-current-outline-entry-pos)))
|
||||
(unless (eq current-heading-pos khoj--last-heading-pos)
|
||||
(setq khoj--last-heading-pos current-heading-pos)
|
||||
(khoj--find-similar)))))
|
||||
|
||||
(defun khoj--setup-auto-find-similar ()
|
||||
"Setup automatic call to find similar to current element."
|
||||
(if khoj-auto-find-similar
|
||||
(add-hook 'post-command-hook #'khoj--auto-find-similar)
|
||||
(remove-hook 'post-command-hook #'khoj--auto-find-similar)))
|
||||
|
||||
(defun khoj-toggle-auto-find-similar ()
|
||||
"Toggle automatic call to find similar to current element."
|
||||
(interactive)
|
||||
(setq khoj-auto-find-similar (not khoj-auto-find-similar))
|
||||
(khoj--setup-auto-find-similar)
|
||||
(if khoj-auto-find-similar
|
||||
(message "Auto find similar enabled")
|
||||
(message "Auto find similar disabled")))
|
||||
|
||||
|
||||
;; ---------
|
||||
@@ -1185,7 +1266,7 @@ Paragraph only starts at first text after blank line."
|
||||
(transient-define-suffix khoj--update-command (&optional args)
|
||||
"Call khoj API to update index of specified content type."
|
||||
(interactive (list (transient-args transient-current-command)))
|
||||
(let* ((force-update (if (member "--force-update" args) "true" "false"))
|
||||
(let* ((force-update (if (member "--force-update" args) t nil))
|
||||
;; set content type to: specified > last used > based on current buffer > default type
|
||||
(content-type (or (transient-arg-value "--content-type=" args) (khoj--buffer-name-to-content-type (buffer-name))))
|
||||
(url-request-method "GET"))
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
")))
|
||||
(should
|
||||
(equal
|
||||
(khoj--extract-entries-as-markdown json-response-from-khoj-backend user-query)
|
||||
(khoj--extract-entries-as-markdown json-response-from-khoj-backend user-query nil)
|
||||
"\
|
||||
# Become God\n\
|
||||
## Upgrade\n\
|
||||
@@ -100,7 +100,7 @@ Rule everything\n\n"))))
|
||||
")))
|
||||
(should
|
||||
(equal
|
||||
(khoj--extract-entries-as-org json-response-from-khoj-backend user-query)
|
||||
(khoj--extract-entries-as-org json-response-from-khoj-backend user-query nil)
|
||||
"\
|
||||
* Become God\n\
|
||||
** Upgrade\n\
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "1.14.0",
|
||||
"version": "1.21.3",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Khoj Inc.",
|
||||
"authorUrl": "https://github.com/khoj-ai",
|
||||
"isDesktopOnly": false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.14.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"version": "1.21.3",
|
||||
"description": "Your Second Brain",
|
||||
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"main": "src/main.js",
|
||||
@@ -14,13 +14,14 @@
|
||||
"search",
|
||||
"chat",
|
||||
"AI",
|
||||
"assistant"
|
||||
"assistant",
|
||||
"second brain"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.14.47",
|
||||
"obsidian": "latest",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MarkdownRenderer, WorkspaceLeaf, request, requestUrl, setIcon } from 'obsidian';
|
||||
import {ItemView, MarkdownRenderer, Scope, WorkspaceLeaf, request, requestUrl, setIcon, Platform} from 'obsidian';
|
||||
import * as DOMPurify from 'dompurify';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { KhojPaneView } from 'src/pane_view';
|
||||
import { KhojView, createCopyParentText, getLinkToEntry, pasteTextAtCursor } from 'src/utils';
|
||||
import { KhojSearchModal } from './search_modal';
|
||||
|
||||
export interface ChatJsonResult {
|
||||
image?: string;
|
||||
@@ -11,6 +12,25 @@ export interface ChatJsonResult {
|
||||
inferredQueries?: string[];
|
||||
}
|
||||
|
||||
interface ChunkResult {
|
||||
objects: string[];
|
||||
remainder: string;
|
||||
}
|
||||
|
||||
interface MessageChunk {
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface ChatMessageState {
|
||||
newResponseTextEl: HTMLElement | null;
|
||||
newResponseEl: HTMLElement | null;
|
||||
loadingEllipsis: HTMLElement | null;
|
||||
references: any;
|
||||
rawResponse: string;
|
||||
rawQuery: string;
|
||||
isVoice: boolean;
|
||||
}
|
||||
|
||||
interface Location {
|
||||
region: string;
|
||||
@@ -24,10 +44,23 @@ export class KhojChatView extends KhojPaneView {
|
||||
setting: KhojSetting;
|
||||
waitingForLocation: boolean;
|
||||
location: Location;
|
||||
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
|
||||
private currentUserInput: string = ""; // Stores the current user input that is being typed in chat
|
||||
private startingMessage: string = "Message";
|
||||
chatMessageState: ChatMessageState;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf, setting);
|
||||
|
||||
// Register chat view keybindings
|
||||
this.scope = new Scope(this.app.scope);
|
||||
this.scope.register(["Ctrl"], 'n', (_) => this.createNewConversation());
|
||||
this.scope.register(["Ctrl"], 'o', async (_) => await this.toggleChatSessions());
|
||||
this.scope.register(["Ctrl"], 'f', (_) => new KhojSearchModal(this.app, this.setting).open());
|
||||
this.scope.register(["Ctrl"], 'r', (_) => new KhojSearchModal(this.app, this.setting, true).open());
|
||||
|
||||
this.waitingForLocation = true;
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
@@ -61,18 +94,25 @@ export class KhojChatView extends KhojPaneView {
|
||||
return "message-circle";
|
||||
}
|
||||
|
||||
async chat() {
|
||||
|
||||
async chat(isVoice: boolean = false) {
|
||||
// Get text in chat input element
|
||||
let input_el = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
|
||||
// Clear text after extracting message to send
|
||||
let user_message = input_el.value.trim();
|
||||
// Store the message in the array if it's not empty
|
||||
if (user_message) {
|
||||
this.userMessages.push(user_message);
|
||||
// Update starting message after sending a new message
|
||||
const modifierKey = Platform.isMacOS ? '⌘' : '^';
|
||||
this.startingMessage = `(${modifierKey}+↑/↓) for prev messages`;
|
||||
input_el.placeholder = this.startingMessage;
|
||||
}
|
||||
input_el.value = "";
|
||||
this.autoResize();
|
||||
|
||||
// Get and render chat response to user message
|
||||
await this.getChatResponse(user_message);
|
||||
await this.getChatResponse(user_message, isVoice);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
@@ -92,8 +132,9 @@ export class KhojChatView extends KhojPaneView {
|
||||
const objectSrc = `object-src 'none';`;
|
||||
const csp = `${defaultSrc} ${scriptSrc} ${connectSrc} ${styleSrc} ${imgSrc} ${childSrc} ${objectSrc}`;
|
||||
|
||||
// Add CSP meta tag to the Khoj Chat modal
|
||||
document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
|
||||
// WARNING: CSP DISABLED for now as it breaks other Obsidian plugins. Enable when can scope CSP to only Khoj plugin.
|
||||
// CSP meta tag for the Khoj Chat modal
|
||||
// document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
|
||||
|
||||
// Create area for chat logs
|
||||
let chatBodyEl = contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
|
||||
@@ -104,9 +145,10 @@ export class KhojChatView extends KhojPaneView {
|
||||
text: "Chat Sessions",
|
||||
attr: {
|
||||
class: "khoj-input-row-button clickable-icon",
|
||||
title: "Show Conversations (^O)",
|
||||
},
|
||||
})
|
||||
chatSessions.addEventListener('click', async (_) => { await this.toggleChatSessions(chatBodyEl) });
|
||||
chatSessions.addEventListener('click', async (_) => { await this.toggleChatSessions() });
|
||||
setIcon(chatSessions, "history");
|
||||
|
||||
let chatInput = inputRow.createEl("textarea", {
|
||||
@@ -117,16 +159,25 @@ export class KhojChatView extends KhojPaneView {
|
||||
},
|
||||
})
|
||||
chatInput.addEventListener('input', (_) => { this.onChatInput() });
|
||||
chatInput.addEventListener('keydown', (event) => { this.incrementalChat(event) });
|
||||
chatInput.addEventListener('keydown', (event) => {
|
||||
this.incrementalChat(event);
|
||||
this.handleArrowKeys(event);
|
||||
});
|
||||
|
||||
// Add event listeners for long press keybinding
|
||||
this.contentEl.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
this.contentEl.addEventListener('keyup', this.handleKeyUp.bind(this));
|
||||
|
||||
let transcribe = inputRow.createEl("button", {
|
||||
text: "Transcribe",
|
||||
attr: {
|
||||
id: "khoj-transcribe",
|
||||
class: "khoj-transcribe khoj-input-row-button clickable-icon ",
|
||||
title: "Start Voice Chat (^S)",
|
||||
},
|
||||
})
|
||||
transcribe.addEventListener('mousedown', async (event) => { await this.speechToText(event) });
|
||||
transcribe.addEventListener('mousedown', (event) => { this.startSpeechToText(event) });
|
||||
transcribe.addEventListener('mouseup', async (event) => { await this.stopSpeechToText(event) });
|
||||
transcribe.addEventListener('touchstart', async (event) => { await this.speechToText(event) });
|
||||
transcribe.addEventListener('touchend', async (event) => { await this.speechToText(event) });
|
||||
transcribe.addEventListener('touchcancel', async (event) => { await this.speechToText(event) });
|
||||
@@ -145,7 +196,8 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
// Get chat history from Khoj backend and set chat input state
|
||||
let getChatHistorySucessfully = await this.getChatHistory(chatBodyEl);
|
||||
let placeholderText = getChatHistorySucessfully ? "Message" : "Configure Khoj to enable chat";
|
||||
|
||||
let placeholderText : string = getChatHistorySucessfully ? this.startingMessage : "Configure Khoj to enable chat";
|
||||
chatInput.placeholder = placeholderText;
|
||||
chatInput.disabled = !getChatHistorySucessfully;
|
||||
|
||||
@@ -160,6 +212,46 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
}
|
||||
|
||||
startSpeechToText(event: KeyboardEvent | MouseEvent | TouchEvent, timeout=200) {
|
||||
if (!this.keyPressTimeout) {
|
||||
this.keyPressTimeout = setTimeout(async () => {
|
||||
// Reset auto send voice message timer, UI if running
|
||||
if (this.sendMessageTimeout) {
|
||||
// Stop the auto send voice message countdown timer UI
|
||||
clearTimeout(this.sendMessageTimeout);
|
||||
const sendButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-chat-send")[0]
|
||||
setIcon(sendButton, "arrow-up-circle")
|
||||
let sendImg = <SVGElement>sendButton.getElementsByClassName("lucide-arrow-up-circle")[0]
|
||||
sendImg.addEventListener('click', async (_) => { await this.chat() });
|
||||
// Reset chat input value
|
||||
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
chatInput.value = "";
|
||||
}
|
||||
// Start new voice message
|
||||
await this.speechToText(event);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
async stopSpeechToText(event: KeyboardEvent | MouseEvent | TouchEvent) {
|
||||
if (this.mediaRecorder) {
|
||||
await this.speechToText(event);
|
||||
}
|
||||
if (this.keyPressTimeout) {
|
||||
clearTimeout(this.keyPressTimeout);
|
||||
this.keyPressTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
// Start speech to text if keyboard shortcut is pressed
|
||||
if (event.key === 's' && event.getModifierState('Control')) this.startSpeechToText(event);
|
||||
}
|
||||
|
||||
async handleKeyUp(event: KeyboardEvent) {
|
||||
// Stop speech to text if keyboard shortcut is released
|
||||
if (event.key === 's' && event.getModifierState('Control')) await this.stopSpeechToText(event);
|
||||
}
|
||||
|
||||
processOnlineReferences(referenceSection: HTMLElement, onlineContext: any) {
|
||||
let numOnlineReferences = 0;
|
||||
for (let subquery in onlineContext) {
|
||||
@@ -294,6 +386,57 @@ export class KhojChatView extends KhojPaneView {
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
textToSpeech(message: string, event: MouseEvent | null = null): void {
|
||||
// Replace the speaker with a loading icon.
|
||||
let loader = document.createElement("span");
|
||||
loader.classList.add("loader");
|
||||
|
||||
let speechButton: HTMLButtonElement;
|
||||
let speechIcon: Element;
|
||||
|
||||
if (event === null) {
|
||||
// Pick the last speech button if none is provided
|
||||
let speechButtons = document.getElementsByClassName("speech-button");
|
||||
speechButton = speechButtons[speechButtons.length - 1] as HTMLButtonElement;
|
||||
|
||||
let speechIcons = document.getElementsByClassName("speech-icon");
|
||||
speechIcon = speechIcons[speechIcons.length - 1];
|
||||
} else {
|
||||
speechButton = event.currentTarget as HTMLButtonElement;
|
||||
speechIcon = event.target as Element;
|
||||
}
|
||||
|
||||
speechButton.appendChild(loader);
|
||||
speechButton.disabled = true;
|
||||
|
||||
const context = new AudioContext();
|
||||
let textToSpeechApi = `${this.setting.khojUrl}/api/chat/speech?text=${encodeURIComponent(message)}`;
|
||||
fetch(textToSpeechApi, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": `Bearer ${this.setting.khojApiKey}`,
|
||||
},
|
||||
})
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(arrayBuffer => context.decodeAudioData(arrayBuffer))
|
||||
.then(audioBuffer => {
|
||||
const source = context.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(context.destination);
|
||||
source.start(0);
|
||||
source.onended = function() {
|
||||
speechButton.removeChild(loader);
|
||||
speechButton.disabled = false;
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error playing speech:", err);
|
||||
speechButton.removeChild(loader);
|
||||
speechButton.disabled = false; // Consider enabling the button again to allow retrying
|
||||
});
|
||||
}
|
||||
|
||||
formatHTMLMessage(message: string, raw = false, willReplace = true) {
|
||||
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for some AI chat model.
|
||||
message = message.replace(/<s>\[INST\].+(<\/s>)?/g, '');
|
||||
@@ -302,25 +445,23 @@ export class KhojChatView extends KhojPaneView {
|
||||
message = DOMPurify.sanitize(message);
|
||||
|
||||
// Convert the message to html, sanitize the message html and render it to the real DOM
|
||||
let chat_message_body_text_el = this.contentEl.createDiv();
|
||||
chat_message_body_text_el.className = "chat-message-text-response";
|
||||
chat_message_body_text_el.innerHTML = this.markdownTextToSanitizedHtml(message);
|
||||
let chatMessageBodyTextEl = this.contentEl.createDiv();
|
||||
chatMessageBodyTextEl.innerHTML = this.markdownTextToSanitizedHtml(message, this);
|
||||
|
||||
// Add a copy button to each chat message, if it doesn't already exist
|
||||
if (willReplace === true) {
|
||||
this.renderActionButtons(message, chat_message_body_text_el);
|
||||
this.renderActionButtons(message, chatMessageBodyTextEl);
|
||||
}
|
||||
|
||||
return chat_message_body_text_el;
|
||||
return chatMessageBodyTextEl;
|
||||
}
|
||||
|
||||
markdownTextToSanitizedHtml(markdownText: string): string {
|
||||
markdownTextToSanitizedHtml(markdownText: string, component: ItemView): string {
|
||||
// Render markdown to an unlinked DOM element
|
||||
let virtualChatMessageBodyTextEl = document.createElement("div");
|
||||
|
||||
// Convert the message to html
|
||||
// @ts-ignore
|
||||
MarkdownRenderer.renderMarkdown(markdownText, virtualChatMessageBodyTextEl, '', null);
|
||||
MarkdownRenderer.renderMarkdown(markdownText, virtualChatMessageBodyTextEl, '', component);
|
||||
|
||||
// Remove image HTML elements with any non whitelisted src prefix
|
||||
virtualChatMessageBodyTextEl.innerHTML = virtualChatMessageBodyTextEl.innerHTML.replace(
|
||||
@@ -396,23 +537,23 @@ export class KhojChatView extends KhojPaneView {
|
||||
class: `khoj-chat-message ${sender}`
|
||||
},
|
||||
})
|
||||
let chat_message_body_el = chatMessageEl.createDiv();
|
||||
chat_message_body_el.addClasses(["khoj-chat-message-text", sender]);
|
||||
let chat_message_body_text_el = chat_message_body_el.createDiv();
|
||||
let chatMessageBodyEl = chatMessageEl.createDiv();
|
||||
chatMessageBodyEl.addClasses(["khoj-chat-message-text", sender]);
|
||||
let chatMessageBodyTextEl = chatMessageBodyEl.createDiv();
|
||||
|
||||
// Sanitize the markdown to render
|
||||
message = DOMPurify.sanitize(message);
|
||||
|
||||
if (raw) {
|
||||
chat_message_body_text_el.innerHTML = message;
|
||||
chatMessageBodyTextEl.innerHTML = message;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
chat_message_body_text_el.innerHTML = this.markdownTextToSanitizedHtml(message);
|
||||
chatMessageBodyTextEl.innerHTML = this.markdownTextToSanitizedHtml(message, this);
|
||||
}
|
||||
|
||||
// Add action buttons to each chat message element
|
||||
if (willReplace === true) {
|
||||
this.renderActionButtons(message, chat_message_body_text_el);
|
||||
this.renderActionButtons(message, chatMessageBodyTextEl);
|
||||
}
|
||||
|
||||
// Remove user-select: none property to make text selectable
|
||||
@@ -425,56 +566,69 @@ export class KhojChatView extends KhojPaneView {
|
||||
}
|
||||
|
||||
createKhojResponseDiv(dt?: Date): HTMLDivElement {
|
||||
let message_time = this.formatDate(dt ?? new Date());
|
||||
let messageTime = this.formatDate(dt ?? new Date());
|
||||
|
||||
// Append message to conversation history HTML element.
|
||||
// The chat logs should display above the message input box to follow standard UI semantics
|
||||
let chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
let chat_message_el = chat_body_el.createDiv({
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
let chatMessageEl = chatBodyEl.createDiv({
|
||||
attr: {
|
||||
"data-meta": `🏮 Khoj at ${message_time}`,
|
||||
"data-meta": `🏮 Khoj at ${messageTime}`,
|
||||
class: `khoj-chat-message khoj`
|
||||
},
|
||||
}).createDiv({
|
||||
attr: {
|
||||
class: `khoj-chat-message-text khoj`
|
||||
},
|
||||
}).createDiv();
|
||||
})
|
||||
|
||||
// Scroll to bottom after inserting chat messages
|
||||
this.scrollChatToBottom();
|
||||
|
||||
return chat_message_el;
|
||||
return chatMessageEl;
|
||||
}
|
||||
|
||||
async renderIncrementalMessage(htmlElement: HTMLDivElement, additionalMessage: string) {
|
||||
this.result += additionalMessage;
|
||||
this.chatMessageState.rawResponse += additionalMessage;
|
||||
htmlElement.innerHTML = "";
|
||||
// Sanitize the markdown to render
|
||||
this.result = DOMPurify.sanitize(this.result);
|
||||
this.chatMessageState.rawResponse = DOMPurify.sanitize(this.chatMessageState.rawResponse);
|
||||
// @ts-ignore
|
||||
htmlElement.innerHTML = this.markdownTextToSanitizedHtml(this.result);
|
||||
htmlElement.innerHTML = this.markdownTextToSanitizedHtml(this.chatMessageState.rawResponse, this);
|
||||
// Render action buttons for the message
|
||||
this.renderActionButtons(this.result, htmlElement);
|
||||
this.renderActionButtons(this.chatMessageState.rawResponse, htmlElement);
|
||||
// Scroll to bottom of modal, till the send message input box
|
||||
this.scrollChatToBottom();
|
||||
}
|
||||
|
||||
renderActionButtons(message: string, chat_message_body_text_el: HTMLElement) {
|
||||
renderActionButtons(message: string, chatMessageBodyTextEl: HTMLElement) {
|
||||
let copyButton = this.contentEl.createEl('button');
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.classList.add("chat-action-button");
|
||||
copyButton.title = "Copy Message to Clipboard";
|
||||
setIcon(copyButton, "copy-plus");
|
||||
copyButton.addEventListener('click', createCopyParentText(message));
|
||||
chat_message_body_text_el.append(copyButton);
|
||||
|
||||
// Add button to paste into current buffer
|
||||
let pasteToFile = this.contentEl.createEl('button');
|
||||
pasteToFile.classList.add("copy-button");
|
||||
pasteToFile.classList.add("chat-action-button");
|
||||
pasteToFile.title = "Paste Message to File";
|
||||
setIcon(pasteToFile, "clipboard-paste");
|
||||
pasteToFile.addEventListener('click', (event) => { pasteTextAtCursor(createCopyParentText(message, 'clipboard-paste')(event)); });
|
||||
chat_message_body_text_el.append(pasteToFile);
|
||||
|
||||
// Only enable the speech feature if the user is subscribed
|
||||
let speechButton = null;
|
||||
|
||||
if (this.setting.userInfo?.is_active) {
|
||||
// Create a speech button icon to play the message out loud
|
||||
speechButton = this.contentEl.createEl('button');
|
||||
speechButton.classList.add("chat-action-button", "speech-button");
|
||||
speechButton.title = "Listen to Message";
|
||||
setIcon(speechButton, "speech")
|
||||
speechButton.addEventListener('click', (event) => this.textToSpeech(message, event));
|
||||
}
|
||||
|
||||
// Append buttons to parent element
|
||||
chatMessageBodyTextEl.append(copyButton, pasteToFile);
|
||||
|
||||
if (speechButton) {
|
||||
chatMessageBodyTextEl.append(speechButton);
|
||||
}
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
@@ -484,14 +638,25 @@ export class KhojChatView extends KhojPaneView {
|
||||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
createNewConversation(chatBodyEl: HTMLElement) {
|
||||
createNewConversation() {
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
chatBodyEl.innerHTML = "";
|
||||
chatBodyEl.dataset.conversationId = "";
|
||||
chatBodyEl.dataset.conversationTitle = "";
|
||||
this.userMessages = [];
|
||||
this.startingMessage = "Message";
|
||||
|
||||
// Update the placeholder of the chat input
|
||||
const chatInput = this.contentEl.querySelector('.khoj-chat-input') as HTMLTextAreaElement;
|
||||
if (chatInput) {
|
||||
chatInput.placeholder = this.startingMessage;
|
||||
}
|
||||
this.renderMessage(chatBodyEl, "Hey 👋🏾, what's up?", "khoj");
|
||||
}
|
||||
|
||||
async toggleChatSessions(chatBodyEl: HTMLElement, forceShow: boolean = false): Promise<boolean> {
|
||||
async toggleChatSessions(forceShow: boolean = false): Promise<boolean> {
|
||||
this.userMessages = []; // clear user previous message history
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
if (!forceShow && this.contentEl.getElementsByClassName("side-panel")?.length > 0) {
|
||||
chatBodyEl.innerHTML = "";
|
||||
return this.getChatHistory(chatBodyEl);
|
||||
@@ -505,9 +670,10 @@ 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(chatBodyEl));
|
||||
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation());
|
||||
setIcon(newConversationButtonEl, "plus");
|
||||
newConversationButtonEl.innerHTML += "New";
|
||||
newConversationButtonEl.title = "New Conversation (^N)";
|
||||
|
||||
const existingConversationsEl = sidePanelEl.createDiv("existing-conversations");
|
||||
const conversationListEl = existingConversationsEl.createDiv("conversation-list");
|
||||
@@ -579,8 +745,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
let editConversationTitleButtonEl = this.contentEl.createEl('button');
|
||||
setIcon(editConversationTitleButtonEl, "edit");
|
||||
editConversationTitleButtonEl.title = "Rename";
|
||||
editConversationTitleButtonEl.classList.add("edit-title-button");
|
||||
editConversationTitleButtonEl.classList.add("three-dot-menu-button-item");
|
||||
editConversationTitleButtonEl.classList.add("edit-title-button", "three-dot-menu-button-item", "clickable-icon");
|
||||
if (selectedConversation) editConversationTitleButtonEl.classList.add("selected-conversation");
|
||||
editConversationTitleButtonEl.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
@@ -608,7 +773,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
let editConversationTitleSaveButtonEl = this.contentEl.createEl('button');
|
||||
conversationSessionTitleEl.replaceWith(editConversationTitleInputEl);
|
||||
editConversationTitleSaveButtonEl.innerHTML = "Save";
|
||||
editConversationTitleSaveButtonEl.classList.add("three-dot-menu-button-item");
|
||||
editConversationTitleSaveButtonEl.classList.add("three-dot-menu-button-item", "clickable-icon");
|
||||
if (selectedConversation) editConversationTitleSaveButtonEl.classList.add("selected-conversation");
|
||||
editConversationTitleSaveButtonEl.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
@@ -655,8 +820,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
let deleteConversationButtonEl = this.contentEl.createEl('button');
|
||||
setIcon(deleteConversationButtonEl, "trash");
|
||||
deleteConversationButtonEl.title = "Delete";
|
||||
deleteConversationButtonEl.classList.add("delete-conversation-button");
|
||||
deleteConversationButtonEl.classList.add("three-dot-menu-button-item");
|
||||
deleteConversationButtonEl.classList.add("delete-conversation-button", "three-dot-menu-button-item", "clickable-icon");
|
||||
if (selectedConversation) deleteConversationButtonEl.classList.add("selected-conversation");
|
||||
deleteConversationButtonEl.addEventListener('click', () => {
|
||||
// Ask for confirmation before deleting chat session
|
||||
@@ -669,7 +833,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
chatBodyEl.innerHTML = "";
|
||||
chatBodyEl.dataset.conversationId = "";
|
||||
chatBodyEl.dataset.conversationTitle = "";
|
||||
this.toggleChatSessions(chatBodyEl, true);
|
||||
this.toggleChatSessions(true);
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
@@ -707,7 +871,6 @@ export class KhojChatView extends KhojPaneView {
|
||||
chatBodyEl.dataset.conversationId = responseJson.response.conversation_id;
|
||||
chatBodyEl.dataset.conversationTitle = responseJson.response.slug || `New conversation 🌱`;
|
||||
|
||||
|
||||
let chatLogs = responseJson.response?.conversation_id ? responseJson.response.chat ?? [] : responseJson.response;
|
||||
chatLogs.forEach((chatLog: any) => {
|
||||
this.renderMessageWithReferences(
|
||||
@@ -720,7 +883,23 @@ export class KhojChatView extends KhojPaneView {
|
||||
chatLog.intent?.type,
|
||||
chatLog.intent?.["inferred-queries"],
|
||||
);
|
||||
// push the user messages to the chat history
|
||||
if(chatLog.by === "you"){
|
||||
this.userMessages.push(chatLog.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Update starting message after loading history
|
||||
const modifierKey: string = Platform.isMacOS ? '⌘' : '^';
|
||||
this.startingMessage = this.userMessages.length > 0
|
||||
? `(${modifierKey}+↑/↓) for prev messages`
|
||||
: "Message";
|
||||
|
||||
// Update the placeholder of the chat input
|
||||
const chatInput = this.contentEl.querySelector('.khoj-chat-input') as HTMLTextAreaElement;
|
||||
if (chatInput) {
|
||||
chatInput.placeholder = this.startingMessage;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
let errorMsg = "Unable to get response from Khoj server ❤️🩹. Ensure server is running or contact developers for help at [team@khoj.dev](mailto:team@khoj.dev) or in [Discord](https://discord.gg/BDgyabRM6e)";
|
||||
@@ -730,36 +909,127 @@ export class KhojChatView extends KhojPaneView {
|
||||
return true;
|
||||
}
|
||||
|
||||
async readChatStream(response: Response, responseElement: HTMLDivElement): Promise<void> {
|
||||
convertMessageChunkToJson(rawChunk: string): MessageChunk {
|
||||
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
|
||||
try {
|
||||
let jsonChunk = JSON.parse(rawChunk);
|
||||
if (!jsonChunk.type)
|
||||
jsonChunk = {type: 'message', data: jsonChunk};
|
||||
return jsonChunk;
|
||||
} catch (e) {
|
||||
return {type: 'message', data: rawChunk};
|
||||
}
|
||||
} else if (rawChunk.length > 0) {
|
||||
return {type: 'message', data: rawChunk};
|
||||
}
|
||||
return {type: '', data: ''};
|
||||
}
|
||||
|
||||
processMessageChunk(rawChunk: string): void {
|
||||
const chunk = this.convertMessageChunkToJson(rawChunk);
|
||||
console.debug("Chunk:", chunk);
|
||||
if (!chunk || !chunk.type) return;
|
||||
if (chunk.type === 'status') {
|
||||
console.log(`status: ${chunk.data}`);
|
||||
const statusMessage = chunk.data;
|
||||
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, statusMessage, this.chatMessageState.loadingEllipsis, false);
|
||||
} else if (chunk.type === 'start_llm_response') {
|
||||
console.log("Started streaming", new Date());
|
||||
} else if (chunk.type === 'end_llm_response') {
|
||||
console.log("Stopped streaming", new Date());
|
||||
|
||||
// Automatically respond with voice if the subscribed user has sent voice message
|
||||
if (this.chatMessageState.isVoice && this.setting.userInfo?.is_active)
|
||||
this.textToSpeech(this.chatMessageState.rawResponse);
|
||||
|
||||
// Append any references after all the data has been streamed
|
||||
this.finalizeChatBodyResponse(this.chatMessageState.references, this.chatMessageState.newResponseTextEl);
|
||||
|
||||
const liveQuery = this.chatMessageState.rawQuery;
|
||||
// Reset variables
|
||||
this.chatMessageState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
rawQuery: liveQuery,
|
||||
isVoice: false,
|
||||
};
|
||||
} else if (chunk.type === "references") {
|
||||
this.chatMessageState.references = {"notes": chunk.data.context, "online": chunk.data.onlineContext};
|
||||
} else if (chunk.type === 'message') {
|
||||
const chunkData = chunk.data;
|
||||
if (typeof chunkData === 'object' && chunkData !== null) {
|
||||
// If chunkData is already a JSON object
|
||||
this.handleJsonResponse(chunkData);
|
||||
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
|
||||
// Try process chunk data as if it is a JSON object
|
||||
try {
|
||||
const jsonData = JSON.parse(chunkData.trim());
|
||||
this.handleJsonResponse(jsonData);
|
||||
} catch (e) {
|
||||
this.chatMessageState.rawResponse += chunkData;
|
||||
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, this.chatMessageState.rawResponse, this.chatMessageState.loadingEllipsis);
|
||||
}
|
||||
} else {
|
||||
this.chatMessageState.rawResponse += chunkData;
|
||||
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, this.chatMessageState.rawResponse, this.chatMessageState.loadingEllipsis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleJsonResponse(jsonData: any): void {
|
||||
if (jsonData.image || jsonData.detail) {
|
||||
this.chatMessageState.rawResponse = this.handleImageResponse(jsonData, this.chatMessageState.rawResponse);
|
||||
} else if (jsonData.response) {
|
||||
this.chatMessageState.rawResponse = jsonData.response;
|
||||
}
|
||||
|
||||
if (this.chatMessageState.newResponseTextEl) {
|
||||
this.chatMessageState.newResponseTextEl.innerHTML = "";
|
||||
this.chatMessageState.newResponseTextEl.appendChild(this.formatHTMLMessage(this.chatMessageState.rawResponse));
|
||||
}
|
||||
}
|
||||
|
||||
async readChatStream(response: Response): Promise<void> {
|
||||
// Exit if response body is empty
|
||||
if (response.body == null) return;
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const eventDelimiter = '␃🔚␗';
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
// Break if the stream is done
|
||||
if (done) break;
|
||||
if (done) {
|
||||
this.processMessageChunk(buffer);
|
||||
buffer = '';
|
||||
// Break if the stream is done
|
||||
break;
|
||||
}
|
||||
|
||||
let responseText = decoder.decode(value);
|
||||
if (responseText.includes("### compiled references:")) {
|
||||
// Render any references used to generate the response
|
||||
const [additionalResponse, rawReference] = responseText.split("### compiled references:", 2);
|
||||
await this.renderIncrementalMessage(responseElement, additionalResponse);
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
console.debug("Raw Chunk:", chunk)
|
||||
// Start buffering chunks until complete event is received
|
||||
buffer += chunk;
|
||||
|
||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||
let references = this.extractReferences(rawReferenceAsJson);
|
||||
responseElement.appendChild(this.createReferenceSection(references));
|
||||
} else {
|
||||
// Render incremental chat response
|
||||
await this.renderIncrementalMessage(responseElement, responseText);
|
||||
// Once the buffer contains a complete event
|
||||
let newEventIndex;
|
||||
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
|
||||
// Extract the event from the buffer
|
||||
const event = buffer.slice(0, newEventIndex);
|
||||
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
||||
|
||||
// Process the event
|
||||
if (event) this.processMessageChunk(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getChatResponse(query: string | undefined | null): Promise<void> {
|
||||
async getChatResponse(query: string | undefined | null, isVoice: boolean = false): Promise<void> {
|
||||
// Exit if query is empty
|
||||
if (!query || query === "") return;
|
||||
|
||||
@@ -767,83 +1037,59 @@ export class KhojChatView extends KhojPaneView {
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
this.renderMessage(chatBodyEl, query, "you");
|
||||
|
||||
let conversationID = chatBodyEl.dataset.conversationId;
|
||||
if (!conversationID) {
|
||||
let conversationId = chatBodyEl.dataset.conversationId;
|
||||
if (!conversationId) {
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat/sessions?client=obsidian`;
|
||||
let response = await fetch(chatUrl, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` },
|
||||
});
|
||||
let data = await response.json();
|
||||
conversationID = data.conversation_id;
|
||||
chatBodyEl.dataset.conversationId = conversationID;
|
||||
conversationId = data.conversation_id;
|
||||
chatBodyEl.dataset.conversationId = conversationId;
|
||||
}
|
||||
|
||||
// Get chat response from Khoj backend
|
||||
let encodedQuery = encodeURIComponent(query);
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&n=${this.setting.resultsCount}&client=obsidian&stream=true®ion=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`;
|
||||
let responseElement = this.createKhojResponseDiv();
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&conversation_id=${conversationId}&n=${this.setting.resultsCount}&stream=true&client=obsidian`;
|
||||
if (!!this.location) chatUrl += `®ion=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`;
|
||||
|
||||
let newResponseEl = this.createKhojResponseDiv();
|
||||
let newResponseTextEl = newResponseEl.createDiv();
|
||||
newResponseTextEl.classList.add("khoj-chat-message-text", "khoj");
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
this.result = "";
|
||||
let loadingEllipsis = this.createLoadingEllipse();
|
||||
responseElement.appendChild(loadingEllipsis);
|
||||
newResponseTextEl.appendChild(loadingEllipsis);
|
||||
|
||||
// Set chat message state
|
||||
this.chatMessageState = {
|
||||
newResponseEl: newResponseEl,
|
||||
newResponseTextEl: newResponseTextEl,
|
||||
loadingEllipsis: loadingEllipsis,
|
||||
references: {},
|
||||
rawQuery: query,
|
||||
rawResponse: "",
|
||||
isVoice: isVoice,
|
||||
};
|
||||
|
||||
let response = await fetch(chatUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Content-Type": "text/plain",
|
||||
"Authorization": `Bearer ${this.setting.khojApiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
if (response.body === null) {
|
||||
throw new Error("Response body is null");
|
||||
}
|
||||
if (response.body === null) throw new Error("Response body is null");
|
||||
|
||||
// Clear loading status message
|
||||
if (responseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis) {
|
||||
responseElement.removeChild(loadingEllipsis);
|
||||
}
|
||||
|
||||
// Reset collated chat result to empty string
|
||||
this.result = "";
|
||||
responseElement.innerHTML = "";
|
||||
if (response.headers.get("content-type") === "application/json") {
|
||||
let responseText = ""
|
||||
try {
|
||||
const responseAsJson = await response.json() as ChatJsonResult;
|
||||
if (responseAsJson.image) {
|
||||
// If response has image field, response is a generated image.
|
||||
if (responseAsJson.intentType === "text-to-image") {
|
||||
responseText += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image2") {
|
||||
responseText += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image-v3") {
|
||||
responseText += ``;
|
||||
}
|
||||
const inferredQuery = responseAsJson.inferredQueries?.[0];
|
||||
if (inferredQuery) {
|
||||
responseText += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
} else if (responseAsJson.detail) {
|
||||
responseText = responseAsJson.detail;
|
||||
}
|
||||
} catch (error) {
|
||||
// If the chunk is not a JSON object, just display it as is
|
||||
responseText = await response.text();
|
||||
} finally {
|
||||
await this.renderIncrementalMessage(responseElement, responseText);
|
||||
}
|
||||
} else {
|
||||
// Stream and render chat response
|
||||
await this.readChatStream(response, responseElement);
|
||||
}
|
||||
// Stream and render chat response
|
||||
await this.readChatStream(response);
|
||||
} catch (err) {
|
||||
console.log(`Khoj chat response failed with\n${err}`);
|
||||
console.error(`Khoj chat response failed with\n${err}`);
|
||||
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
|
||||
responseElement.innerHTML = errorMsg
|
||||
newResponseTextEl.textContent = errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,7 +1132,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
sendMessageTimeout: NodeJS.Timeout | undefined;
|
||||
mediaRecorder: MediaRecorder | undefined;
|
||||
async speechToText(event: MouseEvent | TouchEvent) {
|
||||
async speechToText(event: MouseEvent | TouchEvent | KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
const transcribeButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-transcribe")[0];
|
||||
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
@@ -919,9 +1165,19 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
|
||||
// Parse response from Khoj backend
|
||||
let noSpeechText: string[] = [
|
||||
"Thanks for watching!",
|
||||
"Thanks for watching.",
|
||||
"Thank you for watching!",
|
||||
"Thank you for watching.",
|
||||
"You",
|
||||
"Bye."
|
||||
];
|
||||
let noSpeech: boolean = false;
|
||||
if (response.status === 200) {
|
||||
console.log(response);
|
||||
chatInput.value += response.json.text.trimStart();
|
||||
noSpeech = noSpeechText.includes(response.json.text.trimStart());
|
||||
if (!noSpeech) chatInput.value += response.json.text.trimStart();
|
||||
this.autoResize();
|
||||
} else if (response.status === 501) {
|
||||
throw new Error("⛔️ Configure speech-to-text model on server.");
|
||||
@@ -931,8 +1187,8 @@ export class KhojChatView extends KhojPaneView {
|
||||
throw new Error("⛔️ Failed to transcribe audio.");
|
||||
}
|
||||
|
||||
// Don't auto-send empty messages
|
||||
if (chatInput.value.length === 0) return;
|
||||
// Don't auto-send empty messages or when no speech is detected
|
||||
if (chatInput.value.length === 0 || noSpeech) return;
|
||||
|
||||
// Show stop auto-send button. It stops auto-send when clicked
|
||||
setIcon(sendButton, "stop-circle");
|
||||
@@ -941,6 +1197,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
// Start the countdown timer UI
|
||||
stopSendButtonImg.getElementsByTagName("circle")[0].style.animation = "countdown 3s linear 1 forwards";
|
||||
stopSendButtonImg.getElementsByTagName("circle")[0].style.color = "var(--icon-color-active)";
|
||||
|
||||
// Auto send message after 3 seconds
|
||||
this.sendMessageTimeout = setTimeout(() => {
|
||||
@@ -950,7 +1207,7 @@ export class KhojChatView extends KhojPaneView {
|
||||
sendImg.addEventListener('click', async (_) => { await this.chat() });
|
||||
|
||||
// Send message
|
||||
this.chat();
|
||||
this.chat(true);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@@ -969,21 +1226,23 @@ export class KhojChatView extends KhojPaneView {
|
||||
});
|
||||
|
||||
this.mediaRecorder.start();
|
||||
setIcon(transcribeButton, "mic-off");
|
||||
// setIcon(transcribeButton, "mic-off");
|
||||
transcribeButton.classList.add("loading-encircle")
|
||||
};
|
||||
|
||||
// Toggle recording
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart') {
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart' || event.type === 'mousedown' || event.type === 'keydown') {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
?.then(handleRecording)
|
||||
.catch((e) => {
|
||||
this.flashStatusInChatInput("⛔️ Failed to access microphone");
|
||||
});
|
||||
} else if (this.mediaRecorder.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel') {
|
||||
} else if (this.mediaRecorder?.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel' || event.type === 'mouseup' || event.type === 'keyup') {
|
||||
this.mediaRecorder.stop();
|
||||
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
this.mediaRecorder = undefined;
|
||||
transcribeButton.classList.remove("loading-encircle");
|
||||
setIcon(transcribeButton, "mic");
|
||||
}
|
||||
}
|
||||
@@ -1009,7 +1268,9 @@ export class KhojChatView extends KhojPaneView {
|
||||
onChatInput() {
|
||||
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
chatInput.value = chatInput.value.trimStart();
|
||||
|
||||
this.currentMessageIndex = -1;
|
||||
// store the current input
|
||||
this.currentUserInput = chatInput.value;
|
||||
this.autoResize();
|
||||
}
|
||||
|
||||
@@ -1055,30 +1316,21 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
handleStreamResponse(newResponseElement: HTMLElement | null, rawResponse: string, loadingEllipsis: HTMLElement | null, replace = true) {
|
||||
if (!newResponseElement) return;
|
||||
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis) {
|
||||
// Remove loading ellipsis if it exists
|
||||
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
|
||||
newResponseElement.removeChild(loadingEllipsis);
|
||||
}
|
||||
if (replace) {
|
||||
newResponseElement.innerHTML = "";
|
||||
}
|
||||
// Clear the response element if replace is true
|
||||
if (replace) newResponseElement.innerHTML = "";
|
||||
|
||||
// Append response to the response element
|
||||
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse, false, replace));
|
||||
|
||||
// Append loading ellipsis if it exists
|
||||
if (!replace && loadingEllipsis) newResponseElement.appendChild(loadingEllipsis);
|
||||
// Scroll to bottom of chat view
|
||||
this.scrollChatToBottom();
|
||||
}
|
||||
|
||||
handleCompiledReferences(rawResponseElement: HTMLElement | null, chunk: string, references: any, rawResponse: string) {
|
||||
if (!rawResponseElement || !chunk) return { rawResponse, references };
|
||||
|
||||
const [additionalResponse, rawReference] = chunk.split("### compiled references:", 2);
|
||||
rawResponse += additionalResponse;
|
||||
rawResponseElement.innerHTML = "";
|
||||
rawResponseElement.appendChild(this.formatHTMLMessage(rawResponse));
|
||||
|
||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||
references = this.extractReferences(rawReferenceAsJson);
|
||||
|
||||
return { rawResponse, references };
|
||||
}
|
||||
|
||||
handleImageResponse(imageJson: any, rawResponse: string) {
|
||||
if (imageJson.image) {
|
||||
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
|
||||
@@ -1095,33 +1347,10 @@ export class KhojChatView extends KhojPaneView {
|
||||
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
}
|
||||
let references = {};
|
||||
if (imageJson.context && imageJson.context.length > 0) {
|
||||
references = this.extractReferences(imageJson.context);
|
||||
}
|
||||
if (imageJson.detail) {
|
||||
// If response has detail field, response is an error message.
|
||||
rawResponse += imageJson.detail;
|
||||
}
|
||||
return { rawResponse, references };
|
||||
}
|
||||
// If response has detail field, response is an error message.
|
||||
if (imageJson.detail) rawResponse += imageJson.detail;
|
||||
|
||||
extractReferences(rawReferenceAsJson: any): object {
|
||||
let references: any = {};
|
||||
if (rawReferenceAsJson instanceof Array) {
|
||||
references["notes"] = rawReferenceAsJson;
|
||||
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
||||
references["online"] = rawReferenceAsJson;
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
addMessageToChatBody(rawResponse: string, newResponseElement: HTMLElement | null, references: any) {
|
||||
if (!newResponseElement) return;
|
||||
newResponseElement.innerHTML = "";
|
||||
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse));
|
||||
|
||||
this.finalizeChatBodyResponse(references, newResponseElement);
|
||||
return rawResponse;
|
||||
}
|
||||
|
||||
finalizeChatBodyResponse(references: object, newResponseElement: HTMLElement | null) {
|
||||
@@ -1173,4 +1402,27 @@ export class KhojChatView extends KhojPaneView {
|
||||
|
||||
return referencesDiv;
|
||||
}
|
||||
|
||||
// function to loop through the user's past messages
|
||||
handleArrowKeys(event: KeyboardEvent) {
|
||||
const chatInput = event.target as HTMLTextAreaElement;
|
||||
const isModKey = Platform.isMacOS ? event.metaKey : event.ctrlKey;
|
||||
|
||||
if (isModKey && event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
if (this.currentMessageIndex < this.userMessages.length - 1) {
|
||||
this.currentMessageIndex++;
|
||||
chatInput.value = this.userMessages[this.userMessages.length - 1 - this.currentMessageIndex];
|
||||
}
|
||||
} else if (isModKey && event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
if (this.currentMessageIndex > 0) {
|
||||
this.currentMessageIndex--;
|
||||
chatInput.value = this.userMessages[this.userMessages.length - 1 - this.currentMessageIndex];
|
||||
} else if (this.currentMessageIndex === 0) {
|
||||
this.currentMessageIndex = -1;
|
||||
chatInput.value = this.currentUserInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Plugin, WorkspaceLeaf } from 'obsidian';
|
||||
import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings'
|
||||
import { KhojSearchModal } from 'src/search_modal'
|
||||
import { KhojChatView } from 'src/chat_view'
|
||||
import { updateContentIndex, canConnectToBackend, KhojView } from './utils';
|
||||
import { updateContentIndex, canConnectToBackend, KhojView, jumpToPreviousView } from './utils';
|
||||
import { KhojPaneView } from './pane_view';
|
||||
|
||||
|
||||
export default class Khoj extends Plugin {
|
||||
@@ -79,16 +80,30 @@ export default class Khoj extends Plugin {
|
||||
const leaves = workspace.getLeavesOfType(viewType);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
}
|
||||
|
||||
// "Reveal" the leaf in case it is in a collapsed sidebar
|
||||
if (leaf) workspace.revealLeaf(leaf);
|
||||
}
|
||||
if (leaf) {
|
||||
const activeKhojLeaf = workspace.getActiveViewOfType(KhojPaneView)?.leaf;
|
||||
// Jump to the previous view if the current view is Khoj Side Pane
|
||||
if (activeKhojLeaf === leaf) jumpToPreviousView();
|
||||
// Else Reveal the leaf in case it is in a collapsed sidebar
|
||||
else {
|
||||
workspace.revealLeaf(leaf);
|
||||
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// focus on the chat input when the chat view is opened
|
||||
let chatView = leaf.view as KhojChatView;
|
||||
let chatInput = <HTMLTextAreaElement>chatView.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
if (chatInput) chatInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +38,24 @@ export abstract class KhojPaneView extends ItemView {
|
||||
const leaves = workspace.getLeavesOfType(viewType);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
// A leaf with our view already exists, use that
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
// Our view could not be found in the workspace, create a new leaf
|
||||
// in the right sidebar for it
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: viewType, active: true });
|
||||
}
|
||||
|
||||
// "Reveal" the leaf in case it is in a collapsed sidebar
|
||||
if (leaf) workspace.revealLeaf(leaf);
|
||||
}
|
||||
if (leaf) {
|
||||
if (viewType === KhojView.CHAT) {
|
||||
// focus on the chat input when the chat view is opened
|
||||
let chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
if (chatInput) chatInput.focus();
|
||||
}
|
||||
|
||||
// "Reveal" the leaf in case it is in a collapsed sidebar
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { App, SuggestModal, request, MarkdownRenderer, Instruction, Platform } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { createNoteAndCloseModal, getLinkToEntry } from 'src/utils';
|
||||
import { supportedBinaryFileTypes, createNoteAndCloseModal, getFileFromPath, getLinkToEntry, supportedImageFilesTypes } from 'src/utils';
|
||||
|
||||
export interface SearchResult {
|
||||
entry: string;
|
||||
@@ -112,28 +112,41 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
let os_path_separator = result.file.includes('\\') ? '\\' : '/';
|
||||
let filename = result.file.split(os_path_separator).pop();
|
||||
|
||||
// Remove YAML frontmatter when rendering string
|
||||
result.entry = result.entry.replace(/---[\n\r][\s\S]*---[\n\r]/, '');
|
||||
|
||||
// Truncate search results to lines_to_render
|
||||
let entry_snipped_indicator = result.entry.split('\n').length > lines_to_render ? ' **...**' : '';
|
||||
let snipped_entry = result.entry.split('\n').slice(0, lines_to_render).join('\n');
|
||||
|
||||
// Show filename of each search result for context
|
||||
el.createEl("div",{ cls: 'khoj-result-file' }).setText(filename ?? "");
|
||||
let result_el = el.createEl("div", { cls: 'khoj-result-entry' })
|
||||
|
||||
let resultToRender = "";
|
||||
let fileExtension = filename?.split(".").pop() ?? "";
|
||||
if (supportedImageFilesTypes.includes(fileExtension) && filename) {
|
||||
let linkToEntry: string = filename;
|
||||
let imageFiles = this.app.vault.getFiles().filter(file => supportedImageFilesTypes.includes(fileExtension));
|
||||
// Find vault file of chosen search result
|
||||
let fileInVault = getFileFromPath(imageFiles, result.file);
|
||||
if (fileInVault)
|
||||
linkToEntry = this.app.vault.getResourcePath(fileInVault);
|
||||
|
||||
resultToRender = ``;
|
||||
} else {
|
||||
// Remove YAML frontmatter when rendering string
|
||||
result.entry = result.entry.replace(/---[\n\r][\s\S]*---[\n\r]/, '');
|
||||
|
||||
// Truncate search results to lines_to_render
|
||||
let entry_snipped_indicator = result.entry.split('\n').length > lines_to_render ? ' **...**' : '';
|
||||
let snipped_entry = result.entry.split('\n').slice(0, lines_to_render).join('\n');
|
||||
resultToRender = `${snipped_entry}${entry_snipped_indicator}`;
|
||||
}
|
||||
// @ts-ignore
|
||||
MarkdownRenderer.renderMarkdown(snipped_entry + entry_snipped_indicator, result_el, result.file, null);
|
||||
MarkdownRenderer.renderMarkdown(resultToRender, result_el, result.file, null);
|
||||
}
|
||||
|
||||
async onChooseSuggestion(result: SearchResult, _: MouseEvent | KeyboardEvent) {
|
||||
// Get all markdown and PDF files in vault
|
||||
// Get all markdown, pdf and image files in vault
|
||||
const mdFiles = this.app.vault.getMarkdownFiles();
|
||||
const pdfFiles = this.app.vault.getFiles().filter(file => file.extension === 'pdf');
|
||||
const binaryFiles = this.app.vault.getFiles().filter(file => supportedBinaryFileTypes.includes(file.extension));
|
||||
|
||||
// Find, Open vault file at heading of chosen search result
|
||||
let linkToEntry = getLinkToEntry(mdFiles.concat(pdfFiles), result.file, result.entry);
|
||||
let linkToEntry = getLinkToEntry(mdFiles.concat(binaryFiles), result.file, result.entry);
|
||||
if (linkToEntry) this.app.workspace.openLinkText(linkToEntry, '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface UserInfo {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface KhojSetting {
|
||||
resultsCount: number;
|
||||
khojUrl: string;
|
||||
|
||||
@@ -48,11 +48,14 @@ function filenameToMimeType (filename: TFile): string {
|
||||
}
|
||||
}
|
||||
|
||||
export const supportedImageFilesTypes = ['png', 'jpg', 'jpeg'];
|
||||
export const supportedBinaryFileTypes = ['pdf'].concat(supportedImageFilesTypes);
|
||||
export const supportedFileTypes = ['md', 'markdown'].concat(supportedBinaryFileTypes);
|
||||
|
||||
export async function updateContentIndex(vault: Vault, setting: KhojSetting, lastSync: Map<TFile, number>, regenerate: boolean = false): Promise<Map<TFile, number>> {
|
||||
// Get all markdown, pdf files in the vault
|
||||
console.log(`Khoj: Updating Khoj content index...`)
|
||||
const files = vault.getFiles().filter(file => file.extension === 'md' || file.extension === 'markdown' || file.extension === 'pdf');
|
||||
const binaryFileTypes = ['pdf']
|
||||
const files = vault.getFiles().filter(file => supportedFileTypes.includes(file.extension));
|
||||
let countOfFilesToIndex = 0;
|
||||
let countOfFilesToDelete = 0;
|
||||
lastSync = lastSync.size > 0 ? lastSync : new Map<TFile, number>();
|
||||
@@ -66,7 +69,7 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
}
|
||||
|
||||
countOfFilesToIndex++;
|
||||
const encoding = binaryFileTypes.includes(file.extension) ? "binary" : "utf8";
|
||||
const encoding = supportedBinaryFileTypes.includes(file.extension) ? "binary" : "utf8";
|
||||
const mimeType = fileExtensionToMimeType(file.extension) + (encoding === "utf8" ? "; charset=UTF-8" : "");
|
||||
const fileContent = encoding == 'binary' ? await vault.readBinary(file) : await vault.read(file);
|
||||
fileData.push({blob: new Blob([fileContent], { type: mimeType }), path: file.path});
|
||||
@@ -89,10 +92,11 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||
for (let i = 0; i < fileData.length; i += 1000) {
|
||||
const filesGroup = fileData.slice(i, i + 1000);
|
||||
const formData = new FormData();
|
||||
const method = regenerate ? "PUT" : "PATCH";
|
||||
filesGroup.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
|
||||
// Call Khoj backend to update index with all markdown, pdf files
|
||||
const response = await fetch(`${setting.khojUrl}/api/v1/index/update?force=${regenerate}&client=obsidian`, {
|
||||
method: 'POST',
|
||||
const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${setting.khojApiKey}`,
|
||||
},
|
||||
@@ -201,12 +205,12 @@ export function getBackendStatusMessage(
|
||||
): string {
|
||||
// Welcome message with default settings. Khoj cloud always expects an API key.
|
||||
if (!khojApiKey && khojUrl === 'https://app.khoj.dev')
|
||||
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`;
|
||||
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/settings#clients and set it in the Khoj plugin settings on Obsidian`;
|
||||
|
||||
if (!connectedToServer)
|
||||
return `❗️Could not connect to Khoj at ${khojUrl}. Ensure your can access it`;
|
||||
else if (!userEmail)
|
||||
return `✅ Connected to Khoj. ❗️Get a valid API key from ${khojUrl}/config#clients to log in`;
|
||||
return `✅ Connected to Khoj. ❗️Get a valid API key from ${khojUrl}/settings#clients to log in`;
|
||||
else if (userEmail === 'default@example.com')
|
||||
// Logged in as default user in anonymous mode
|
||||
return `✅ Signed in to Khoj`;
|
||||
@@ -333,6 +337,12 @@ export function createCopyParentText(message: string, originalButton: string = '
|
||||
}
|
||||
}
|
||||
|
||||
export function jumpToPreviousView() {
|
||||
const editor: Editor = this.app.workspace.getActiveFileView()?.editor
|
||||
if (!editor) return;
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
export function pasteTextAtCursor(text: string | undefined) {
|
||||
// Get the current active file's editor
|
||||
const editor: Editor = this.app.workspace.getActiveFileView()?.editor
|
||||
@@ -347,15 +357,21 @@ export function pasteTextAtCursor(text: string | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getLinkToEntry(sourceFiles: TFile[], chosenFile: string, chosenEntry: string): string | undefined {
|
||||
export function getFileFromPath(sourceFiles: TFile[], chosenFile: string): TFile | undefined {
|
||||
// Find the vault file matching file of chosen file, entry
|
||||
let fileMatch = sourceFiles
|
||||
// Sort by descending length of path
|
||||
// This finds longest path match when multiple files have same name
|
||||
.sort((a, b) => b.path.length - a.path.length)
|
||||
// The first match is the best file match across OS
|
||||
// e.g Khoj server on Linux, Obsidian vault on Android
|
||||
// e.g. Khoj server on Linux, Obsidian vault on Android
|
||||
.find(file => chosenFile.replace(/\\/g, "/").endsWith(file.path))
|
||||
return fileMatch;
|
||||
}
|
||||
|
||||
export function getLinkToEntry(sourceFiles: TFile[], chosenFile: string, chosenEntry: string): string | undefined {
|
||||
// Find the vault file matching file of chosen file, entry
|
||||
let fileMatch = getFileFromPath(sourceFiles, chosenFile);
|
||||
|
||||
// Return link to vault file at heading of chosen search result
|
||||
if (fileMatch) {
|
||||
|
||||
@@ -74,17 +74,23 @@ If your plugin does not need CSS, delete this file.
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
text-align: left;
|
||||
user-select: text;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--active-bg);
|
||||
}
|
||||
/* color chat bubble by khoj blue */
|
||||
.khoj-chat-message-text.khoj {
|
||||
color: var(--khoj-storm-grey);
|
||||
background: var(--khoj-winter-sun);
|
||||
border: 1px solid var(--khoj-sun);
|
||||
margin-left: auto;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* Override white-space for ul, ol, li under khoj-chat-message-text.khoj */
|
||||
.khoj-chat-message-text.khoj ul,
|
||||
.khoj-chat-message-text.khoj ol,
|
||||
.khoj-chat-message-text.khoj li {
|
||||
white-space: normal;
|
||||
}
|
||||
/* add left protrusion to khoj chat bubble */
|
||||
.khoj-chat-message-text.khoj:after {
|
||||
content: '';
|
||||
@@ -92,14 +98,12 @@ If your plugin does not need CSS, delete this file.
|
||||
bottom: -2px;
|
||||
left: -7px;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: var(--khoj-winter-sun);
|
||||
border-bottom: 0;
|
||||
transform: rotate(-60deg);
|
||||
}
|
||||
/* color chat bubble by you dark grey */
|
||||
.khoj-chat-message-text.you {
|
||||
color: var(--text-on-accent);
|
||||
background: var(--khoj-storm-grey);
|
||||
border: 1px solid var(--color-accent);
|
||||
margin-right: auto;
|
||||
}
|
||||
/* add right protrusion to you chat bubble */
|
||||
@@ -109,7 +113,6 @@ If your plugin does not need CSS, delete this file.
|
||||
top: 91%;
|
||||
right: -2px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: var(--khoj-storm-grey);
|
||||
border-right: 0;
|
||||
margin-top: -10px;
|
||||
transform: rotate(-60deg)
|
||||
@@ -160,9 +163,8 @@ div.expanded.reference-section {
|
||||
margin: 10px 0;
|
||||
}
|
||||
button.reference-button {
|
||||
background: var(--khoj-winter-sun);
|
||||
color: var(--khoj-storm-grey);
|
||||
border: 1px solid var(--khoj-storm-grey);
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
padding: 4px;
|
||||
font-size: 14px;
|
||||
@@ -202,8 +204,7 @@ button.reference-button[aria-expanded="true"]::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
button.reference-expand-button {
|
||||
background: var(--khoj-winter-sun);
|
||||
color: var(--khoj-storm-grey);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--khoj-storm-grey);
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
@@ -216,8 +217,8 @@ button.reference-expand-button {
|
||||
text-align: left;
|
||||
}
|
||||
button.reference-expand-button:hover {
|
||||
background: var(--khoj-sun);
|
||||
color: var(--khoj-storm-grey);
|
||||
background: var(--background-modifier-active-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
a.inline-chat-link {
|
||||
color: #475569;
|
||||
@@ -229,15 +230,6 @@ a.inline-chat-link {
|
||||
border-bottom: 1px dotted var(--khoj-storm-grey);
|
||||
}
|
||||
|
||||
button.copy-button {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-base-00);
|
||||
}
|
||||
button.copy-button:hover {
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
img {
|
||||
max-width: 60%;
|
||||
}
|
||||
@@ -270,19 +262,8 @@ div.conversation-session {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.three-dot-menu {
|
||||
display: block;
|
||||
/* background: var(--background-color); */
|
||||
/* border: 1px solid var(--main-text-color); */
|
||||
border-radius: 5px;
|
||||
/* position: relative; */
|
||||
}
|
||||
|
||||
button.selected-conversation {
|
||||
background: var(--khoj-winter-sun);
|
||||
}
|
||||
button.three-dot-menu-button-item {
|
||||
color: var(--color-base-90);
|
||||
color: var(--text-accent);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
@@ -296,26 +277,7 @@ button.three-dot-menu-button-item {
|
||||
}
|
||||
|
||||
button.three-dot-menu-button-item:hover {
|
||||
background: var(--khoj-storm-grey);
|
||||
color: var(--khoj-winter-sun);
|
||||
}
|
||||
|
||||
.three-dot-menu-button {
|
||||
background: var(--khoj-winter-sun);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease-in-out;
|
||||
font-family: var(--font-family);
|
||||
border-radius: 4px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.conversation-button:hover .three-dot-menu {
|
||||
display: block;
|
||||
background: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
div.conversation-menu {
|
||||
@@ -325,13 +287,15 @@ div.conversation-menu {
|
||||
text-align: right;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
display: grid;
|
||||
grid-gap: 4px;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
div.conversation-session:hover {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
div.selected-conversation {
|
||||
background: var(--khoj-winter-sun) !important;
|
||||
color: var(--khoj-storm-grey) !important;
|
||||
background: var(--background-modifier-active-hover) !important;
|
||||
}
|
||||
|
||||
#khoj-chat-footer {
|
||||
@@ -373,9 +337,8 @@ div.selected-conversation {
|
||||
position: relative;
|
||||
}
|
||||
#khoj-chat-send .lucide-arrow-up-circle {
|
||||
background: var(--khoj-sun);
|
||||
background: var(--background-modifier-active-hover);
|
||||
border-radius: 50%;
|
||||
color: #222;
|
||||
}
|
||||
#khoj-chat-send .lucide-stop-circle {
|
||||
transform: rotateY(-180deg) rotateZ(-90deg);
|
||||
@@ -488,7 +451,7 @@ div.khoj-logo {
|
||||
}
|
||||
|
||||
.khoj-nav a {
|
||||
color: var(--main-text-color);
|
||||
color: var(--text-normal);
|
||||
text-decoration: none;
|
||||
font-size: small;
|
||||
font-weight: normal;
|
||||
@@ -498,11 +461,11 @@ div.khoj-logo {
|
||||
margin: 0;
|
||||
}
|
||||
.khoj-nav a:hover {
|
||||
background-color: var(--khoj-sun);
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
a.khoj-nav-selected {
|
||||
background-color: var(--khoj-winter-sun);
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
#similar-nav-icon-svg,
|
||||
.khoj-nav-icon {
|
||||
@@ -520,10 +483,12 @@ span.khoj-nav-item-text {
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
button.copy-button {
|
||||
button.chat-action-button {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid var(--main-text-color);
|
||||
color: var(--text-muted);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--khoj-storm-grey);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.5s;
|
||||
@@ -532,28 +497,54 @@ button.copy-button {
|
||||
margin-top: 8px;
|
||||
float: right;
|
||||
}
|
||||
button.copy-button span {
|
||||
button.chat-action-button span {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transition: 0.5s;
|
||||
}
|
||||
button.chat-action-button:hover {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
img.copy-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.you button.copy-button {
|
||||
color: var(--text-on-accent);
|
||||
|
||||
/* Circular Loading Spinner */
|
||||
.loader {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid #FFF;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
.khoj button.copy-button {
|
||||
color: var(--khoj-storm-grey);
|
||||
.loader::after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-bottom-color: var(--flower);
|
||||
}
|
||||
.you button.copy-button:hover {
|
||||
color: var(--khoj-storm-grey);
|
||||
background: var(--text-on-accent);
|
||||
}
|
||||
.khoj button.copy-button:hover {
|
||||
background: var(--text-on-accent);
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
@@ -613,6 +604,44 @@ img.copy-icon {
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Encircle */
|
||||
.loading-encircle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-encircle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: -16px;
|
||||
margin-left: -16px;
|
||||
border: 4px solid transparent;
|
||||
border-color: var(--icon-color-active);
|
||||
border-radius: 50%;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.2;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
div.khoj-header {
|
||||
display: grid;
|
||||
|
||||
@@ -51,5 +51,17 @@
|
||||
"1.12.0": "0.15.0",
|
||||
"1.12.1": "0.15.0",
|
||||
"1.13.0": "0.15.0",
|
||||
"1.14.0": "0.15.0"
|
||||
"1.14.0": "0.15.0",
|
||||
"1.15.0": "0.15.0",
|
||||
"1.16.0": "0.15.0",
|
||||
"1.17.0": "0.15.0",
|
||||
"1.20.0": "0.15.0",
|
||||
"1.20.1": "0.15.0",
|
||||
"1.20.2": "0.15.0",
|
||||
"1.20.3": "0.15.0",
|
||||
"1.20.4": "0.15.0",
|
||||
"1.21.0": "0.15.0",
|
||||
"1.21.1": "0.15.0",
|
||||
"1.21.2": "0.15.0",
|
||||
"1.21.3": "0.15.0"
|
||||
}
|
||||
|
||||
1
src/interface/web/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_ENV='development'
|
||||
1
src/interface/web/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_ENV='production'
|
||||
11
src/interface/web/.eslintrc.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": [
|
||||
"next",
|
||||
"next/core-web-vitals",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "warn"
|
||||
}
|
||||
}
|
||||
36
src/interface/web/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
5
src/interface/web/.husky/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn run lint-staged
|
||||
yarn test
|
||||
93
src/interface/web/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
This is a [Next.js](https://nextjs.org/) project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
In case you run into any dependency linking issues, you can try running:
|
||||
|
||||
```bash
|
||||
yarn add next
|
||||
```
|
||||
|
||||
### Run the development server:
|
||||
|
||||
```bash
|
||||
yarn 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.
|
||||
|
||||
```js
|
||||
rewrites: async () => {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://localhost:42110/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
```
|
||||
|
||||
The `destination` should be the URL of the API server.
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying any of the `.tsx` pages. The page auto-updates as you edit the file.
|
||||
|
||||
### Testing built files
|
||||
|
||||
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:
|
||||
```bash
|
||||
yarn export
|
||||
```
|
||||
|
||||
If you're using Windows:
|
||||
```bash
|
||||
yarn windowsexport
|
||||
```
|
||||
|
||||
|
||||
2. Continuously building code
|
||||
|
||||
To keep building the files and serving them, run:
|
||||
```bash
|
||||
yarn watch
|
||||
```
|
||||
|
||||
If you're using Windows:
|
||||
```bash
|
||||
yarn 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:
|
||||
|
||||
```python
|
||||
@web_client.post("/new_route", response_class=FileResponse)
|
||||
@requires(["authenticated"], redirect="login_page")
|
||||
def index_post(request: Request):
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"new_file/index.html",
|
||||
context={
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Next.js App Router](https://nextjs.org/docs/app) - learn about the Next.js router.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
66
src/interface/web/app/agents/agents.module.css
Normal file
@@ -0,0 +1,66 @@
|
||||
div.titleBar {
|
||||
padding: 16px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.agentPersonality p {
|
||||
white-space: inherit;
|
||||
overflow: hidden;
|
||||
height: 77px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
div.agentPersonality {
|
||||
text-align: left;
|
||||
grid-column: span 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.pageLayout {
|
||||
max-width: 60vw;
|
||||
margin: auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
div.sidePanel {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
button.infoButton {
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
div.agentList {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
padding-top: 30px;
|
||||
margin-right: auto;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
div.agentList {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
div.pageLayout {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
div.sidePanel {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
52
src/interface/web/app/agents/layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Noto_Sans } from "next/font/google";
|
||||
import "../globals.css";
|
||||
|
||||
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Agents",
|
||||
description: "Find a specialized agent that can help you address more specific needs.",
|
||||
icons: {
|
||||
icon: "/static/assets/icons/khoj_lantern.ico",
|
||||
apple: "/static/assets/icons/khoj_lantern_256x256.png",
|
||||
},
|
||||
openGraph: {
|
||||
siteName: "Khoj AI",
|
||||
title: "Khoj AI - Agents",
|
||||
description: "Your Second Brain.",
|
||||
url: "https://app.khoj.dev/agents",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://assets.khoj.dev/khoj_lantern_256x256.png",
|
||||
width: 256,
|
||||
height: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
media-src * blob:;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
339
src/interface/web/app/agents/page.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
"use client";
|
||||
|
||||
import styles from "./agents.module.css";
|
||||
|
||||
import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useAuthenticatedData, UserProfile } from "../common/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
import { PaperPlaneTilt, Lightning, Plus } from "@phosphor-icons/react";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import LoginPrompt from "../components/loginPrompt/loginPrompt";
|
||||
import { InlineLoading } from "../components/loading/loading";
|
||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||
import { getIconFromIconName } from "../common/iconUtils";
|
||||
import { convertColorToTextClass } from "../common/colorUtils";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { useIsMobileWidth } from "../common/utils";
|
||||
|
||||
export interface AgentData {
|
||||
slug: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
persona: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
async function openChat(slug: string, userData: UserProfile | null) {
|
||||
const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`;
|
||||
if (!userData) {
|
||||
window.location.href = unauthenticatedRedirectUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
|
||||
const data = await response.json();
|
||||
if (response.status == 200) {
|
||||
window.location.href = `/chat?conversationId=${data.conversation_id}`;
|
||||
} else if (response.status == 403 || response.status == 401) {
|
||||
window.location.href = unauthenticatedRedirectUrl;
|
||||
} else {
|
||||
alert("Failed to start chat session");
|
||||
}
|
||||
}
|
||||
|
||||
const agentsFetcher = () =>
|
||||
window
|
||||
.fetch("/api/agents")
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
interface AgentCardProps {
|
||||
data: AgentData;
|
||||
userProfile: UserProfile | null;
|
||||
isMobileWidth: boolean;
|
||||
}
|
||||
|
||||
function AgentCard(props: AgentCardProps) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const agentSlug = searchParams.get("agent");
|
||||
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
|
||||
const userData = props.userProfile;
|
||||
|
||||
if (showModal) {
|
||||
window.history.pushState(
|
||||
{},
|
||||
`Khoj AI - Agent ${props.data.slug}`,
|
||||
`/agents?agent=${props.data.slug}`,
|
||||
);
|
||||
}
|
||||
|
||||
const stylingString = convertColorToTextClass(props.data.color);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`shadow-sm bg-gradient-to-b from-white 20% to-${props.data.color ? props.data.color : "gray"}-100/50 dark:from-[hsl(var(--background))] dark:to-${props.data.color ? props.data.color : "gray"}-950/50 rounded-xl hover:shadow-md`}
|
||||
>
|
||||
{showLoginPrompt && (
|
||||
<LoginPrompt
|
||||
loginRedirectMessage={`Sign in to start chatting with ${props.data.name}`}
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
/>
|
||||
)}
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{!props.isMobileWidth ? (
|
||||
<Dialog
|
||||
open={showModal}
|
||||
onOpenChange={() => {
|
||||
setShowModal(!showModal);
|
||||
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<div className="flex items-center relative top-2">
|
||||
{getIconFromIconName(props.data.icon, props.data.color) || (
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
{props.data.name}
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<div className="float-right">
|
||||
{props.userProfile ? (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
|
||||
onClick={() => openChat(props.data.slug, userData)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
|
||||
onClick={() => setShowLoginPrompt(true)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DialogContent className="whitespace-pre-line max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center">
|
||||
{getIconFromIconName(props.data.icon, props.data.color) || (
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={32}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
<p className="font-bold text-lg">{props.data.name}</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
|
||||
{props.data.persona}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className={`pt-6 pb-6 ${stylingString} bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white border-2 border-stone-100 shadow-sm rounded-xl hover:bg-stone-100 dark:hover:bg-neutral-900 dark:border-neutral-700`}
|
||||
onClick={() => {
|
||||
openChat(props.data.slug, userData);
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 m-2 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
Start Chatting
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Drawer
|
||||
open={showModal}
|
||||
onOpenChange={(open) => {
|
||||
setShowModal(open);
|
||||
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger>
|
||||
<div className="flex items-center">
|
||||
{getIconFromIconName(props.data.icon, props.data.color) || (
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
{props.data.name}
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<div className="float-right">
|
||||
{props.userProfile ? (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100`}
|
||||
onClick={() => openChat(props.data.slug, userData)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm`}
|
||||
onClick={() => setShowLoginPrompt(true)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DrawerContent className="whitespace-pre-line p-2">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{props.data.name}</DrawerTitle>
|
||||
<DrawerDescription>Full Prompt</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{props.data.persona}
|
||||
<DrawerFooter>
|
||||
<DrawerClose>Done</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.agentPersonality}>
|
||||
<button
|
||||
className={`${styles.infoButton} text-neutral-500 dark:text-white`}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<p>{props.data.persona}</p>
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Agents() {
|
||||
const { data, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
const isMobileWidth = useIsMobileWidth();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>Agents</div>
|
||||
<div className={styles.agentList}>Error loading agents</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.agentList}>
|
||||
<InlineLoading /> booting up your agents
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`w-full mx-auto`}>
|
||||
<div className={`grid w-full mx-auto`}>
|
||||
<div className={`${styles.sidePanel} top-0`}>
|
||||
<SidePanel
|
||||
conversationId={null}
|
||||
uploadedFiles={[]}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.pageLayout} w-full`}>
|
||||
<div className={`pt-6 md:pt-8 flex justify-between`}>
|
||||
<h1 className="text-3xl flex items-center">Agents</h1>
|
||||
<div className="ml-auto float-right border p-2 pt-3 rounded-xl font-bold hover:bg-stone-100 dark:hover:bg-neutral-900">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex flex-row">
|
||||
<Plus className="pr-2 w-6 h-6" />
|
||||
<p className="pr-2">Create Agent</p>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Coming Soon!</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
{showLoginPrompt && (
|
||||
<LoginPrompt
|
||||
loginRedirectMessage="Sign in to start chatting with a specialized agent"
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
/>
|
||||
)}
|
||||
<Alert className="bg-secondary border-none my-4">
|
||||
<AlertDescription>
|
||||
<Lightning weight={"fill"} className="h-4 w-4 text-purple-400 inline" />
|
||||
<span className="font-bold">How it works</span> Use any of these
|
||||
specialized personas to tune your conversation to your needs.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className={`${styles.agentList}`}>
|
||||
{data.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.slug}
|
||||
data={agent}
|
||||
userProfile={authenticatedData}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
37
src/interface/web/app/automations/automations.module.css
Normal file
@@ -0,0 +1,37 @@
|
||||
div.automationsLayout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
div.automationCard {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
div.pageLayout {
|
||||
max-width: 60vw;
|
||||
margin: auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
div.sidePanel {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
div.automationsLayout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
div.pageLayout {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
div.sidePanel {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
40
src/interface/web/app/automations/layout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
import "../globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Automations",
|
||||
description: "Use Autoomations with Khoj to simplify the process of running repetitive tasks.",
|
||||
icons: {
|
||||
icon: "/static/assets/icons/khoj_lantern.ico",
|
||||
apple: "/static/assets/icons/khoj_lantern_256x256.png",
|
||||
},
|
||||
openGraph: {
|
||||
siteName: "Khoj AI",
|
||||
title: "Khoj AI - Automations",
|
||||
description: "Your Second Brain.",
|
||||
url: "https://app.khoj.dev/automations",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://assets.khoj.dev/khoj_lantern_256x256.png",
|
||||
width: 256,
|
||||
height: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1166
src/interface/web/app/automations/page.tsx
Normal file
120
src/interface/web/app/chat/chat.module.css
Normal file
@@ -0,0 +1,120 @@
|
||||
div.main {
|
||||
height: 100dvh;
|
||||
color: hsla(var(--foreground));
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: flex;
|
||||
overflow-x: none;
|
||||
height: 50%;
|
||||
padding: 10px;
|
||||
white-space: nowrap;
|
||||
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); */
|
||||
gap: 1rem;
|
||||
/* justify-content: center; */
|
||||
}
|
||||
|
||||
div.inputBox {
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
input.inputBox {
|
||||
border: none;
|
||||
}
|
||||
|
||||
input.inputBox:focus {
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
div.inputBox:focus {
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
div.chatBodyFull {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button.inputBox {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: linear-gradient(var(--calm-green), var(--calm-blue));
|
||||
}
|
||||
|
||||
div.chatBody {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inputBox {
|
||||
color: hsla(var(--foreground));
|
||||
}
|
||||
|
||||
div.chatLayout {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
div.chatBox {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.titleBar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
div.chatBoxBody {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 70%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
div.agentIndicator a {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.agentIndicator {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
div.inputBox {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
div.chatBoxBody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.chatBody {
|
||||
grid-template-columns: 0fr 1fr;
|
||||
}
|
||||
|
||||
div.chatBox {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.chatLayout {
|
||||
gap: 0;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
53
src/interface/web/app/chat/layout.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Noto_Sans } from "next/font/google";
|
||||
import "../globals.css";
|
||||
|
||||
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Chat",
|
||||
description:
|
||||
"Ask anything. Khoj will use the internet and your docs to answer, paint and even automate stuff for you.",
|
||||
icons: {
|
||||
icon: "/static/assets/icons/khoj_lantern.ico",
|
||||
apple: "/static/assets/icons/khoj_lantern_256x256.png",
|
||||
},
|
||||
openGraph: {
|
||||
siteName: "Khoj AI",
|
||||
title: "Khoj AI - Chat",
|
||||
description: "Your Second Brain.",
|
||||
url: "https://app.khoj.dev/chat",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://assets.khoj.dev/khoj_lantern_256x256.png",
|
||||
width: 256,
|
||||
height: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
media-src * blob:;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
291
src/interface/web/app/chat/page.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
"use client";
|
||||
|
||||
import styles from "./chat.module.css";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
|
||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||
import ChatHistory from "../components/chatHistory/chatHistory";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Loading from "../components/loading/loading";
|
||||
|
||||
import { processMessageChunk } from "../common/chatFunctions";
|
||||
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
|
||||
import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../common/utils";
|
||||
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
|
||||
import { useAuthenticatedData } from "../common/auth";
|
||||
import { AgentData } from "../agents/page";
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
setTitle: (title: string) => void;
|
||||
onConversationIdChange?: (conversationId: string) => void;
|
||||
setQueryToProcess: (query: string) => void;
|
||||
streamedMessages: StreamMessage[];
|
||||
setUploadedFiles: (files: string[]) => void;
|
||||
isMobileWidth?: boolean;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const conversationId = searchParams.get("conversationId");
|
||||
const [message, setMessage] = useState("");
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
|
||||
|
||||
const setQueryToProcess = props.setQueryToProcess;
|
||||
const onConversationIdChange = props.onConversationIdChange;
|
||||
|
||||
useEffect(() => {
|
||||
const storedMessage = localStorage.getItem("message");
|
||||
if (storedMessage) {
|
||||
setProcessingMessage(true);
|
||||
setQueryToProcess(storedMessage);
|
||||
}
|
||||
}, [setQueryToProcess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
setProcessingMessage(true);
|
||||
setQueryToProcess(message);
|
||||
}
|
||||
}, [message, setQueryToProcess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId) {
|
||||
onConversationIdChange?.(conversationId);
|
||||
}
|
||||
}, [conversationId, onConversationIdChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.streamedMessages &&
|
||||
props.streamedMessages.length > 0 &&
|
||||
props.streamedMessages[props.streamedMessages.length - 1].completed
|
||||
) {
|
||||
setProcessingMessage(false);
|
||||
} else {
|
||||
setMessage("");
|
||||
}
|
||||
}, [props.streamedMessages]);
|
||||
|
||||
if (!conversationId) {
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={false ? styles.chatBody : styles.chatBodyFull}>
|
||||
<ChatHistory
|
||||
conversationId={conversationId}
|
||||
setTitle={props.setTitle}
|
||||
setAgent={setAgentMetadata}
|
||||
pendingMessage={processingMessage ? message : ""}
|
||||
incomingMessages={props.streamedMessages}
|
||||
/>
|
||||
</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-t-2xl rounded-b-none md:rounded-xl`}
|
||||
>
|
||||
<ChatInputArea
|
||||
agentColor={agentMetadata?.color}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
sendDisabled={processingMessage}
|
||||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={conversationId}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
const defaultTitle = "Khoj AI - Chat";
|
||||
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [title, setTitle] = useState(defaultTitle);
|
||||
const [conversationId, setConversationID] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>("");
|
||||
const [processQuerySignal, setProcessQuerySignal] = useState(false);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const locationData = useIPLocationData();
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
const isMobileWidth = useIsMobileWidth();
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/chat/options")
|
||||
.then((response) => response.json())
|
||||
.then((data: ChatOptions) => {
|
||||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
if (data) {
|
||||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
||||
welcomeConsole();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryToProcess) {
|
||||
const newStreamMessage: StreamMessage = {
|
||||
rawResponse: "",
|
||||
trainOfThought: [],
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
|
||||
setProcessQuerySignal(true);
|
||||
}
|
||||
}, [queryToProcess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (processQuerySignal) {
|
||||
chat();
|
||||
}
|
||||
}, [processQuerySignal]);
|
||||
|
||||
async function readChatStream(response: Response) {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is null");
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const eventDelimiter = "␃🔚␗";
|
||||
let buffer = "";
|
||||
|
||||
// Track context used for chat response
|
||||
let context: Context[] = [];
|
||||
let onlineContext: OnlineContext = {};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
break;
|
||||
}
|
||||
|
||||
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 } = processMessageChunk(
|
||||
event,
|
||||
currentMessage,
|
||||
context,
|
||||
onlineContext,
|
||||
));
|
||||
|
||||
setMessages([...messages]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function chat() {
|
||||
localStorage.removeItem("message");
|
||||
if (!queryToProcess || !conversationId) return;
|
||||
let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`;
|
||||
if (locationData) {
|
||||
chatAPI += `®ion=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`;
|
||||
}
|
||||
|
||||
const response = await fetch(chatAPI);
|
||||
try {
|
||||
await readChatStream(response);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// 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;
|
||||
currentMessage.rawResponse = `Encountered Error: ${errorMessage}. Please try again later.`;
|
||||
|
||||
// Complete message streaming teardown properly
|
||||
currentMessage.completed = true;
|
||||
setMessages([...messages]);
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleConversationIdChange = (newConversationId: string) => {
|
||||
setConversationID(newConversationId);
|
||||
};
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
return (
|
||||
<div className={`${styles.main} ${styles.chatLayout}`}>
|
||||
<title>
|
||||
{`${defaultTitle}${!!title && title !== defaultTitle ? `: ${title}` : ""}`}
|
||||
</title>
|
||||
<div>
|
||||
<SidePanel
|
||||
conversationId={conversationId}
|
||||
uploadedFiles={uploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chatBox}>
|
||||
<div className={styles.chatBoxBody}>
|
||||
{!isMobileWidth && (
|
||||
<div
|
||||
className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}
|
||||
>
|
||||
{title && (
|
||||
<h2
|
||||
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ChatBodyData
|
||||
isLoggedIn={authenticatedData !== null}
|
||||
streamedMessages={messages}
|
||||
chatOptionsData={chatOptionsData}
|
||||
setTitle={setTitle}
|
||||
setQueryToProcess={setQueryToProcess}
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
onConversationIdChange={handleConversationIdChange}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||