Compare commits
498 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ff871217 | ||
|
|
979088b3dc | ||
|
|
255781e135 | ||
|
|
b2d229c116 | ||
|
|
f4cef377ca | ||
|
|
20cb314171 | ||
|
|
07cf5a214a | ||
|
|
7364bac8ae | ||
|
|
62704cac09 | ||
|
|
77755c0284 | ||
|
|
4c135ea316 | ||
|
|
0a86220d42 | ||
|
|
362063f5fe | ||
|
|
571e8c2548 | ||
|
|
4b79d8216f | ||
|
|
61e131f95c | ||
|
|
af30d01e85 | ||
|
|
171ce19e1f | ||
|
|
e588f7c528 | ||
|
|
c9fc4d1296 | ||
|
|
11f0a9f196 | ||
|
|
48870d9170 | ||
|
|
279662620b | ||
|
|
2ec8da89e8 | ||
|
|
bf427cd8dd | ||
|
|
1d77fe712c | ||
|
|
2f31de5ed5 | ||
|
|
d97682fdac | ||
|
|
f5cf09424b | ||
|
|
a2c668268f | ||
|
|
d688ddf92c | ||
|
|
8f36572a9b | ||
|
|
41ac1e24c9 | ||
|
|
6c2a8a5bce | ||
|
|
e111eda6ae | ||
|
|
e562114f6b | ||
|
|
46269ddfd3 | ||
|
|
0ba838b53a | ||
|
|
8271abe729 | ||
|
|
c12ec1fd03 | ||
|
|
257a421e45 | ||
|
|
4e6b66b139 | ||
|
|
3ff5074cf5 | ||
|
|
68e635cc32 | ||
|
|
67a8795b1f | ||
|
|
79b1b1d350 | ||
|
|
afd162de01 | ||
|
|
8f491d72de | ||
|
|
5889eceba4 | ||
|
|
89354def9b | ||
|
|
b1940519c3 | ||
|
|
ecf9730cd7 | ||
|
|
017e8c1aef | ||
|
|
a6f313589e | ||
|
|
70f6b8266c | ||
|
|
8bfd5828e6 | ||
|
|
92d81d3b16 | ||
|
|
61403138d5 | ||
|
|
ea3dc2cfa3 | ||
|
|
260272dca2 | ||
|
|
bf8914d0c8 | ||
|
|
faad1297f4 | ||
|
|
0f993b332e | ||
|
|
fa218ff5aa | ||
|
|
a8b83da872 | ||
|
|
c9db5321e7 | ||
|
|
77a45f4215 | ||
|
|
b86a3bb0c5 | ||
|
|
a52c1c8380 | ||
|
|
eff1436857 | ||
|
|
30459ee4ba | ||
|
|
feac71ce1e | ||
|
|
1a1b044d12 | ||
|
|
e4c445f805 | ||
|
|
4b02a8c788 | ||
|
|
b6772d8fc3 | ||
|
|
2a7e4f2b71 | ||
|
|
4915b7214d | ||
|
|
c747562897 | ||
|
|
bab7f39d47 | ||
|
|
36537606da | ||
|
|
ea9ae4ae28 | ||
|
|
d2083dd395 | ||
|
|
a71440f62a | ||
|
|
7db97d8aa9 | ||
|
|
f0f6390366 | ||
|
|
2fbc609233 | ||
|
|
d77e05c279 | ||
|
|
32d73500ba | ||
|
|
30d87a9a01 | ||
|
|
51826d28d6 | ||
|
|
dac2d14380 | ||
|
|
dbe713604d | ||
|
|
931aab4464 | ||
|
|
d01afb3ee4 | ||
|
|
01aa285d7b | ||
|
|
31655447e7 | ||
|
|
cebaa51c2f | ||
|
|
796102c74e | ||
|
|
a443af3a71 | ||
|
|
db3026739d | ||
|
|
ef72508914 | ||
|
|
b950889f47 | ||
|
|
780c769567 | ||
|
|
6c10d68262 | ||
|
|
b2dd946c6d | ||
|
|
51dfa48e2b | ||
|
|
65bf894302 | ||
|
|
d212298573 | ||
|
|
da2de21339 | ||
|
|
77672ac0ae | ||
|
|
6edc32f2f4 | ||
|
|
f272d4503e | ||
|
|
b41c14b258 | ||
|
|
e6053951f0 | ||
|
|
ab7dabe74f | ||
|
|
601b738135 | ||
|
|
fecf6700d2 | ||
|
|
70e550250a | ||
|
|
5f2717cc4b | ||
|
|
5f7eaa7ded | ||
|
|
56ce97ef9e | ||
|
|
f516d127c8 | ||
|
|
b1767f93d6 | ||
|
|
8eae7c898c | ||
|
|
630bf995f1 | ||
|
|
1773a78339 | ||
|
|
212b1a96c8 | ||
|
|
0636ceaf14 | ||
|
|
510bb7e684 | ||
|
|
1b11d5723d | ||
|
|
09f739b8cc | ||
|
|
c0d35bafdd | ||
|
|
9d62d66a77 | ||
|
|
2697c7a186 | ||
|
|
227169ebde | ||
|
|
37a1f15c38 | ||
|
|
5da6a5e669 | ||
|
|
ddd550e6f4 | ||
|
|
35e24d7851 | ||
|
|
5e39421f56 | ||
|
|
4410a3bb4b | ||
|
|
ffe66b848a | ||
|
|
b1890aa050 | ||
|
|
946af0889d | ||
|
|
40d1abfe50 | ||
|
|
05a3c81adb | ||
|
|
edabede93a | ||
|
|
98642e01b5 | ||
|
|
4744d69221 | ||
|
|
26abafa658 | ||
|
|
2728c714d7 | ||
|
|
20a37697de | ||
|
|
c467a0cbb0 | ||
|
|
0ce2ec590a | ||
|
|
d30a9ddd33 | ||
|
|
6d4aad57e1 | ||
|
|
69d4fa6525 | ||
|
|
5c4eb950d5 | ||
|
|
2cd3e799d3 | ||
|
|
d5fb4196de | ||
|
|
5c7c8d1f46 | ||
|
|
1192e49307 | ||
|
|
0144e610d6 | ||
|
|
f6a7aa6c96 | ||
|
|
6d94d6e75a | ||
|
|
d292dc03b3 | ||
|
|
db07362ca3 | ||
|
|
285d17af2a | ||
|
|
79d325fbb6 | ||
|
|
e97a20d70c | ||
|
|
6224dce49d | ||
|
|
4722a2c16d | ||
|
|
668135c763 | ||
|
|
81183a1fe1 | ||
|
|
a44cde2865 | ||
|
|
595cc5b0f5 | ||
|
|
e06be395f9 | ||
|
|
e31a540a5e | ||
|
|
6fdac24416 | ||
|
|
87975e589a | ||
|
|
9c70af960c | ||
|
|
10d4c38ce9 | ||
|
|
aad7f825e0 | ||
|
|
5f97afbfac | ||
|
|
c2d46de8bc | ||
|
|
ded3100caf | ||
|
|
3f24e53b6e | ||
|
|
63ec84ad78 | ||
|
|
0c1c7583b5 | ||
|
|
31d17d0b22 | ||
|
|
c29c141a7e | ||
|
|
9f00a366ab | ||
|
|
ac96f43b1b | ||
|
|
07ade2262a | ||
|
|
751edfefe5 | ||
|
|
3a61919344 | ||
|
|
019d3732de | ||
|
|
08d79f5ba4 | ||
|
|
a6cd96a6a9 | ||
|
|
c68cde4803 | ||
|
|
59fa48036f | ||
|
|
139a3ba060 | ||
|
|
c5666e0404 | ||
|
|
5d5ebcbf7c | ||
|
|
7119ed0849 | ||
|
|
948ba6ddca | ||
|
|
6212d7c2e8 | ||
|
|
f65ff9815d | ||
|
|
eb6175e9b0 | ||
|
|
bb2363f324 | ||
|
|
caab55fbdd | ||
|
|
de2494154f | ||
|
|
168c11cea7 | ||
|
|
8617cf1389 | ||
|
|
d13db2e666 | ||
|
|
5f4223efb4 | ||
|
|
0e63a90377 | ||
|
|
f0efe0177e | ||
|
|
f6ceb22373 | ||
|
|
068ee0ac5e | ||
|
|
6508379d7b | ||
|
|
7af8a56434 | ||
|
|
ec280067ef | ||
|
|
90439a8db1 | ||
|
|
e022910f31 | ||
|
|
e9ed7a19fd | ||
|
|
89fbfce20a | ||
|
|
bbe3bf9733 | ||
|
|
e3892945d4 | ||
|
|
85144006a1 | ||
|
|
acd14a5e41 | ||
|
|
d63194c3a9 | ||
|
|
286b500f66 | ||
|
|
1b3effd8e6 | ||
|
|
1cd9ecd449 | ||
|
|
ed4d0f9076 | ||
|
|
703a7c89c0 | ||
|
|
18081b3bc6 | ||
|
|
277d2f5c96 | ||
|
|
334be4e600 | ||
|
|
efcf7d1508 | ||
|
|
b484953bb3 | ||
|
|
9cfaaf0941 | ||
|
|
a0d0dbaca7 | ||
|
|
ebb5d7b8e5 | ||
|
|
d02415edcc | ||
|
|
dc0626856e | ||
|
|
dc495babb3 | ||
|
|
55d72231b3 | ||
|
|
e9f04dc644 | ||
|
|
07b19964d4 | ||
|
|
d42f0f5055 | ||
|
|
134cce9d32 | ||
|
|
3ede919c66 | ||
|
|
f2e89f6f46 | ||
|
|
9ca61d62ff | ||
|
|
131b8407b5 | ||
|
|
cc75f986b2 | ||
|
|
f9ccce430e | ||
|
|
f0253e2cbb | ||
|
|
6b535cc345 | ||
|
|
02aeee60aa | ||
|
|
94825a70b9 | ||
|
|
5de04621b5 | ||
|
|
0e3fb59e09 | ||
|
|
45a991d75c | ||
|
|
3386cc92b5 | ||
|
|
948a4274e4 | ||
|
|
731ef5688f | ||
|
|
f046523b33 | ||
|
|
76df393eb5 | ||
|
|
ceae06ae9d | ||
|
|
8269adf849 | ||
|
|
865d12b6f2 | ||
|
|
26cb878327 | ||
|
|
e3180d63e6 | ||
|
|
62e6e09521 | ||
|
|
b079fb31bc | ||
|
|
d90df966a9 | ||
|
|
dc3f399f91 | ||
|
|
d5000c63e1 | ||
|
|
453c84ab79 | ||
|
|
35aa06067f | ||
|
|
8f4e5d3d83 | ||
|
|
5673bd5b96 | ||
|
|
a2ab68a7a2 | ||
|
|
67129964a7 | ||
|
|
d3257cb24e | ||
|
|
40091489c0 | ||
|
|
240db7b4f0 | ||
|
|
234be96e53 | ||
|
|
53d421f9c6 | ||
|
|
c8c0cfd10e | ||
|
|
7ecae224e7 | ||
|
|
3d616c8d65 | ||
|
|
23bd737f6b | ||
|
|
81e98c3079 | ||
|
|
59ff1ae27f | ||
|
|
001ac7b5eb | ||
|
|
112f388ada | ||
|
|
1d3d949962 | ||
|
|
cd46a17e5f | ||
|
|
c0972e09e6 | ||
|
|
64fff1d372 | ||
|
|
7478d08803 | ||
|
|
fc218508f9 | ||
|
|
87090531da | ||
|
|
83a7ccd729 | ||
|
|
5c2327ee4f | ||
|
|
6e8a40906d | ||
|
|
526a927bce | ||
|
|
7243059507 | ||
|
|
8a9055f918 | ||
|
|
ae535a06eb | ||
|
|
36b17d4ae0 | ||
|
|
924424c754 | ||
|
|
359a2cacef | ||
|
|
d7fb9a596e | ||
|
|
8a21aff438 | ||
|
|
cb40a96c85 | ||
|
|
50760acc37 | ||
|
|
82eb4bfd0d | ||
|
|
99d19dcf43 | ||
|
|
c92d79118a | ||
|
|
e281a498b4 | ||
|
|
4f655d20ae | ||
|
|
f6ff7b1beb | ||
|
|
285a2b86d2 | ||
|
|
67c850a4ac | ||
|
|
0aebf624fc | ||
|
|
ff846f05c5 | ||
|
|
7e36f421f9 | ||
|
|
4725416fbd | ||
|
|
508b2176b7 | ||
|
|
b08745b541 | ||
|
|
27217a330d | ||
|
|
5e9558d39d | ||
|
|
cf28f104c7 | ||
|
|
93e2aff786 | ||
|
|
d78454d4ad | ||
|
|
4070d13a96 | ||
|
|
863933daaa | ||
|
|
e9ca04af0d | ||
|
|
06df394d6c | ||
|
|
364e6c11af | ||
|
|
36b52fdd0a | ||
|
|
72f63a6ef7 | ||
|
|
e4d67694e1 | ||
|
|
98e5ea4940 | ||
|
|
2f6284872d | ||
|
|
a9b81975f2 | ||
|
|
b351cfb8a0 | ||
|
|
601ff2541b | ||
|
|
e28526bbc9 | ||
|
|
939d7731da | ||
|
|
f63fd0995e | ||
|
|
10836dedee | ||
|
|
08f5fb315f | ||
|
|
f09bdd515b | ||
|
|
36c7389b46 | ||
|
|
2600cc9d4d | ||
|
|
45cb510421 | ||
|
|
d871e04a81 | ||
|
|
1a5d1130f4 | ||
|
|
d0f14d3f85 | ||
|
|
dfb277ee37 | ||
|
|
e75e13d788 | ||
|
|
4e15b4e411 | ||
|
|
1b4d562700 | ||
|
|
b6d63137f1 | ||
|
|
3f719c9e17 | ||
|
|
7526a50dd4 | ||
|
|
7c4d546039 | ||
|
|
c1128a1ad8 | ||
|
|
9306cd901a | ||
|
|
24ddebf3ce | ||
|
|
8609e3129e | ||
|
|
6c0e82b2d6 | ||
|
|
cccd225247 | ||
|
|
b9caad458e | ||
|
|
198d9af8cf | ||
|
|
a71f168273 | ||
|
|
bcc0bed9db | ||
|
|
8bb8824d0c | ||
|
|
e16d0b6d7e | ||
|
|
c3c7b8a951 | ||
|
|
3838f9d8e3 | ||
|
|
f7b8cdd02e | ||
|
|
2739a492b4 | ||
|
|
87d1e1341d | ||
|
|
280061e1fa | ||
|
|
672f61529e | ||
|
|
4fb628975c | ||
|
|
b6cdc5c7cb | ||
|
|
7f994274bb | ||
|
|
d73042426d | ||
|
|
45f461d175 | ||
|
|
7cad1c9428 | ||
|
|
ad1f1cf620 | ||
|
|
9d42b5d60d | ||
|
|
c3b624e351 | ||
|
|
7184508784 | ||
|
|
211e460398 | ||
|
|
c823f46d89 | ||
|
|
b6dbe4dd1d | ||
|
|
1ae40163a9 | ||
|
|
fe03ba3dce | ||
|
|
ed177db2be | ||
|
|
7ad251b8ef | ||
|
|
2bed4c3b50 | ||
|
|
8914dbd073 | ||
|
|
e77a5ffc83 | ||
|
|
b09350c052 | ||
|
|
b177adf3a7 | ||
|
|
ede6eb6879 | ||
|
|
88a9eadfba | ||
|
|
ab501a56c9 | ||
|
|
f944408e69 | ||
|
|
88344f9ed2 | ||
|
|
c2814fce58 | ||
|
|
f3f24387ec | ||
|
|
1e43f1a12e | ||
|
|
9d38eadd42 | ||
|
|
68bd5d9ebc | ||
|
|
d91c7e2761 | ||
|
|
47b58a2a4d | ||
|
|
ab0d3a08e2 | ||
|
|
55a032e8c4 | ||
|
|
fcbbe8c759 | ||
|
|
f57d7bf5ad | ||
|
|
fada617faa | ||
|
|
61b6ee2857 | ||
|
|
47c2cc63e1 | ||
|
|
a8940462c4 | ||
|
|
053d6141f3 | ||
|
|
47569da38e | ||
|
|
36be3c4b8f | ||
|
|
fd0a2f55f8 | ||
|
|
5c0d340970 | ||
|
|
051f0e3fb5 | ||
|
|
5e83baab21 | ||
|
|
6130fddf45 | ||
|
|
8b293edd7c | ||
|
|
7a9a811874 | ||
|
|
dcb86c2d3e | ||
|
|
c641eb4ad6 | ||
|
|
a403def19e | ||
|
|
eee57599ad | ||
|
|
af6d65a909 | ||
|
|
25e06f26c0 | ||
|
|
11873795a6 | ||
|
|
e76c285bdc | ||
|
|
bc7477ea3e | ||
|
|
f83cf4ebc6 | ||
|
|
25a749ca1d | ||
|
|
cc31cd070d | ||
|
|
84322b2a45 | ||
|
|
a4dcb20622 | ||
|
|
24aa696ef5 | ||
|
|
11517ba8eb | ||
|
|
c156b3e087 | ||
|
|
3ec41c4d64 | ||
|
|
d1945c5ba8 | ||
|
|
9a013ec48f | ||
|
|
24c553877c | ||
|
|
6d5930363a | ||
|
|
215235efd2 | ||
|
|
55e4fa9719 | ||
|
|
2445664d40 | ||
|
|
2e052913b6 | ||
|
|
a26ab31d20 | ||
|
|
99a03da3f7 | ||
|
|
d3e82b918f | ||
|
|
c11f7b47e4 | ||
|
|
11a18cc452 | ||
|
|
fbb7747dcc | ||
|
|
66dca6cf33 | ||
|
|
cba9a6a703 | ||
|
|
14f28e3a03 | ||
|
|
f26cee604d | ||
|
|
2e13e15625 | ||
|
|
85ae46f429 | ||
|
|
a8ab9448da | ||
|
|
b415f87093 | ||
|
|
37063f6a38 | ||
|
|
4456cf5c8f | ||
|
|
4070be637c | ||
|
|
c203c6a3fd | ||
|
|
e18124ef6f | ||
|
|
477ef28e08 | ||
|
|
f9fb58aec3 | ||
|
|
86e808abfb | ||
|
|
be6acda212 | ||
|
|
0d0bf3b5aa | ||
|
|
07e9e4ecc3 | ||
|
|
a0b03c8bb1 | ||
|
|
013c7c10a4 |
@@ -6,4 +6,5 @@ docs/
|
||||
tests/
|
||||
build/
|
||||
dist/
|
||||
scripts/
|
||||
*.egg-info/
|
||||
|
||||
6
.github/workflows/build_khoj_el.yml
vendored
@@ -24,16 +24,16 @@ jobs:
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with: { python-version: 3.9 }
|
||||
- name: Install
|
||||
- name: ⏬️ Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
sudo apt-get install emacs && emacs --version
|
||||
git clone https://github.com/riscy/melpazoid.git ~/melpazoid
|
||||
pip install ~/melpazoid
|
||||
- name: Run
|
||||
- name: 🌡️ Validate Khoj.el
|
||||
env:
|
||||
# Khoj recipe from https://github.com/melpa/melpa/pull/8321/files
|
||||
RECIPE: (khoj :fetcher github :repo "debanjum/khoj" :files ("src/interface/emacs/*.el"))
|
||||
RECIPE: (khoj :fetcher github :repo "khoj-ai/khoj" :files ("src/interface/emacs/*.el"))
|
||||
EXIST_OK: true
|
||||
LOCAL_REPO: ${{ github.workspace }}
|
||||
run: echo $GITHUB_REF && make -C ~/melpazoid
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
name: build
|
||||
name: dockerize
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- src/**
|
||||
- src/khoj/**
|
||||
- config/**
|
||||
- setup.py
|
||||
- pyproject.toml
|
||||
- Dockerfile
|
||||
- docker-compose.yml
|
||||
- .github/workflows/build.yml
|
||||
- .github/workflows/dockerize.yml
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -22,19 +24,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.PAT }}
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
- name: 📦 Build and Push Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
@@ -42,4 +44,4 @@ jobs:
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }}
|
||||
build-args: |
|
||||
PORT=8000
|
||||
PORT=8000
|
||||
47
.github/workflows/dockerize_telemetry_server.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: dockerize telemetry server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- src/telemetry/**
|
||||
- .github/workflows/dockerize_telemetry_server.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- src/telemetry/**
|
||||
- .github/workflows/dockerize_telemetry_server.yml
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKER_IMAGE_TAG: ${{ github.ref == 'refs/heads/master' && 'latest' || github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker Image, Push to Container Registry
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.PAT }}
|
||||
|
||||
- name: 📦 Build and Push Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: src/telemetry
|
||||
file: src/telemetry/Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}-telemetry:${{ env.DOCKER_IMAGE_TAG }}
|
||||
secrets: |
|
||||
"POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}"
|
||||
95
.github/workflows/publish.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/**
|
||||
- setup.py
|
||||
- .github/workflows/publish.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/**
|
||||
- setup.py
|
||||
- .github/workflows/publish.yml
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish App to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
|
||||
- name: Install Application
|
||||
run: |
|
||||
pip install --upgrade .
|
||||
|
||||
- name: Publish Release to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }}
|
||||
run: |
|
||||
# Setup Environment for Reproducible Builds
|
||||
export PYTHONHASHSEED=42
|
||||
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
|
||||
|
||||
# Build and Upload PyPi Package
|
||||
rm -rf dist
|
||||
python -m build
|
||||
twine check dist/*
|
||||
twine upload --verbose dist/*
|
||||
|
||||
- name: Publish Master to PyPI
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }}
|
||||
run: |
|
||||
# Set Pre-Release Version
|
||||
sed -E -i "s/version=(.*)',/version=\1a$(date +%s)',/g" setup.py
|
||||
|
||||
# Setup Environment for Reproducible Builds
|
||||
export PYTHONHASHSEED=42
|
||||
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
|
||||
|
||||
# Build and Upload PyPi Package
|
||||
rm -rf dist
|
||||
python -m build
|
||||
twine check dist/*
|
||||
twine upload --verbose dist/*
|
||||
|
||||
- name: Publish PR to Test PyPI
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_KEY }}
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
# Set Development Release Version
|
||||
sed -E -i "s/version=(.*)',/version=\1.dev$PULL_REQUEST_NUMBER$(date +%s)',/g" setup.py
|
||||
|
||||
# Setup Environment for Reproducible Builds
|
||||
export PYTHONHASHSEED=42
|
||||
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
|
||||
|
||||
# Build and Upload PyPi Package
|
||||
rm -rf dist
|
||||
python -m build
|
||||
twine check dist/*
|
||||
twine upload -r testpypi --verbose dist/*
|
||||
64
.github/workflows/pypi.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: pypi
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/pypi.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/khoj/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/pypi.yml
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Python Package to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: ⬇️ Install Application
|
||||
run: python -m pip install --upgrade pip && pip install --upgrade .
|
||||
|
||||
- name: ⚙️ Build Python Package
|
||||
run: |
|
||||
# Setup Environment for Reproducible Builds
|
||||
export PYTHONHASHSEED=42
|
||||
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
|
||||
rm -rf dist
|
||||
|
||||
# Build PyPi Package
|
||||
pipx run build
|
||||
|
||||
- name: 🌡️ Validate Python Package
|
||||
run: |
|
||||
# Validate PyPi Package
|
||||
pipx run check-wheel-contents dist/*.whl
|
||||
pipx run twine check dist/*
|
||||
|
||||
- name: ⏫ Upload Python Package Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: khoj-assistant
|
||||
path: dist/*.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.6.4
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_KEY }}
|
||||
75
.github/workflows/release.yml
vendored
@@ -12,7 +12,61 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
publish_obsidian_plugin:
|
||||
name: 💎 Publish Obsidian Plugin
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: src/interface/obsidian
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: ⚙️ Build Obsidian Plugin
|
||||
run: |
|
||||
yarn
|
||||
yarn run build --if-present
|
||||
|
||||
- name: ⏫ Upload Obsidian Plugin main.js
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: main.js
|
||||
path: src/interface/obsidian/main.js
|
||||
|
||||
- name: ⏫ Upload Obsidian Plugin manifest.json
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: manifest.json
|
||||
path: src/interface/obsidian/manifest.json
|
||||
|
||||
- name: ⏫ Upload Obsidian Plugin styles.css
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: styles.css
|
||||
path: src/interface/obsidian/styles.css
|
||||
|
||||
- name: 🌈 Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
src/interface/obsidian/main.js
|
||||
src/interface/obsidian/manifest.json
|
||||
src/interface/obsidian/styles.css
|
||||
|
||||
publish_desktop_apps:
|
||||
name: 🖥️ Publish Desktop Apps
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -23,6 +77,8 @@ jobs:
|
||||
- os: windows-latest
|
||||
extension: exe
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -31,7 +87,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install Dependencies
|
||||
- name: ⏬️ Install Dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
@@ -40,11 +96,11 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller
|
||||
|
||||
- name: Install Khoj App
|
||||
- name: ⬇️ Install Khoj App
|
||||
run: |
|
||||
pip install --upgrade .
|
||||
|
||||
- name: Package Khoj App
|
||||
- name: 📦 Package Khoj App
|
||||
shell: bash
|
||||
run: |
|
||||
# Setup Environment for Reproducible Builds
|
||||
@@ -56,7 +112,7 @@ jobs:
|
||||
mv dist/Khoj.exe dist/khoj_"$GITHUB_REF_NAME"_amd64.exe
|
||||
fi
|
||||
|
||||
- name: Create Mac App DMG
|
||||
- name: 💻 Create Mac App DMG
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
# Install Mac DMG Creator
|
||||
@@ -66,7 +122,7 @@ jobs:
|
||||
# Create disk image with the app
|
||||
create-dmg \
|
||||
--volname "Khoj" \
|
||||
--volicon "src/interface/web/assets/icons/favicon.icns" \
|
||||
--volicon "src/khoj/interface/web/assets/icons/favicon.icns" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 300 \
|
||||
--icon-size 100 \
|
||||
@@ -80,7 +136,7 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
- name: Create Debian Package
|
||||
- name: 🐧 Create Debian Package
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
shell: bash
|
||||
env:
|
||||
@@ -92,7 +148,7 @@ jobs:
|
||||
# Copy app files into expected output directory structure
|
||||
mkdir -p package/opt package/usr/share/applications package/usr/share/icons/hicolor/128x128/apps
|
||||
cp -r dist/Khoj package/opt/Khoj
|
||||
cp src/interface/web/assets/icons/favicon-128x128.png package/usr/share/icons/hicolor/128x128/apps/Khoj.png
|
||||
cp src/khoj/interface/web/assets/icons/favicon-128x128.png package/usr/share/icons/hicolor/128x128/apps/Khoj.png
|
||||
cp Khoj.desktop package/usr/share/applications
|
||||
|
||||
# Fix permissions to be usable by non-root users
|
||||
@@ -110,8 +166,9 @@ jobs:
|
||||
name: khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
|
||||
path: dist/khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
|
||||
|
||||
- name: Release
|
||||
- name: 🌈 Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
generate_release_notes: true
|
||||
files: dist/khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
|
||||
|
||||
38
.github/workflows/test.yml
vendored
@@ -5,43 +5,53 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/**
|
||||
- src/khoj/**
|
||||
- tests/**
|
||||
- config/**
|
||||
- setup.py
|
||||
- pyproject.toml
|
||||
- .pre-commit-config.yml
|
||||
- .github/workflows/test.yml
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/**
|
||||
- src/khoj/**
|
||||
- tests/**
|
||||
- config/**
|
||||
- setup.py
|
||||
- pyproject.toml
|
||||
- .pre-commit-config.yml
|
||||
- .github/workflows/test.yml
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python_version:
|
||||
- '3.8'
|
||||
- '3.9'
|
||||
- '3.10'
|
||||
- '3.11'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: ${{ matrix.python_version }}
|
||||
|
||||
- name: Install Dependencies
|
||||
- name: ⏬️ Install Dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y libegl1
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest
|
||||
|
||||
- name: Install Application
|
||||
run: |
|
||||
pip install --upgrade .
|
||||
- name: ⬇️ Install Application
|
||||
run: pip install --upgrade .[dev]
|
||||
|
||||
- name: Test Application
|
||||
run: |
|
||||
pytest
|
||||
- name: 🌡️ Validate Application
|
||||
run: pre-commit run --hook-stage manual --all
|
||||
|
||||
- name: 🧪 Test Application
|
||||
run: pytest
|
||||
|
||||
52
.github/workflows/test_khoj_el.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: test khoj.el
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/interface/emacs/*.el
|
||||
- src/interface/emacs/tests/*.el
|
||||
- .github/workflows/test_khoj_el.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- src/interface/emacs/*.el
|
||||
- src/interface/emacs/tests/*.el
|
||||
- .github/workflows/test_khoj_el.yml
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
emacs_version:
|
||||
- 27.1
|
||||
- 27.2
|
||||
- 28.1
|
||||
- 28.2
|
||||
- snapshot
|
||||
steps:
|
||||
- uses: purcell/setup-emacs@master
|
||||
with:
|
||||
version: ${{ matrix.emacs_version }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: 🧪 Test Khoj.el
|
||||
run: |
|
||||
# Run ERT tests on khoj.el
|
||||
emacs -batch \
|
||||
--eval "(progn \
|
||||
(require 'package) \
|
||||
(push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives) \
|
||||
(package-initialize) \
|
||||
(unless package-archive-contents (package-refresh-contents)) \
|
||||
(unless (package-installed-p 'transient) (package-install 'transient)) \
|
||||
(unless (package-installed-p 'dash) (package-install 'dash)) \
|
||||
(unless (package-installed-p 'org) (package-install 'org)) \
|
||||
)" \
|
||||
-l ert \
|
||||
-l ./src/interface/emacs/khoj.el \
|
||||
-l ./src/interface/emacs/tests/khoj-tests.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
7
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
# Khoj artifacts
|
||||
*.gz
|
||||
*.pt
|
||||
src/.data
|
||||
tests/data/models
|
||||
tests/data/embeddings
|
||||
|
||||
@@ -11,12 +10,14 @@ __pycache__
|
||||
.emacs.desktop*
|
||||
*.py[cod]
|
||||
.vscode
|
||||
.env
|
||||
.venv/*
|
||||
|
||||
# Build artifacts
|
||||
/src/interface/web/images
|
||||
/src/khoj/interface/web/images
|
||||
/build/
|
||||
/dist/
|
||||
/khoj_assistant.egg-info/
|
||||
khoj_assistant.egg-info
|
||||
/config/khoj*.yml
|
||||
.pytest_cache
|
||||
khoj.log
|
||||
|
||||
13
.mypy.ini
@@ -1,13 +0,0 @@
|
||||
[mypy]
|
||||
strict_optional = False
|
||||
ignore_missing_imports = True
|
||||
install_types = True
|
||||
non_interactive = True
|
||||
show_error_codes = True
|
||||
exclude = (?x)(
|
||||
src/interface/desktop/main_window.py
|
||||
| src/interface/desktop/file_browser.py
|
||||
| src/interface/desktop/system_tray.py
|
||||
| build/*
|
||||
| tests/*
|
||||
)
|
||||
25
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
# Exclude elisp files to not clear page breaks
|
||||
exclude: \.el$
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
stages: [push, manual]
|
||||
pass_filenames: false
|
||||
args:
|
||||
- --config-file=pyproject.toml
|
||||
14
Dockerfile
@@ -1,18 +1,14 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM python:3.10-slim-bullseye
|
||||
LABEL org.opencontainers.image.source https://github.com/debanjum/khoj
|
||||
FROM ubuntu:kinetic
|
||||
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
|
||||
|
||||
# Install System Dependencies
|
||||
RUN apt-get update -y && \
|
||||
apt-get -y install python3-pyqt5
|
||||
|
||||
# Copy Application to Container
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN apt update -y && \
|
||||
apt -y install python3-pip python3-pyqt6 git
|
||||
|
||||
# Install Python Dependencies
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --upgrade .
|
||||
pip install git+https://github.com/khoj-ai/khoj.git
|
||||
|
||||
# Run the Application
|
||||
# There are more arguments required for the application to run,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Khoj
|
||||
Comment=A natural language search engine for your personal notes, transactions and images.
|
||||
Comment=An AI personal assistant for your Digital Brain
|
||||
Path=/opt
|
||||
Exec=/opt/Khoj
|
||||
Icon=Khoj
|
||||
Icon=Khoj
|
||||
|
||||
20
Khoj.spec
@@ -5,9 +5,12 @@ from PyInstaller.utils.hooks import copy_metadata
|
||||
import sysconfig
|
||||
|
||||
datas = [
|
||||
('src/interface/web', 'src/interface/web'),
|
||||
(f'{sysconfig.get_paths()["purelib"]}/transformers', 'transformers')
|
||||
('src/khoj/interface/web', 'khoj/interface/web'),
|
||||
(f'{sysconfig.get_paths()["purelib"]}/transformers', 'transformers'),
|
||||
(f'{sysconfig.get_paths()["purelib"]}/langchain', 'langchain'),
|
||||
(f'{sysconfig.get_paths()["purelib"]}/PIL', 'PIL')
|
||||
]
|
||||
datas += copy_metadata('torch')
|
||||
datas += copy_metadata('tqdm')
|
||||
datas += copy_metadata('regex')
|
||||
datas += copy_metadata('requests')
|
||||
@@ -15,15 +18,16 @@ datas += copy_metadata('packaging')
|
||||
datas += copy_metadata('filelock')
|
||||
datas += copy_metadata('numpy')
|
||||
datas += copy_metadata('tokenizers')
|
||||
datas += copy_metadata('pillow')
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['src/main.py'],
|
||||
['src/khoj/main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=['huggingface_hub.repository'],
|
||||
hiddenimports=['huggingface_hub.repository', 'PIL', 'PIL._tkinter_finder', 'tiktoken_ext', 'tiktoken_ext.openai_public'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
@@ -50,7 +54,7 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
if system() != 'Darwin':
|
||||
# Add Splash screen to show on app launch
|
||||
splash = Splash(
|
||||
'src/interface/web/assets/icons/favicon-144x144.png',
|
||||
'src/khoj/interface/web/assets/icons/favicon-128x128.png',
|
||||
binaries=a.binaries,
|
||||
datas=a.datas,
|
||||
text_pos=(10, 160),
|
||||
@@ -82,7 +86,7 @@ if system() != 'Darwin':
|
||||
target_arch='x86_64',
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='src/interface/web/assets/icons/favicon-144x144.ico',
|
||||
icon='src/khoj/interface/web/assets/icons/favicon-128x128.ico',
|
||||
)
|
||||
else:
|
||||
exe = EXE(
|
||||
@@ -105,11 +109,11 @@ else:
|
||||
target_arch='x86_64',
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='src/interface/web/assets/icons/favicon.icns',
|
||||
icon='src/khoj/interface/web/assets/icons/favicon.icns',
|
||||
)
|
||||
app = BUNDLE(
|
||||
exe,
|
||||
name='Khoj.app',
|
||||
icon='src/interface/web/assets/icons/favicon.icns',
|
||||
icon='src/khoj/interface/web/assets/icons/favicon.icns',
|
||||
bundle_identifier=None,
|
||||
)
|
||||
|
||||
1
LICENSE
@@ -619,4 +619,3 @@ Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
include Readme.md
|
||||
graft src/interface/*
|
||||
prune src/interface/web/images*
|
||||
prune docs*
|
||||
global-exclude .DS_Store *.py[cod]
|
||||
525
README.md
Normal file
@@ -0,0 +1,525 @@
|
||||
<h1><img src="src/khoj/interface/web/assets/icons/khoj-logo-sideways.svg" width="330" alt="Khoj Logo"></h1>
|
||||
|
||||
[](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/)
|
||||
|
||||
*An AI personal assistant for your digital brain*
|
||||
|
||||
**Supported Plugins**
|
||||
|
||||
[](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#readme)
|
||||
[](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#readme)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#Features)
|
||||
- [Demos](#Demos)
|
||||
- [Khoj in Obsidian](#khoj-in-obsidian)
|
||||
- [Khoj in Emacs, Browser](#khoj-in-emacs-browser)
|
||||
- [Interfaces](#Interfaces)
|
||||
- [Architecture](#Architecture)
|
||||
- [Setup](#Setup)
|
||||
- [Install](#1-Install)
|
||||
- [Run](#2-Run)
|
||||
- [Configure](#3-Configure)
|
||||
- [Install Plugins](#4-install-interface-plugins)
|
||||
- [Use](#Use)
|
||||
- [Khoj Search](#Khoj-search)
|
||||
- [Khoj Chat](#Khoj-chat)
|
||||
- [Upgrade](#Upgrade)
|
||||
- [Khoj Server](#upgrade-khoj-server)
|
||||
- [Khoj.el](#upgrade-khoj-on-emacs)
|
||||
- [Khoj Obsidian](#upgrade-khoj-on-obsidian)
|
||||
- [Uninstall](#uninstall)
|
||||
- [Troubleshoot](#Troubleshoot)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Access Khoj on Mobile](#access-khoj-on-mobile)
|
||||
- [Use OpenAI Models for Search](#use-openai-models-for-search)
|
||||
- [Search across Different Languages](#search-across-different-languages)
|
||||
- [Boostrap Khoj Search for Offline Usage Later](#bootstrap-khoj-search-for-offline-usage-later)
|
||||
- [Miscellaneous](#Miscellaneous)
|
||||
- [Setup OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
- [GPT API](#gpt-api)
|
||||
- [Performance](#Performance)
|
||||
- [Query Performance](#Query-performance)
|
||||
- [Indexing Performance](#Indexing-performance)
|
||||
- [Miscellaneous](#Miscellaneous-1)
|
||||
- [Development](#Development)
|
||||
- [Visualize Codebase](#visualize-codebase)
|
||||
- [Setup](#Setup)
|
||||
- [Using Pip](#Using-Pip)
|
||||
- [Using Docker](#Using-Docker)
|
||||
- [Using Conda](#Using-Conda)
|
||||
- [Validate](#Validate)
|
||||
- [Credits](#Credits)
|
||||
|
||||
## Features
|
||||
- **Search**
|
||||
- **Local**: Your personal data stays local. All search and indexing is done on your machine. *Unlike chat which requires access to GPT.*
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
- **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
|
||||
- **General**
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Pluggable**: Modular architecture makes it easy to plug in new data sources, frontends and ML models
|
||||
- **Multiple Sources**: Index your Org-mode and Markdown notes, PDF files, Github repositories, and Photos
|
||||
- **Multiple Interfaces**: Interact from your [Web Browser](./src/khoj/interface/web/index.html), [Emacs](./src/interface/emacs/khoj.el) or [Obsidian](./src/interface/obsidian/)
|
||||
|
||||
## Demos
|
||||
### Khoj in Obsidian
|
||||
https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b
|
||||
|
||||
<details><summary>Description</summary>
|
||||
|
||||
1. Install Khoj via `pip` and start Khoj backend in a terminal (Run `khoj`)
|
||||
```
|
||||
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)
|
||||
</details>
|
||||
|
||||
### Khoj in Emacs, Browser
|
||||
https://user-images.githubusercontent.com/6413477/184735169-92c78bf1-d827-4663-9087-a1ea194b8f4b.mp4
|
||||
|
||||
<details><summary>Description</summary>
|
||||
|
||||
- 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)
|
||||
</details>
|
||||
|
||||
<details><summary>Analysis</summary>
|
||||
|
||||
- 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
|
||||
</details>
|
||||
|
||||
### Interfaces
|
||||
|
||||

|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
These are the general setup instructions for 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.el Readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Setup) to setup Khoj with Emacs<br />
|
||||
Its simpler as it can skip the server *install*, *run* and *configure* step below.
|
||||
- Check the [Khoj Obsidian Readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#Setup) to setup Khoj with Obsidian<br />
|
||||
Its simpler as it can skip the *configure* step below.
|
||||
|
||||
### 1. Install
|
||||
Run the following command in your terminal to install the Khoj backend.
|
||||
|
||||
- On Linux/MacOS
|
||||
```shell
|
||||
python -m pip install khoj-assistant
|
||||
```
|
||||
|
||||
- On Windows
|
||||
```shell
|
||||
py -m pip install khoj-assistant
|
||||
```
|
||||
|
||||
### 2. Run
|
||||
|
||||
Run the following commmand from your terminal to start the Khoj backend and open Khoj in your browser.
|
||||
|
||||
```shell
|
||||
khoj --gui
|
||||
```
|
||||
|
||||
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`)
|
||||
|
||||
### 3. Configure
|
||||
1. Set `File`, `Folder` and hit `Save` in each Plugins you want to enable for Search on the Khoj config page
|
||||
2. Add your OpenAI API key to Chat Feature settings if you want to use Chat
|
||||
3. Click `Configure` and wait. The app will download ML models and index the content for search and (optionally) chat
|
||||
|
||||
### 4. Install Interface Plugins
|
||||
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.
|
||||
|
||||
- **Khoj Obsidian**:<br />
|
||||
[Install](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) the Khoj Obsidian plugin
|
||||
|
||||
- **Khoj Emacs**:<br />
|
||||
[Install](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#2-Install-Khojel) khoj.el
|
||||
|
||||
## Use
|
||||
### Khoj Search
|
||||
- **Khoj via Obsidian**
|
||||
- Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or Search for *Khoj: Search* in the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
|
||||
- **Khoj via Emacs**
|
||||
- Run `M-x khoj <user-query>`
|
||||
- **Khoj via Web**
|
||||
- Open <http://localhost:8000/> directly
|
||||
- **Khoj via API**
|
||||
- See the Khoj FastAPI [Swagger Docs](http://localhost:8000/docs), [ReDocs](http://localhost:8000/redocs)
|
||||
|
||||
<details><summary>Query Filters</summary>
|
||||
|
||||
Use structured query syntax to filter the natural language search results
|
||||
- **Word Filter**: Get entries that include/exclude a specified term
|
||||
- Entries that contain term_to_include: `+"term_to_include"`
|
||||
- Entries that contain term_to_exclude: `-"term_to_exclude"`
|
||||
- **Date Filter**: Get entries containing dates in YYYY-MM-DD format from specified date (range)
|
||||
- Entries from April 1st 1984: `dt:"1984-04-01"`
|
||||
- Entries after March 31st 1984: `dt>="1984-04-01"`
|
||||
- Entries before April 2nd 1984 : `dt<="1984-04-01"`
|
||||
- **File Filter**: Get entries from a specified file
|
||||
- Entries from incoming.org file: `file:"incoming.org"`
|
||||
- Combined Example
|
||||
- `what is the meaning of life? file:"1984.org" dt>="1984-01-01" dt<="1985-01-01" -"big" -"brother"`
|
||||
- Adds all filters to the natural language query. It should return entries
|
||||
- from the file *1984.org*
|
||||
- containing dates from the year *1984*
|
||||
- excluding words *"big"* and *"brother"*
|
||||
- that best match the natural language query *"what is the meaning of life?"*
|
||||
|
||||
</details>
|
||||
|
||||
### Khoj Chat
|
||||
#### Overview
|
||||
- Creates a personal assistant for you to inquire and engage with your notes
|
||||
- Uses [ChatGPT](https://openai.com/blog/chatgpt) and [Khoj search](#khoj-search)
|
||||
- Supports multi-turn conversations with the relevant notes for context
|
||||
- Shows reference notes used to generate a response
|
||||
- **Note**: *Your query and top notes from khoj search will be sent to OpenAI for processing*
|
||||
|
||||
#### Setup
|
||||
- [Setup your OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
|
||||
#### Use
|
||||
1. Open [/chat](http://localhost:8000/chat)[^2]
|
||||
2. Type your queries and see response by Khoj from your notes
|
||||
|
||||
#### Demo
|
||||

|
||||
|
||||
### Details
|
||||
1. Your query is used to retrieve the most relevant notes, if any, using Khoj search
|
||||
2. These notes, the last few messages and associated metadata is passed to ChatGPT along with your query for a response
|
||||
|
||||
## Upgrade
|
||||
### Upgrade Khoj Server
|
||||
```shell
|
||||
pip install --upgrade khoj-assistant
|
||||
```
|
||||
|
||||
*Note: To upgrade to the latest pre-release version of the khoj server run below command*
|
||||
```shell
|
||||
# Maps to the latest commit on the master branch
|
||||
pip install --upgrade --pre khoj-assistant
|
||||
```
|
||||
|
||||
### Upgrade Khoj on Emacs
|
||||
- Use your Emacs Package Manager to Upgrade
|
||||
- See [khoj.el readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade) for details
|
||||
|
||||
### Upgrade Khoj on Obsidian
|
||||
- Upgrade via the Community plugins tab on the settings pane in the Obsidian app
|
||||
- See the [khoj plugin readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) for details
|
||||
|
||||
## Uninstall
|
||||
1. (Optional) Hit `Ctrl-C` in the terminal running the khoj server to stop it
|
||||
2. Delete the khoj directory in your home folder (i.e `~/.khoj` on Linux, Mac or `C:\Users\<your-username>\.khoj` on Windows)
|
||||
3. Uninstall the khoj server with `pip uninstall khoj-assistant`
|
||||
4. (Optional) Uninstall khoj.el or the khoj obsidian plugin in the standard way on Emacs, Obsidian
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
#### Install fails while building Tokenizer dependency
|
||||
- **Details**: `pip install khoj-assistant` 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
|
||||
rustup-init
|
||||
source ~/.cargo/env
|
||||
```
|
||||
- **Refer**: [Issue with Fix](https://github.com/khoj-ai/khoj/issues/82#issuecomment-1241890946) for more details
|
||||
|
||||
#### Search starts giving wonky results
|
||||
- **Fix**: Open [/api/update?force=true](http://localhost:8000/api/update?force=true)[^2] in browser to regenerate index from scratch
|
||||
- **Note**: *This is a fix for when you percieve the search results have degraded. Not if you think they've always given wonky results*
|
||||
|
||||
#### 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)
|
||||
|
||||
#### Khoj errors out complaining about Tensors mismatch or null
|
||||
- **Mitigation**: Disable `image` search using the desktop GUI
|
||||
|
||||
## Advanced Usage
|
||||
### Access Khoj on Mobile
|
||||
1. [Setup Khoj](#Setup) on your personal server. This can be any always-on machine, i.e an old computer, RaspberryPi(?) etc
|
||||
2. [Install](https://tailscale.com/kb/installation/) [Tailscale](tailscale.com/) on your personal server and phone
|
||||
3. Open the Khoj web interface of the server from your phone browser.<br /> It should be `http://tailscale-ip-of-server:8000` or `http://name-of-server:8000` if you've setup [MagicDNS](https://tailscale.com/kb/1081/magicdns/)
|
||||
4. Click the [Add to Homescreen](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen) button
|
||||
5. Enjoy exploring your notes, documents and images from your phone!
|
||||
|
||||

|
||||
|
||||
### Use OpenAI Models for Search
|
||||
#### Setup
|
||||
1. Set `encoder-type`, `encoder` and `model-directory` under `asymmetric` and/or `symmetric` `search-type` in your `khoj.yml`[^1]:
|
||||
```diff
|
||||
asymmetric:
|
||||
- encoder: "sentence-transformers/multi-qa-MiniLM-L6-cos-v1"
|
||||
+ encoder: text-embedding-ada-002
|
||||
+ encoder-type: khoj.utils.models.OpenAI
|
||||
cross-encoder: "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
||||
- encoder-type: sentence_transformers.SentenceTransformer
|
||||
- model_directory: "~/.khoj/search/asymmetric/"
|
||||
+ model-directory: null
|
||||
```
|
||||
2. [Setup your OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
3. Restart Khoj server to generate embeddings. It will take longer than with offline models.
|
||||
|
||||
#### Warnings
|
||||
This configuration *uses an online model*
|
||||
- It will **send all notes to OpenAI** to generate embeddings
|
||||
- **All queries will be sent to OpenAI** when you search with Khoj
|
||||
- You will be **charged by OpenAI** based on the total tokens processed
|
||||
- It *requires an active internet connection* to search and index
|
||||
|
||||
### Search across Different Languages
|
||||
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 `search-type > asymmetric > encoder` to `paraphrase-multilingual-MiniLM-L12-v2` in your `~/.khoj/khoj.yml` file for now. See diff of `khoj.yml` below for illustration:
|
||||
```diff
|
||||
asymmetric:
|
||||
- encoder: "sentence-transformers/multi-qa-MiniLM-L6-cos-v1"
|
||||
+ encoder: "paraphrase-multilingual-MiniLM-L12-v2"
|
||||
cross-encoder: "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
||||
model_directory: "~/.khoj/search/asymmetric/"
|
||||
```
|
||||
|
||||
2. Regenerate your content index. For example, by opening [\<khoj-url\>/api/update?t=force](http://localhost:8000/api/update?t=force)
|
||||
|
||||
### Bootstrap Khoj Search for Offline Usage later
|
||||
|
||||
You can bootstrap Khoj pre-emptively to run on machines that do not have internet access. An example use-case would be to run Khoj on an air-gapped machine.
|
||||
Note: *Only search can currently run in fully offline mode, not chat.*
|
||||
|
||||
- With Internet
|
||||
1. Manually download the [asymmetric text](https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1), [symmetric text](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)and [image search](https://huggingface.co/sentence-transformers/clip-ViT-B-32) models from HuggingFace
|
||||
2. Pip install khoj (and dependencies) in an associated virtualenv. E.g `python -m venv .venv && source .venv/bin/activate && pip install khoj-assistant`
|
||||
- Without Internet
|
||||
1. Copy each of the search models into their respective folders, `asymmetric`, `symmetric` and `image` under the `~/.khoj/search/` directory on the air-gapped machine
|
||||
2. Copy the khoj virtual environment directory onto the air-gapped machine, activate the environment and start and khoj as normal. E.g `source .venv/bin/activate && khoj`
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
### Set your OpenAI API key in Khoj
|
||||
If you want, Khoj can be configured to use OpenAI for search and chat.<br />
|
||||
Add your OpenAI API to Khoj by using either of the two options below:
|
||||
- Open your [Khoj settings](http://localhost:8000/config/processor/conversation), add your OpenAI API key, and click *Save*. Then go to your [Khoj settings](http://localhost:8000/config) and click `Configure`. This will refresh Khoj with your OpenAI API key.
|
||||
- Set `openai-api-key` field under `processor.conversation` section in your `khoj.yml`[^1] to your [OpenAI API key](https://beta.openai.com/account/api-keys) and restart khoj:
|
||||
```diff
|
||||
processor:
|
||||
conversation:
|
||||
- openai-api-key: # "YOUR_OPENAI_API_KEY"
|
||||
+ openai-api-key: sk-aaaaaaaaaaaaaaaaaaaaaaaahhhhhhhhhhhhhhhhhhhhhhhh
|
||||
model: "text-davinci-003"
|
||||
conversation-logfile: "~/.khoj/processor/conversation/conversation_logs.json"
|
||||
```
|
||||
|
||||
**Warning**: *This will enable Khoj to send your query and note(s) to OpenAI for processing*
|
||||
|
||||
### GPT API
|
||||
- The [chat](http://localhost:8000/api/chat), [answer](http://localhost:8000/api/beta/answer) and [search](http://localhost:8000/api/beta/search) API endpoints use [OpenAI API](https://openai.com/api/)
|
||||
- They are disabled by default
|
||||
- To use them:
|
||||
1. [Setup your OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
2. Interact with them from the [Khoj Swagger docs](http://locahost:8000/docs)[^2]
|
||||
|
||||
### Index Github Repository for Search, Chat
|
||||
The Khoj Github plugin can index issues, commit messages and markdown, org-mode and PDF files from any repositories you have access to. This allows you to chat or search with these repositories. Get answers, resolve issues or just explore a repo with the help of your AI personal assistant.
|
||||
|
||||
See the [Khoj FAQ](https://faq.khoj.dev) for a demo of Khoj search and chat. It makes the Khoj github repo available for exploring.
|
||||
|
||||
Note: *Khoj will ignore code files in the repository for now as the default AI model used works best with natural language text, not code.*
|
||||
|
||||
#### Setup Khoj Github plugin
|
||||
1. Get a [pat token](https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token) with `repo` and `read:org` scopes in the classic flow.
|
||||
2. Configure Khoj settings to include the `owner` and `repo_name`. The `owner` will be the organization name if the repo is in an organization. The `repo_name` will be the name of the repository. Optionally, you can also supply a branch name. If no branch name is supplied, the `master` branch will be used.
|
||||
|
||||
## Performance
|
||||
|
||||
### Query performance
|
||||
|
||||
- Semantic search using the bi-encoder is fairly fast at \<50 ms
|
||||
- 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
|
||||
|
||||
### Indexing performance
|
||||
|
||||
- Indexing is more strongly impacted by the size of the source data
|
||||
- Indexing 100K+ line corpus of notes takes about 10 minutes
|
||||
- Indexing 4000+ images takes about 15 minutes and more than 8Gb of RAM
|
||||
- Note: *It should only take this long on the first run* as the index is incrementally updated
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Testing done on a Mac M1 and a \>100K line corpus of notes
|
||||
- Search, indexing on a GPU has not been tested yet
|
||||
|
||||
## Development
|
||||
### Visualize Codebase
|
||||
|
||||
*[Interactive Visualization](https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=debanjum%2Fkhoj)*
|
||||
|
||||

|
||||
|
||||
### Setup
|
||||
#### Using Pip
|
||||
##### 1. Install
|
||||
|
||||
```shell
|
||||
# Get Khoj Code
|
||||
git clone https://github.com/khoj-ai/khoj && cd khoj
|
||||
|
||||
# Create, Activate Virtual Environment
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
|
||||
# Install Khoj for Development
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
##### 2. Run
|
||||
1. Start Khoj
|
||||
```shell
|
||||
khoj -vv
|
||||
```
|
||||
2. Configure Khoj
|
||||
- **Via the Settings UI**: Add files, directories to index the [Khoj settings](http://localhost:8000/config) UI once Khoj has started up. Once you've saved all your settings, click `Configure`.
|
||||
- **Manually**:
|
||||
- Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml`
|
||||
- Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml`
|
||||
- Set `input-directories` field in `image` `content-type` section
|
||||
- Delete `content-type` and `processor` sub-section(s) irrelevant for your use-case
|
||||
- Restart khoj
|
||||
|
||||
Note: Wait after configuration for khoj to Load ML model, generate embeddings and expose API to query notes, images, documents etc specified in config YAML
|
||||
|
||||
#### Using Docker
|
||||
##### 1. Clone
|
||||
|
||||
```shell
|
||||
git clone https://github.com/khoj-ai/khoj && cd khoj
|
||||
```
|
||||
|
||||
##### 2. Configure
|
||||
|
||||
- **Required**: Update [docker-compose.yml](./docker-compose.yml) to mount your images, (org-mode or markdown) notes, PDFs and Github repositories
|
||||
- **Optional**: Edit application configuration in [khoj_docker.yml](./config/khoj_docker.yml)
|
||||
|
||||
##### 3. Run
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
*Note: The first run will take time. Let it run, it\'s mostly not hung, just generating embeddings*
|
||||
|
||||
##### 4. Upgrade
|
||||
|
||||
```shell
|
||||
docker-compose build --pull
|
||||
```
|
||||
|
||||
#### Using Conda
|
||||
##### 1. Install Dependencies
|
||||
- [Install Conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
||||
|
||||
##### 2. Install Khoj
|
||||
```shell
|
||||
git clone https://github.com/khoj-ai/khoj && cd khoj
|
||||
conda env create -f config/environment.yml
|
||||
conda activate khoj
|
||||
python3 -m pip install pyqt6 # As conda does not support pyqt6 yet
|
||||
```
|
||||
|
||||
##### 3. Configure
|
||||
- Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml`
|
||||
- Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml`
|
||||
- Set `input-directories` field in `image` `content-type` section
|
||||
- Delete `content-type`, `processor` sub-sections irrelevant for your use-case
|
||||
|
||||
##### 4. Run
|
||||
```shell
|
||||
python3 -m src.khoj.main -vv
|
||||
```
|
||||
Load ML model, generate embeddings and expose API to query notes, images, documents etc specified in config YAML
|
||||
|
||||
##### 5. Upgrade
|
||||
```shell
|
||||
cd khoj
|
||||
git pull origin master
|
||||
conda deactivate khoj
|
||||
conda env update -f config/environment.yml
|
||||
conda activate khoj
|
||||
```
|
||||
|
||||
### Validate
|
||||
#### Before Make Changes
|
||||
1. Install Git Hooks for Validation
|
||||
```shell
|
||||
pre-commit install -t pre-push -t pre-commit
|
||||
```
|
||||
- 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`
|
||||
- Note 2: To run the pre-commit changes manually, use `pre-commit run --hook-stage manual --all` before creating PR
|
||||
|
||||
#### Before Creating PR
|
||||
|
||||
1. Run Tests. If you get an error complaining about a missing `fast_tokenizer_file`, follow the solution [in this Github issue](https://github.com/UKPLab/sentence-transformers/issues/1659).
|
||||
```shell
|
||||
pytest
|
||||
```
|
||||
|
||||
2. Run MyPy to check types
|
||||
```shell
|
||||
mypy --config-file pyproject.toml
|
||||
```
|
||||
|
||||
#### After Creating PR
|
||||
- Automated [validation workflows](.github/workflows) run for every PR.
|
||||
|
||||
Ensure any issues seen by them our fixed
|
||||
|
||||
- Test the python packge created for a PR
|
||||
1. Download and extract the zipped `.whl` artifact generated from the pypi workflow run for the PR.
|
||||
2. Install (in your virtualenv) with `pip install /path/to/download*.whl>`
|
||||
3. Start and use the application to see if it works fine
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
- [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)
|
||||
- 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
|
||||
|
||||
|
||||
[^1]: Default Khoj config file @ `~/.khoj/khoj.yml`
|
||||
|
||||
[^2]: Default Khoj url @ http://localhost:8000
|
||||
414
Readme.md
@@ -1,414 +0,0 @@
|
||||
# Khoj 🦅
|
||||
[](https://github.com/debanjum/khoj/actions/workflows/build.yml)
|
||||
[](https://github.com/debanjum/khoj/actions/workflows/test.yml)
|
||||
[](https://github.com/debanjum/khoj/actions/workflows/publish.yml)
|
||||
|
||||
*A natural language search engine for your personal notes, transactions and images*
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#Features)
|
||||
- [Demos](#Demos)
|
||||
- [Khoj in Obsidian](#khoj-in-obsidian)
|
||||
- [Khoj in Emacs, Browser](#khoj-in-emacs-browser)
|
||||
- [Interfaces](#Interfaces)
|
||||
- [Architecture](#Architecture)
|
||||
- [Setup](#Setup)
|
||||
- [Install](#1-Install)
|
||||
- [Configure](#2-Configure)
|
||||
- [Run](#3-Run)
|
||||
- [Use](#Use)
|
||||
- [Interfaces](#Interfaces-1)
|
||||
- [Query Filters](#Query-filters)
|
||||
- [Upgrade](#Upgrade)
|
||||
- [Khoj Server](#upgrade-khoj-server)
|
||||
- [Khoj.el](#upgrade-khoj-on-emacs)
|
||||
- [Khoj Obsidian](#upgrade-khoj-on-obsidian)
|
||||
- [Troubleshoot](#Troubleshoot)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Access Khoj on Mobile](#access-khoj-on-mobile)
|
||||
- [Chat with Notes](#chat-with-notes)
|
||||
- [Use OpenAI Models for Search](#use-openai-models-for-search)
|
||||
- [Miscellaneous](#Miscellaneous)
|
||||
- [Setup OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
- [Beta API](#beta-api)
|
||||
- [Performance](#Performance)
|
||||
- [Query Performance](#Query-performance)
|
||||
- [Indexing Performance](#Indexing-performance)
|
||||
- [Miscellaneous](#Miscellaneous-1)
|
||||
- [Development](#Development)
|
||||
- [Visualize Codebase](#visualize-codebase)
|
||||
- [Setup](#Setup)
|
||||
- [Using Pip](#Using-Pip)
|
||||
- [Using Docker](#Using-Docker)
|
||||
- [Using Conda](#Test)
|
||||
- [Test](#Test)
|
||||
- [Credits](#Credits)
|
||||
|
||||
## Features
|
||||
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Local**: Your personal data stays local. All search, indexing is done on your machine[\*](https://github.com/debanjum/khoj#beta-api)
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
- **Pluggable**: Modular architecture makes it easy to plug in new data sources, frontends and ML models
|
||||
- **Multiple Sources**: Search your Org-mode and Markdown notes, Beancount transactions and Photos
|
||||
- **Multiple Interfaces**: Search using a [Web Browser](./src/interface/web/index.html), [Emacs](./src/interface/emacs/khoj.el) or the [API](http://localhost:8000/docs)
|
||||
|
||||
## Demos
|
||||
### Khoj in Obsidian
|
||||
https://user-images.githubusercontent.com/6413477/210486007-36ee3407-e6aa-4185-8a26-b0bfc0a4344f.mp4
|
||||
|
||||
<details><summary>Description</summary>
|
||||
|
||||
- Install Khoj via `pip` and start Khoj backend in non-gui mode
|
||||
- Install Khoj plugin via Community Plugins settings pane on Obsidian app
|
||||
- Check the new Khoj plugin settings
|
||||
- Let Khoj backend index the 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)
|
||||
</details>
|
||||
|
||||
### Khoj in Emacs, Browser
|
||||
https://user-images.githubusercontent.com/6413477/184735169-92c78bf1-d827-4663-9087-a1ea194b8f4b.mp4
|
||||
|
||||
<details><summary>Description</summary>
|
||||
|
||||
- Install Khoj via pip
|
||||
- Start Khoj app
|
||||
- Add this readme and [khoj.el readme](https://github.com/debanjum/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/debanjum/khoj/tree/master/src/interface/emacs#2-Install-Khojel)
|
||||
</details>
|
||||
|
||||
<details><summary>Analysis</summary>
|
||||
|
||||
- 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
|
||||
</details>
|
||||
|
||||
### Interfaces
|
||||
|
||||

|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
These are the general setup instructions for Khoj.
|
||||
|
||||
- Check the [Khoj.el Readme](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Setup) to setup Khoj with Emacs
|
||||
- Check the [Khoj Obsidian Readme](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#Setup) to setup Khoj with Obsidian<br />
|
||||
Its simpler as it can skip the configure step below.
|
||||
|
||||
### 1. Install
|
||||
|
||||
```shell
|
||||
pip install khoj-assistant
|
||||
```
|
||||
|
||||
### 2. Start App
|
||||
|
||||
```shell
|
||||
khoj
|
||||
```
|
||||
|
||||
### 3. Configure
|
||||
|
||||
1. Enable content types and point to files to search in the First Run Screen that pops up on app start
|
||||
2. Click `Configure` and wait. The app will download ML models and index the content for search
|
||||
|
||||
## Use
|
||||
### Interfaces
|
||||
|
||||
- **Khoj via Obsidian**
|
||||
- [Install](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) the Khoj Obsidian plugin
|
||||
- Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or Search for *Khoj: Search* in the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
|
||||
- **Khoj via Emacs**
|
||||
- [Install](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#installation) [khoj.el](./src/interface/emacs/khoj.el)
|
||||
- Run `M-x khoj <user-query>`
|
||||
- **Khoj via Web**
|
||||
- Open <http://localhost:8000/> via desktop interface or directly
|
||||
- **Khoj via API**
|
||||
- See the Khoj FastAPI [Swagger Docs](http://localhost:8000/docs), [ReDocs](http://localhost:8000/redocs)
|
||||
|
||||
### Query Filters
|
||||
Use structured query syntax to filter the natural language search results
|
||||
- **Word Filter**: Get entries that include/exclude a specified term
|
||||
- Entries that contain term_to_include: `+"term_to_include"`
|
||||
- Entries that contain term_to_exclude: `-"term_to_exclude"`
|
||||
- **Date Filter**: Get entries containing dates in YYYY-MM-DD format from specified date (range)
|
||||
- Entries from April 1st 1984: `dt:"1984-04-01"`
|
||||
- Entries after March 31st 1984: `dt>="1984-04-01"`
|
||||
- Entries before April 2nd 1984 : `dt<="1984-04-01"`
|
||||
- **File Filter**: Get entries from a specified file
|
||||
- Entries from incoming.org file: `file:"incoming.org"`
|
||||
- Combined Example
|
||||
- `what is the meaning of life? file:"1984.org" dt>="1984-01-01" dt<="1985-01-01" -"big" -"brother"`
|
||||
- Adds all filters to the natural language query. It should return entries
|
||||
- from the file *1984.org*
|
||||
- containing dates from the year *1984*
|
||||
- excluding words *"big"* and *"brother"*
|
||||
- that best match the natural language query *"what is the meaning of life?"*
|
||||
|
||||
## Upgrade
|
||||
### Upgrade Khoj Server
|
||||
```shell
|
||||
pip install --upgrade khoj-assistant
|
||||
```
|
||||
|
||||
### Upgrade Khoj on Emacs
|
||||
- Use your Emacs Package Manager to Upgrade
|
||||
- See [khoj.el readme](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade) for details
|
||||
|
||||
### Upgrade Khoj on Obsidian
|
||||
- Upgrade via the Community plugins tab on the settings pane in the Obsidian app
|
||||
- See the [khoj plugin readme](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) for details
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
#### Install fails while building Tokenizer dependency
|
||||
- **Details**: `pip install khoj-assistant` 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
|
||||
rustup-init
|
||||
source ~/.cargo/env
|
||||
```
|
||||
- **Refer**: [Issue with Fix](https://github.com/debanjum/khoj/issues/82#issuecomment-1241890946) for more details
|
||||
|
||||
#### Search starts giving wonky results
|
||||
- **Fix**: Open [/api/update?force=true](http://localhost:8000/api/update?force=true)[^2] in browser to regenerate index from scratch
|
||||
- **Note**: *This is a fix for when you percieve the search results have degraded. Not if you think they've always given wonky results*
|
||||
|
||||
#### 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)
|
||||
|
||||
#### Khoj errors out complaining about Tensors mismatch or null
|
||||
- **Mitigation**: Disable `image` search using the desktop GUI
|
||||
|
||||
## Advanced Usage
|
||||
### Access Khoj on Mobile
|
||||
1. [Setup Khoj](#Setup) on your personal server. This can be any always-on machine, i.e an old computer, RaspberryPi(?) etc
|
||||
2. [Install](https://tailscale.com/kb/installation/) [Tailscale](tailscale.com/) on your personal server and phone
|
||||
3. Open the Khoj web interface of the server from your phone browser.<br /> It should be `http://tailscale-ip-of-server:8000` or `http://name-of-server:8000` if you've setup [MagicDNS](https://tailscale.com/kb/1081/magicdns/)
|
||||
4. Click the [Add to Homescreen](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen) button
|
||||
5. Enjoy exploring your notes, transactions and images from your phone!
|
||||
|
||||

|
||||
|
||||
### Chat with Notes
|
||||
#### Overview
|
||||
- Provides a chat interface to inquire and engage with your notes
|
||||
- Chat Types:
|
||||
- **Summarize**: Pulls the most relevant note from your notes and summarizes it
|
||||
- **Chat**: Also does general chat. It guesses whether to give a general response or search, summarizes from your note. <br />
|
||||
E.g *"how was your day?"* will give a general response. But *When did I go surfing?* should give a response from your notes
|
||||
- **Note**: *Your query and top note from search result will be sent to OpenAI for processing*
|
||||
|
||||
#### Use
|
||||
1. [Setup your OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
2. Open [/chat?type=summarize](http://localhost:8000/chat?type=summarize)[^2]
|
||||
3. Type your queries, see summarized response by Khoj from your notes
|
||||
|
||||
#### Demo
|
||||

|
||||
|
||||
### Use OpenAI Models for Search
|
||||
#### Setup
|
||||
1. Set `encoder-type`, `encoder` and `model-directory` under `asymmetric` and/or `symmetric` `search-type` in your `khoj.yml`[^1]:
|
||||
```diff
|
||||
asymmetric:
|
||||
- encoder: "sentence-transformers/multi-qa-MiniLM-L6-cos-v1"
|
||||
+ encoder: text-embedding-ada-002
|
||||
+ encoder-type: src.utils.models.OpenAI
|
||||
cross-encoder: "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
||||
- encoder-type: sentence_transformers.SentenceTransformer
|
||||
- model_directory: "~/.khoj/search/asymmetric/"
|
||||
+ model-directory: null
|
||||
```
|
||||
2. [Setup your OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
3. Restart Khoj server to generate embeddings. It will take longer than with offline models.
|
||||
|
||||
#### Warnings
|
||||
This configuration *uses an online model*
|
||||
- It will **send all notes to OpenAI** to generate embeddings
|
||||
- **All queries will be sent to OpenAI** when you search with Khoj
|
||||
- You will be **charged by OpenAI** based on the total tokens processed
|
||||
- It *requires an active internet connection* to search and index
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
### Set your OpenAI API key in Khoj
|
||||
If you want, Khoj can be configured to use OpenAI for search and chat.<br />
|
||||
Add your OpenAI API to Khoj by using either of the two options below:
|
||||
- Open the Khoj desktop GUI, add your [OpenAI API key](https://beta.openai.com/account/api-keys) and click *Configure*
|
||||
Ensure khoj is started without the `--no-gui` flag. Check your system tray to see if Khoj 🦅 is minimized there.
|
||||
- Set `openai-api-key` field under `processor.conversation` section in your `khoj.yml`[^1] to your [OpenAI API key](https://beta.openai.com/account/api-keys) and restart khoj:
|
||||
```diff
|
||||
processor:
|
||||
conversation:
|
||||
- openai-api-key: # "YOUR_OPENAI_API_KEY"
|
||||
+ openai-api-key: sk-aaaaaaaaaaaaaaaaaaaaaaaahhhhhhhhhhhhhhhhhhhhhhhh
|
||||
model: "text-davinci-003"
|
||||
conversation-logfile: "~/.khoj/processor/conversation/conversation_logs.json"
|
||||
```
|
||||
|
||||
**Warning**: *This will enable khoj to send your query and note(s) to OpenAI for processing*
|
||||
|
||||
### Beta API
|
||||
- The beta [chat](http://localhost:8000/api/beta/chat), [summarize](http://localhost:8000/api/beta/summarize) and [search](http://localhost:8000/api/beta/search) API endpoints use [OpenAI API](https://openai.com/api/)
|
||||
- They are disabled by default
|
||||
- To use them:
|
||||
1. [Setup your OpenAI API key in Khoj](#set-your-openai-api-key-in-khoj)
|
||||
2. Interact with them from the [Khoj Swagger docs](http://locahost:8000/docs)[^2]
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
### Query performance
|
||||
|
||||
- Semantic search using the bi-encoder is fairly fast at \<50 ms
|
||||
- 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
|
||||
|
||||
### Indexing performance
|
||||
|
||||
- Indexing is more strongly impacted by the size of the source data
|
||||
- Indexing 100K+ line corpus of notes takes about 10 minutes
|
||||
- Indexing 4000+ images takes about 15 minutes and more than 8Gb of RAM
|
||||
- Note: *It should only take this long on the first run* as the index is incrementally updated
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Testing done on a Mac M1 and a \>100K line corpus of notes
|
||||
- Search, indexing on a GPU has not been tested yet
|
||||
|
||||
## Development
|
||||
### Visualize Codebase
|
||||
|
||||
*[Interactive Visualization](https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=debanjum%2Fkhoj)*
|
||||
|
||||

|
||||
|
||||
### Setup
|
||||
#### Using Pip
|
||||
##### 1. Install
|
||||
|
||||
```shell
|
||||
git clone https://github.com/debanjum/khoj && cd khoj
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
##### 2. Configure
|
||||
|
||||
- Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml`
|
||||
- Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml`
|
||||
- Set `input-directories` field in `image` `content-type` section
|
||||
- Delete `content-type` and `processor` sub-section(s) irrelevant for your use-case
|
||||
|
||||
##### 3. Run
|
||||
|
||||
```shell
|
||||
khoj -vv
|
||||
```
|
||||
Load ML model, generate embeddings and expose API to query notes, images, transactions etc specified in config YAML
|
||||
|
||||
##### 4. Upgrade
|
||||
|
||||
```shell
|
||||
# To Upgrade To Latest Stable Release
|
||||
# Maps to the latest tagged version of khoj on master branch
|
||||
pip install --upgrade khoj-assistant
|
||||
|
||||
# To Upgrade To Latest Pre-Release
|
||||
# Maps to the latest commit on the master branch
|
||||
pip install --upgrade --pre khoj-assistant
|
||||
|
||||
# To Upgrade To Specific Development Release.
|
||||
# Useful to test, review a PR.
|
||||
# Note: khoj-assistant is published to test PyPi on creating a PR
|
||||
pip install -i https://test.pypi.org/simple/ khoj-assistant==0.1.5.dev57166025766
|
||||
```
|
||||
|
||||
#### Using Docker
|
||||
##### 1. Clone
|
||||
|
||||
```shell
|
||||
git clone https://github.com/debanjum/khoj && cd khoj
|
||||
```
|
||||
|
||||
##### 2. Configure
|
||||
|
||||
- **Required**: Update [docker-compose.yml](./docker-compose.yml) to mount your images, (org-mode or markdown) notes and beancount directories
|
||||
- **Optional**: Edit application configuration in [khoj_docker.yml](./config/khoj_docker.yml)
|
||||
|
||||
##### 3. Run
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
*Note: The first run will take time. Let it run, it\'s mostly not hung, just generating embeddings*
|
||||
|
||||
##### 4. Upgrade
|
||||
|
||||
```shell
|
||||
docker-compose build --pull
|
||||
```
|
||||
|
||||
#### Using Conda
|
||||
##### 1. Install Dependencies
|
||||
- [Install Conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html)
|
||||
|
||||
##### 2. Install Khoj
|
||||
```shell
|
||||
git clone https://github.com/debanjum/khoj && cd khoj
|
||||
conda env create -f config/environment.yml
|
||||
conda activate khoj
|
||||
python3 -m pip install pyqt6 # As conda does not support pyqt6 yet
|
||||
```
|
||||
|
||||
##### 3. Configure
|
||||
- Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml`
|
||||
- Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml`
|
||||
- Set `input-directories` field in `image` `content-type` section
|
||||
- Delete `content-type`, `processor` sub-sections irrelevant for your use-case
|
||||
|
||||
##### 4. Run
|
||||
```shell
|
||||
python3 -m src.main -vv
|
||||
```
|
||||
Load ML model, generate embeddings and expose API to query notes, images, transactions etc specified in config YAML
|
||||
|
||||
##### 5. Upgrade
|
||||
```shell
|
||||
cd khoj
|
||||
git pull origin master
|
||||
conda deactivate khoj
|
||||
conda env update -f config/environment.yml
|
||||
conda activate khoj
|
||||
```
|
||||
|
||||
### Test
|
||||
```shell
|
||||
pytest
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- [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)
|
||||
- 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
|
||||
|
||||
|
||||
[^1]: Default Khoj config file @ `~/.khoj/khoj.yml`
|
||||
|
||||
[^2]: Default Khoj url @ http://localhost:8000
|
||||
@@ -19,4 +19,4 @@ dependencies:
|
||||
- aiofiles=0.8.0
|
||||
- huggingface_hub=0.8.1
|
||||
- dateparser=1.1.1
|
||||
- schedule=1.1.0
|
||||
- schedule=1.1.0
|
||||
|
||||
@@ -4,20 +4,20 @@ content-type:
|
||||
# If changing, the docker-compose volumes should also be changed to match.
|
||||
org:
|
||||
input-files: null
|
||||
input-filter: ["/data/org/*.org"]
|
||||
input-filter: ["/data/org/**/*.org"]
|
||||
compressed-jsonl: "/data/embeddings/notes.jsonl.gz"
|
||||
embeddings-file: "/data/embeddings/note_embeddings.pt"
|
||||
index_heading_entries: false
|
||||
|
||||
markdown:
|
||||
input-files: null
|
||||
input-filter: ["/data/markdown/*.md"]
|
||||
input-filter: ["/data/markdown/**/*.markdown"]
|
||||
compressed-jsonl: "/data/embeddings/markdown.jsonl.gz"
|
||||
embeddings-file: "/data/embeddings/markdown_embeddings.pt"
|
||||
|
||||
ledger:
|
||||
input-files: null
|
||||
input-filter: ["/data/ledger/*.beancount"]
|
||||
input-filter: ["/data/ledger/**/*.beancount"]
|
||||
compressed-jsonl: /data/embeddings/transactions.jsonl.gz
|
||||
embeddings-file: /data/embeddings/transaction_embeddings.pt
|
||||
|
||||
@@ -52,4 +52,4 @@ processor:
|
||||
#conversation:
|
||||
# openai-api-key: null
|
||||
# model: "text-davinci-003"
|
||||
# conversation-logfile: "/data/embeddings/conversation_logs.json"
|
||||
# conversation-logfile: "/data/embeddings/conversation_logs.json"
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
server:
|
||||
image: ghcr.io/debanjum/khoj:latest
|
||||
image: ghcr.io/khoj-ai/khoj:latest
|
||||
ports:
|
||||
# If changing the local port (left hand side), no other changes required.
|
||||
# If changing the remote port (right hand side),
|
||||
# change the port in the args in the build section,
|
||||
# If changing the remote port (right hand side),
|
||||
# change the port in the args in the build section,
|
||||
# as well as the port in the command section to match
|
||||
- "8000:8000"
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
# These mounted volumes hold the raw data that should be indexed for search.
|
||||
# These mounted volumes hold the raw data that should be indexed for search.
|
||||
# The path in your local directory (left hand side)
|
||||
# points to the files you want to index.
|
||||
# The path of the mounted directory (right hand side),
|
||||
# must match the path prefix in your config file.
|
||||
- ./tests/data/org/:/data/org/
|
||||
- ./tests/data/images/:/data/images/
|
||||
- ./tests/data/ledger/:/data/ledger/
|
||||
- ./tests/data/music/:/data/music/
|
||||
- ./tests/data/markdown/:/data/markdown/
|
||||
- ./tests/data/pdf/:/data/pdf/
|
||||
# Embeddings and models are populated after the first run
|
||||
# You can set these volumes to point to empty directories on host
|
||||
- ./tests/data/embeddings/:/data/embeddings/
|
||||
- ./tests/data/models/:/data/models/
|
||||
# Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/
|
||||
command: --no-gui --host="0.0.0.0" --port=8000 -c=config/khoj_docker.yml -vv
|
||||
command: --host="0.0.0.0" --port=8000 -c=config/khoj_docker.yml -vv
|
||||
|
||||
BIN
docs/khoj_chat_on_emacs_0.5.0.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "0.2.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Natural, Incremental Search for your Second Brain 🦅",
|
||||
"author": "Debanjum Singh Solanky",
|
||||
"authorUrl": "https://github.com/debanjum",
|
||||
"isDesktopOnly": false
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "0.8.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "An AI Personal Assistant for your Digital Brain",
|
||||
"author": "Debanjum Singh Solanky",
|
||||
"authorUrl": "https://github.com/debanjum",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
114
pyproject.toml
Normal file
@@ -0,0 +1,114 @@
|
||||
[build-system]
|
||||
requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "khoj-assistant"
|
||||
description = "An AI personal assistant for your Digital Brain"
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0-or-later"
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "Debanjum Singh Solanky, Saba Imran" },
|
||||
]
|
||||
keywords = [
|
||||
"search",
|
||||
"semantic-search",
|
||||
"productivity",
|
||||
"NLP",
|
||||
"AI",
|
||||
"org-mode",
|
||||
"markdown",
|
||||
"images",
|
||||
"pdf",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
"Topic :: Scientific/Engineering :: Human Machine Interfaces",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
]
|
||||
dependencies = [
|
||||
"dateparser == 1.1.1",
|
||||
"defusedxml == 0.7.1",
|
||||
"fastapi == 0.77.1",
|
||||
"jinja2 == 3.1.2",
|
||||
"openai >= 0.27.0",
|
||||
"tiktoken >= 0.3.0",
|
||||
"tenacity >= 8.2.2",
|
||||
"pillow == 9.3.0",
|
||||
"pydantic >= 1.10.10",
|
||||
"pyqt6 == 6.3.1",
|
||||
"pyyaml == 6.0",
|
||||
"rich >= 13.3.1",
|
||||
"schedule == 1.1.0",
|
||||
"sentence-transformers == 2.2.2",
|
||||
"torch >= 2.0.1",
|
||||
"uvicorn == 0.17.6",
|
||||
"aiohttp == 3.8.4",
|
||||
"langchain >= 0.0.187",
|
||||
"pypdf >= 3.9.0",
|
||||
"requests >= 2.26.0",
|
||||
"bs4 >= 0.0.1",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/khoj-ai/khoj#readme"
|
||||
Issues = "https://github.com/khoj-ai/khoj/issues"
|
||||
Discussions = "https://github.com/khoj-ai/khoj/discussions"
|
||||
Releases = "https://github.com/khoj-ai/khoj/releases"
|
||||
|
||||
[project.scripts]
|
||||
khoj = "khoj.main:run"
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest >= 7.1.2",
|
||||
"freezegun >= 1.2.0",
|
||||
"factory-boy >= 3.2.1",
|
||||
"trio >= 0.22.0",
|
||||
]
|
||||
dev = [
|
||||
"khoj-assistant[test]",
|
||||
"mypy >= 1.0.1",
|
||||
"black >= 23.1.0",
|
||||
"pre-commit >= 3.0.4",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
raw-options.local_scheme = "no-local-version" # PEP440 compliant version for PyPi
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["src/khoj"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/khoj"]
|
||||
|
||||
[tool.mypy]
|
||||
files = "src/khoj"
|
||||
pretty = true
|
||||
strict_optional = false
|
||||
install_types = true
|
||||
ignore_missing_imports = true
|
||||
non_interactive = true
|
||||
show_error_codes = true
|
||||
warn_unused_ignores = false
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--strict-markers"
|
||||
markers = [
|
||||
"chatquality: Evaluate chatbot capabilities and quality",
|
||||
]
|
||||
82
scripts/bump_version.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/zsh
|
||||
|
||||
project_root=$PWD
|
||||
|
||||
while getopts 'nc:' opt;
|
||||
do
|
||||
case "${opt}" in
|
||||
c)
|
||||
# Get current project version
|
||||
current_version=$OPTARG
|
||||
|
||||
# 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
|
||||
cp $project_root/versions.json .
|
||||
npm run version # append current version
|
||||
rm *.bak
|
||||
|
||||
# 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/obsidian/package.json \
|
||||
$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
|
||||
;;
|
||||
n)
|
||||
# Induce hatch to compute next version number
|
||||
# remove .dev[commits-since-tag] version suffix from hatch computed version number
|
||||
next_version=$(touch bump.txt && git add bump.txt && hatch version | sed 's/\.dev.*//g')
|
||||
git rm --cached -- bump.txt && rm bump.txt
|
||||
|
||||
# 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
|
||||
|
||||
# Bump Emacs package to next version
|
||||
cd $project_root/src/interface/emacs
|
||||
sed -E -i.bak "s/^;; Version: (.*)/;; Version: $next_version/" khoj.el
|
||||
rm *.bak
|
||||
|
||||
# Run pre-commit validations to fix jsons
|
||||
pre-commit run --hook-stage manual --all
|
||||
|
||||
# Commit changes
|
||||
git add \
|
||||
$project_root/src/interface/obsidian/package.json \
|
||||
$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]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Restore State
|
||||
cd $project_root
|
||||
57
setup.py
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
from pathlib import Path
|
||||
this_directory = Path(__file__).parent
|
||||
|
||||
setup(
|
||||
name='khoj-assistant',
|
||||
version='0.2.5',
|
||||
description="A natural language search engine for your personal notes, transactions and images",
|
||||
long_description=(this_directory / "Readme.md").read_text(encoding="utf-8"),
|
||||
long_description_content_type="text/markdown",
|
||||
author='Debanjum Singh Solanky, Saba Imran',
|
||||
author_email='debanjum+pypi@gmail.com, narmiabas@gmail.com',
|
||||
url='https://github.com/debanjum/khoj',
|
||||
license="GPLv3",
|
||||
keywords="search semantic-search productivity NLP org-mode markdown beancount images",
|
||||
python_requires=">=3.8, <4",
|
||||
packages=find_packages(
|
||||
where=".",
|
||||
exclude=["tests*"],
|
||||
include=["src*"]
|
||||
),
|
||||
install_requires=[
|
||||
"numpy == 1.22.4",
|
||||
"torch == 1.13.1",
|
||||
"torchvision == 0.14.1",
|
||||
"transformers == 4.21.0",
|
||||
"sentence-transformers == 2.1.0",
|
||||
"openai == 0.20.0",
|
||||
"huggingface_hub == 0.8.1",
|
||||
"pydantic == 1.9.1",
|
||||
"fastapi == 0.77.1",
|
||||
"uvicorn == 0.17.6",
|
||||
"jinja2 == 3.1.2",
|
||||
"pyyaml == 6.0",
|
||||
"pytest == 7.1.2",
|
||||
"pillow == 9.3.0",
|
||||
"aiofiles == 0.8.0",
|
||||
"dateparser == 1.1.1",
|
||||
"pyqt6 == 6.3.1",
|
||||
"defusedxml == 0.7.1",
|
||||
'schedule == 1.1.0',
|
||||
],
|
||||
include_package_data=True,
|
||||
entry_points={"console_scripts": ["khoj = src.main:run"]},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
]
|
||||
)
|
||||
136
src/configure.py
@@ -1,136 +0,0 @@
|
||||
# Standard Packages
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
|
||||
# External Packages
|
||||
import schedule
|
||||
|
||||
# Internal Packages
|
||||
from src.processor.ledger.beancount_to_jsonl import BeancountToJsonl
|
||||
from src.processor.markdown.markdown_to_jsonl import MarkdownToJsonl
|
||||
from src.processor.org_mode.org_to_jsonl import OrgToJsonl
|
||||
from src.search_type import image_search, text_search
|
||||
from src.utils.config import SearchType, SearchModels, ProcessorConfigModel, ConversationProcessorConfigModel
|
||||
from src.utils import state
|
||||
from src.utils.helpers import LRU, resolve_absolute_path
|
||||
from src.utils.rawconfig import FullConfig, ProcessorConfig
|
||||
from src.search_filter.date_filter import DateFilter
|
||||
from src.search_filter.word_filter import WordFilter
|
||||
from src.search_filter.file_filter import FileFilter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def configure_server(args, required=False):
|
||||
if args.config is None:
|
||||
if required:
|
||||
logger.error(f'Exiting as Khoj is not configured.\nConfigure it via GUI or by editing {state.config_file}.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.warn(f'Khoj is not configured.\nConfigure it via khoj GUI, plugins or by editing {state.config_file}.')
|
||||
return
|
||||
else:
|
||||
state.config = args.config
|
||||
|
||||
# Initialize Processor from Config
|
||||
state.processor_config = configure_processor(args.config.processor)
|
||||
|
||||
# Initialize the search model from Config
|
||||
state.search_index_lock.acquire()
|
||||
state.model = configure_search(state.model, state.config, args.regenerate)
|
||||
state.search_index_lock.release()
|
||||
|
||||
|
||||
@schedule.repeat(schedule.every(1).hour)
|
||||
def update_search_index():
|
||||
state.search_index_lock.acquire()
|
||||
state.model = configure_search(state.model, state.config, regenerate=False)
|
||||
state.search_index_lock.release()
|
||||
logger.info("Search Index updated via Scheduler")
|
||||
|
||||
|
||||
def configure_search(model: SearchModels, config: FullConfig, regenerate: bool, t: SearchType = None):
|
||||
# Initialize Org Notes Search
|
||||
if (t == SearchType.Org or t == None) and config.content_type.org:
|
||||
# Extract Entries, Generate Notes Embeddings
|
||||
model.orgmode_search = text_search.setup(
|
||||
OrgToJsonl,
|
||||
config.content_type.org,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()])
|
||||
|
||||
# Initialize Org Music Search
|
||||
if (t == SearchType.Music or t == None) and config.content_type.music:
|
||||
# Extract Entries, Generate Music Embeddings
|
||||
model.music_search = text_search.setup(
|
||||
OrgToJsonl,
|
||||
config.content_type.music,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter()])
|
||||
|
||||
# Initialize Markdown Search
|
||||
if (t == SearchType.Markdown or t == None) and config.content_type.markdown:
|
||||
# Extract Entries, Generate Markdown Embeddings
|
||||
model.markdown_search = text_search.setup(
|
||||
MarkdownToJsonl,
|
||||
config.content_type.markdown,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()])
|
||||
|
||||
# Initialize Ledger Search
|
||||
if (t == SearchType.Ledger or t == None) and config.content_type.ledger:
|
||||
# Extract Entries, Generate Ledger Embeddings
|
||||
model.ledger_search = text_search.setup(
|
||||
BeancountToJsonl,
|
||||
config.content_type.ledger,
|
||||
search_config=config.search_type.symmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()])
|
||||
|
||||
# Initialize Image Search
|
||||
if (t == SearchType.Image or t == None) and config.content_type.image:
|
||||
# Extract Entries, Generate Image Embeddings
|
||||
model.image_search = image_search.setup(
|
||||
config.content_type.image,
|
||||
search_config=config.search_type.image,
|
||||
regenerate=regenerate)
|
||||
|
||||
# Invalidate Query Cache
|
||||
state.query_cache = LRU()
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def configure_processor(processor_config: ProcessorConfig):
|
||||
if not processor_config:
|
||||
return
|
||||
|
||||
processor = ProcessorConfigModel()
|
||||
|
||||
# Initialize Conversation Processor
|
||||
if processor_config.conversation:
|
||||
processor.conversation = configure_conversation_processor(processor_config.conversation)
|
||||
|
||||
return processor
|
||||
|
||||
|
||||
def configure_conversation_processor(conversation_processor_config):
|
||||
conversation_processor = ConversationProcessorConfigModel(conversation_processor_config)
|
||||
conversation_logfile = resolve_absolute_path(conversation_processor.conversation_logfile)
|
||||
|
||||
if conversation_logfile.is_file():
|
||||
# Load Metadata Logs from Conversation Logfile
|
||||
with conversation_logfile.open('r') as f:
|
||||
conversation_processor.meta_log = json.load(f)
|
||||
logger.info('Conversation logs loaded from disk.')
|
||||
else:
|
||||
# Initialize Conversation Logs
|
||||
conversation_processor.meta_log = {}
|
||||
conversation_processor.chat_session = ""
|
||||
|
||||
return conversation_processor
|
||||
@@ -1,72 +0,0 @@
|
||||
# External Packages
|
||||
from PyQt6 import QtWidgets
|
||||
from PyQt6.QtCore import QDir
|
||||
|
||||
# Internal Packages
|
||||
from src.utils.config import SearchType
|
||||
from src.utils.helpers import is_none_or_empty
|
||||
|
||||
|
||||
class FileBrowser(QtWidgets.QWidget):
|
||||
def __init__(self, title, search_type: SearchType=None, default_files:list=[]):
|
||||
QtWidgets.QWidget.__init__(self)
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
self.search_type = search_type
|
||||
|
||||
self.filter_name = self.getFileFilter(search_type)
|
||||
self.dirpath = QDir.homePath()
|
||||
|
||||
self.label = QtWidgets.QLabel()
|
||||
self.label.setText(title)
|
||||
self.label.setFixedWidth(95)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
|
||||
self.lineEdit = QtWidgets.QPlainTextEdit(self)
|
||||
self.lineEdit.setFixedWidth(330)
|
||||
self.setFiles(default_files)
|
||||
self.lineEdit.setFixedHeight(min(7+20*len(self.lineEdit.toPlainText().split('\n')),90))
|
||||
self.lineEdit.textChanged.connect(self.updateFieldHeight)
|
||||
layout.addWidget(self.lineEdit)
|
||||
|
||||
self.button = QtWidgets.QPushButton('Add')
|
||||
self.button.clicked.connect(self.storeFilesSelectedInFileDialog)
|
||||
layout.addWidget(self.button)
|
||||
layout.addStretch()
|
||||
|
||||
def getFileFilter(self, search_type):
|
||||
if search_type == SearchType.Org:
|
||||
return 'Org-Mode Files (*.org)'
|
||||
elif search_type == SearchType.Ledger:
|
||||
return 'Beancount Files (*.bean *.beancount)'
|
||||
elif search_type == SearchType.Markdown:
|
||||
return 'Markdown Files (*.md *.markdown)'
|
||||
elif search_type == SearchType.Music:
|
||||
return 'Org-Music Files (*.org)'
|
||||
elif search_type == SearchType.Image:
|
||||
return 'Images (*.jp[e]g)'
|
||||
|
||||
def storeFilesSelectedInFileDialog(self):
|
||||
filepaths = self.getPaths()
|
||||
if self.search_type == SearchType.Image:
|
||||
filepaths.append(QtWidgets.QFileDialog.getExistingDirectory(self, caption='Choose Folder',
|
||||
directory=self.dirpath))
|
||||
else:
|
||||
filepaths.extend(QtWidgets.QFileDialog.getOpenFileNames(self, caption='Choose Files',
|
||||
directory=self.dirpath,
|
||||
filter=self.filter_name)[0])
|
||||
self.setFiles(filepaths)
|
||||
|
||||
def setFiles(self, paths:list):
|
||||
self.filepaths = [path for path in paths if not is_none_or_empty(path)]
|
||||
self.lineEdit.setPlainText("\n".join(self.filepaths))
|
||||
|
||||
def getPaths(self) -> list:
|
||||
if self.lineEdit.toPlainText() == '':
|
||||
return []
|
||||
else:
|
||||
return self.lineEdit.toPlainText().split('\n')
|
||||
|
||||
def updateFieldHeight(self):
|
||||
self.lineEdit.setFixedHeight(min(7+20*len(self.lineEdit.toPlainText().split('\n')),90))
|
||||
@@ -1,27 +0,0 @@
|
||||
# External Packages
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
# Internal Packages
|
||||
from src.utils.config import ProcessorType
|
||||
|
||||
|
||||
class LabelledTextField(QtWidgets.QWidget):
|
||||
def __init__(self, title, processor_type: ProcessorType=None, default_value: str=None):
|
||||
QtWidgets.QWidget.__init__(self)
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
self.processor_type = processor_type
|
||||
|
||||
self.label = QtWidgets.QLabel()
|
||||
self.label.setText(title)
|
||||
self.label.setFixedWidth(95)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
|
||||
self.input_field = QtWidgets.QTextEdit(self)
|
||||
self.input_field.setFixedWidth(410)
|
||||
self.input_field.setFixedHeight(27)
|
||||
self.input_field.setText(default_value)
|
||||
|
||||
layout.addWidget(self.input_field)
|
||||
layout.addStretch()
|
||||
@@ -1,318 +0,0 @@
|
||||
# Standard Packages
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
import webbrowser
|
||||
|
||||
# External Packages
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal
|
||||
|
||||
# Internal Packages
|
||||
from src.configure import configure_server
|
||||
from src.interface.desktop.file_browser import FileBrowser
|
||||
from src.interface.desktop.labelled_text_field import LabelledTextField
|
||||
from src.utils import constants, state, yaml as yaml_utils
|
||||
from src.utils.cli import cli
|
||||
from src.utils.config import SearchType, ProcessorType
|
||||
from src.utils.helpers import merge_dicts, resolve_absolute_path
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
"""Create Window to Configure Khoj
|
||||
Allow user to
|
||||
1. Configure content types to search
|
||||
2. Configure conversation processor
|
||||
3. Save the configuration to khoj.yml
|
||||
"""
|
||||
|
||||
def __init__(self, config_file: Path):
|
||||
super(MainWindow, self).__init__()
|
||||
self.config_file = config_file
|
||||
# Set regenerate flag to regenerate embeddings everytime user clicks configure
|
||||
if state.cli_args:
|
||||
state.cli_args += ['--regenerate']
|
||||
else:
|
||||
state.cli_args = ['--regenerate']
|
||||
|
||||
# Load config from existing config, if exists, else load from default config
|
||||
if resolve_absolute_path(self.config_file).exists():
|
||||
self.first_run = False
|
||||
self.current_config = yaml_utils.load_config_from_file(self.config_file)
|
||||
else:
|
||||
self.first_run = True
|
||||
self.current_config = deepcopy(constants.default_config)
|
||||
self.new_config = self.current_config
|
||||
|
||||
# Initialize Configure Window
|
||||
self.setWindowTitle("Khoj")
|
||||
self.setFixedWidth(600)
|
||||
|
||||
# Set Window Icon
|
||||
icon_path = constants.web_directory / 'assets/icons/favicon-144x144.png'
|
||||
self.setWindowIcon(QtGui.QIcon(f'{icon_path.absolute()}'))
|
||||
|
||||
# Initialize Configure Window Layout
|
||||
self.layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
# Add Settings Panels for each Search Type to Configure Window Layout
|
||||
self.search_settings_panels = []
|
||||
for search_type in SearchType:
|
||||
current_content_config = self.current_config['content-type'].get(search_type, {})
|
||||
self.search_settings_panels += [self.add_settings_panel(current_content_config, search_type)]
|
||||
|
||||
# Add Conversation Processor Panel to Configure Screen
|
||||
self.processor_settings_panels = []
|
||||
conversation_type = ProcessorType.Conversation
|
||||
current_conversation_config = self.current_config['processor'].get(conversation_type, {})
|
||||
self.processor_settings_panels += [self.add_processor_panel(current_conversation_config, conversation_type)]
|
||||
|
||||
# Add Action Buttons Panel
|
||||
self.add_action_panel()
|
||||
|
||||
# Set the central widget of the Window. Widget will expand
|
||||
# to take up all the space in the window by default.
|
||||
self.config_window = QtWidgets.QWidget()
|
||||
self.config_window.setLayout(self.layout)
|
||||
self.setCentralWidget(self.config_window)
|
||||
self.position_window()
|
||||
|
||||
def add_settings_panel(self, current_content_config: dict, search_type: SearchType):
|
||||
"Add Settings Panel for specified Search Type. Toggle Editable Search Types"
|
||||
# Get current files from config for given search type
|
||||
if search_type == SearchType.Image:
|
||||
current_content_files = current_content_config.get('input-directories', [])
|
||||
file_input_text = f'{search_type.name} Folders'
|
||||
else:
|
||||
current_content_files = current_content_config.get('input-files', [])
|
||||
file_input_text = f'{search_type.name} Files'
|
||||
|
||||
# Create widgets to display settings for given search type
|
||||
search_type_settings = QtWidgets.QWidget()
|
||||
search_type_layout = QtWidgets.QVBoxLayout(search_type_settings)
|
||||
enable_search_type = SearchCheckBox(f"Search {search_type.name}", search_type)
|
||||
# Add file browser to set input files for given search type
|
||||
input_files = FileBrowser(file_input_text, search_type, current_content_files or [])
|
||||
|
||||
# Set enabled/disabled based on checkbox state
|
||||
enable_search_type.setChecked(current_content_files is not None and len(current_content_files) > 0)
|
||||
input_files.setEnabled(enable_search_type.isChecked())
|
||||
enable_search_type.stateChanged.connect(lambda _: input_files.setEnabled(enable_search_type.isChecked()))
|
||||
|
||||
# Add setting widgets for given search type to panel
|
||||
search_type_layout.addWidget(enable_search_type)
|
||||
search_type_layout.addWidget(input_files)
|
||||
self.layout.addWidget(search_type_settings)
|
||||
|
||||
return search_type_settings
|
||||
|
||||
def add_processor_panel(self, current_conversation_config: dict, processor_type: ProcessorType):
|
||||
"Add Conversation Processor Panel"
|
||||
# Get current settings from config for given processor type
|
||||
current_openai_api_key = current_conversation_config.get('openai-api-key', None)
|
||||
|
||||
# Create widgets to display settings for given processor type
|
||||
processor_type_settings = QtWidgets.QWidget()
|
||||
processor_type_layout = QtWidgets.QVBoxLayout(processor_type_settings)
|
||||
enable_conversation = ProcessorCheckBox(f"Conversation", processor_type)
|
||||
# Add file browser to set input files for given processor type
|
||||
input_field = LabelledTextField("OpenAI API Key", processor_type, current_openai_api_key)
|
||||
|
||||
# Set enabled/disabled based on checkbox state
|
||||
enable_conversation.setChecked(current_openai_api_key is not None)
|
||||
input_field.setEnabled(enable_conversation.isChecked())
|
||||
enable_conversation.stateChanged.connect(lambda _: input_field.setEnabled(enable_conversation.isChecked()))
|
||||
|
||||
# Add setting widgets for given processor type to panel
|
||||
processor_type_layout.addWidget(enable_conversation)
|
||||
processor_type_layout.addWidget(input_field)
|
||||
self.layout.addWidget(processor_type_settings)
|
||||
|
||||
return processor_type_settings
|
||||
|
||||
def add_action_panel(self):
|
||||
"Add Action Panel"
|
||||
# Button to Save Settings
|
||||
action_bar = QtWidgets.QWidget()
|
||||
action_bar_layout = QtWidgets.QHBoxLayout(action_bar)
|
||||
|
||||
self.configure_button = QtWidgets.QPushButton("Configure", clicked=self.configure_app)
|
||||
self.search_button = QtWidgets.QPushButton("Search", clicked=lambda: webbrowser.open(f'http://{state.host}:{state.port}/'))
|
||||
self.search_button.setEnabled(not self.first_run)
|
||||
|
||||
action_bar_layout.addWidget(self.configure_button)
|
||||
action_bar_layout.addWidget(self.search_button)
|
||||
self.layout.addWidget(action_bar)
|
||||
|
||||
def get_default_config(self, search_type:SearchType=None, processor_type:ProcessorType=None):
|
||||
"Get default config"
|
||||
config = constants.default_config
|
||||
if search_type:
|
||||
return config['content-type'][search_type]
|
||||
elif processor_type:
|
||||
return config['processor'][processor_type]
|
||||
else:
|
||||
return config
|
||||
|
||||
def add_error_message(self, message: str):
|
||||
"Add Error Message to Configure Screen"
|
||||
# Remove any existing error messages
|
||||
for message_prefix in ErrorType:
|
||||
for i in reversed(range(self.layout.count())):
|
||||
current_widget = self.layout.itemAt(i).widget()
|
||||
if isinstance(current_widget, QtWidgets.QLabel) and current_widget.text().startswith(message_prefix.value):
|
||||
self.layout.removeWidget(current_widget)
|
||||
current_widget.deleteLater()
|
||||
|
||||
# Add new error message
|
||||
if message:
|
||||
error_message = QtWidgets.QLabel()
|
||||
error_message.setWordWrap(True)
|
||||
error_message.setText(message)
|
||||
error_message.setStyleSheet("color: red")
|
||||
self.layout.addWidget(error_message)
|
||||
|
||||
def update_search_settings(self):
|
||||
"Update config with search settings from UI"
|
||||
for settings_panel in self.search_settings_panels:
|
||||
for child in settings_panel.children():
|
||||
if not isinstance(child, (SearchCheckBox, FileBrowser)):
|
||||
continue
|
||||
if isinstance(child, SearchCheckBox):
|
||||
# Search Type Disabled
|
||||
if not child.isChecked() and child.search_type in self.new_config['content-type']:
|
||||
del self.new_config['content-type'][child.search_type]
|
||||
# Search Type (re)-Enabled
|
||||
if child.isChecked():
|
||||
current_search_config = self.current_config['content-type'].get(child.search_type, {})
|
||||
default_search_config = self.get_default_config(search_type = child.search_type)
|
||||
self.new_config['content-type'][child.search_type.value] = merge_dicts(current_search_config, default_search_config)
|
||||
elif isinstance(child, FileBrowser) and child.search_type in self.new_config['content-type']:
|
||||
if child.search_type.value == SearchType.Image:
|
||||
self.new_config['content-type'][child.search_type.value]['input-directories'] = child.getPaths() if child.getPaths() != [] else None
|
||||
else:
|
||||
self.new_config['content-type'][child.search_type.value]['input-files'] = child.getPaths() if child.getPaths() != [] else None
|
||||
|
||||
def update_processor_settings(self):
|
||||
"Update config with conversation settings from UI"
|
||||
for settings_panel in self.processor_settings_panels:
|
||||
for child in settings_panel.children():
|
||||
if not isinstance(child, (ProcessorCheckBox, LabelledTextField)):
|
||||
continue
|
||||
if isinstance(child, ProcessorCheckBox):
|
||||
# Processor Type Disabled
|
||||
if not child.isChecked() and child.processor_type in self.new_config['processor']:
|
||||
del self.new_config['processor'][child.processor_type]
|
||||
# Processor Type (re)-Enabled
|
||||
if child.isChecked():
|
||||
current_processor_config = self.current_config['processor'].get(child.processor_type, {})
|
||||
default_processor_config = self.get_default_config(processor_type = child.processor_type)
|
||||
self.new_config['processor'][child.processor_type.value] = merge_dicts(current_processor_config, default_processor_config)
|
||||
elif isinstance(child, LabelledTextField) and child.processor_type in self.new_config['processor']:
|
||||
if child.processor_type == ProcessorType.Conversation:
|
||||
self.new_config['processor'][child.processor_type.value]['openai-api-key'] = child.input_field.toPlainText() if child.input_field.toPlainText() != '' else None
|
||||
|
||||
def save_settings_to_file(self) -> bool:
|
||||
"Save validated settings to file"
|
||||
# Validate config before writing to file
|
||||
try:
|
||||
yaml_utils.parse_config_from_string(self.new_config)
|
||||
except Exception as e:
|
||||
print(f"Error validating config: {e}")
|
||||
self.add_error_message(f"{ErrorType.ConfigValidationError.value}: {e}")
|
||||
return False
|
||||
|
||||
# Save the config to app config file
|
||||
self.add_error_message(None)
|
||||
yaml_utils.save_config_to_file(self.new_config, self.config_file)
|
||||
return True
|
||||
|
||||
def load_updated_settings(self):
|
||||
"Hot swap to use the updated config from config file"
|
||||
# Load parsed, validated config from app config file
|
||||
args = cli(state.cli_args)
|
||||
self.current_config = self.new_config
|
||||
|
||||
# Configure server with loaded config
|
||||
configure_server(args, required=True)
|
||||
|
||||
def configure_app(self):
|
||||
"Save the new settings to khoj.yml. Reload app with updated settings"
|
||||
self.update_search_settings()
|
||||
self.update_processor_settings()
|
||||
if self.save_settings_to_file():
|
||||
# Setup thread to load updated settings in background
|
||||
self.thread = QThread()
|
||||
self.settings_loader = SettingsLoader(self.load_updated_settings)
|
||||
self.settings_loader.moveToThread(self.thread)
|
||||
|
||||
# Connect slots and signals for thread
|
||||
self.thread.started.connect(self.settings_loader.run)
|
||||
self.settings_loader.finished.connect(self.thread.quit)
|
||||
self.settings_loader.finished.connect(self.settings_loader.deleteLater)
|
||||
self.settings_loader.error.connect(self.add_error_message)
|
||||
self.thread.finished.connect(self.thread.deleteLater)
|
||||
|
||||
# Start thread
|
||||
self.thread.start()
|
||||
|
||||
# Disable Save Button
|
||||
self.search_button.setEnabled(False)
|
||||
self.configure_button.setEnabled(False)
|
||||
self.configure_button.setText("Configuring...")
|
||||
|
||||
# Reset UI
|
||||
self.thread.finished.connect(lambda: self.configure_button.setText("Configure"))
|
||||
self.thread.finished.connect(lambda: self.configure_button.setEnabled(True))
|
||||
self.thread.finished.connect(lambda: self.search_button.setEnabled(True))
|
||||
|
||||
def position_window(self):
|
||||
"Position the window at center of X axis and near top on Y axis"
|
||||
window_rectangle = self.geometry()
|
||||
screen_center = self.screen().availableGeometry().center()
|
||||
window_rectangle.moveCenter(screen_center)
|
||||
self.move(window_rectangle.topLeft().x(), 25)
|
||||
|
||||
def show_on_top(self):
|
||||
"Bring Window on Top"
|
||||
self.show()
|
||||
self.setWindowState(Qt.WindowState.WindowActive)
|
||||
self.activateWindow() # For Bringing to Top on Windows
|
||||
self.raise_() # For Bringing to Top from Minimized State on OSX
|
||||
|
||||
|
||||
class SettingsLoader(QObject):
|
||||
"Load Settings Thread"
|
||||
finished = pyqtSignal()
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, load_settings_func):
|
||||
super(SettingsLoader, self).__init__()
|
||||
self.load_settings_func = load_settings_func
|
||||
|
||||
def run(self):
|
||||
"Load Settings"
|
||||
try:
|
||||
self.load_settings_func()
|
||||
except FileNotFoundError as e:
|
||||
self.error.emit(f"{ErrorType.ConfigLoadingError.value}: {e}")
|
||||
else:
|
||||
self.error.emit(None)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class SearchCheckBox(QtWidgets.QCheckBox):
|
||||
def __init__(self, text, search_type: SearchType, parent=None):
|
||||
self.search_type = search_type
|
||||
super(SearchCheckBox, self).__init__(text, parent=parent)
|
||||
|
||||
|
||||
class ProcessorCheckBox(QtWidgets.QCheckBox):
|
||||
def __init__(self, text, processor_type: ProcessorType, parent=None):
|
||||
self.processor_type = processor_type
|
||||
super(ProcessorCheckBox, self).__init__(text, parent=parent)
|
||||
|
||||
class ErrorType(Enum):
|
||||
"Error Types"
|
||||
ConfigLoadingError = "Config Loading Error"
|
||||
ConfigValidationError = "Config Validation Error"
|
||||
@@ -1,65 +1,101 @@
|
||||
* Khoj Emacs 🦅
|
||||
[[https://stable.melpa.org/#/khoj][file:https://stable.melpa.org/packages/khoj-badge.svg]] [[https://melpa.org/#/khoj][file:https://melpa.org/packages/khoj-badge.svg]] [[https://github.com/debanjum/khoj/actions/workflows/build_khoj_el.yml][https://github.com/debanjum/khoj/actions/workflows/build_khoj_el.yml/badge.svg?]]
|
||||
|
||||
/Natural language search from within Emacs using [[https://github.com/debanjum/khoj][Khoj]]/
|
||||
[[https://github.com/khoj-ai/khoj/edit/master/src/interface/emacs/README.org][file:/src/khoj/interface/web/assets/icons/khoj-logo-sideways-200.png]] Emacs
|
||||
|
||||
|
||||
[[https://stable.melpa.org/#/khoj][file:https://stable.melpa.org/packages/khoj-badge.svg]] [[https://melpa.org/#/khoj][file:https://melpa.org/packages/khoj-badge.svg]] [[https://github.com/khoj-ai/khoj/actions/workflows/build_khoj_el.yml][https://github.com/khoj-ai/khoj/actions/workflows/build_khoj_el.yml/badge.svg?]] [[https://github.com/khoj-ai/khoj/actions/workflows/test_khoj_el.yml][https://github.com/khoj-ai/khoj/actions/workflows/test_khoj_el.yml/badge.svg?]]
|
||||
|
||||
/An AI personal assistant for your digital brain/
|
||||
|
||||
** Table of Contents
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#features][Features]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Interface][Interface]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Setup][Setup]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#1-Setup-Backend][Setup Backend]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#2-Install-Khojel][Install Khoj.el]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Use][Use]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Search][Search]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Find-similar-entries][Find Similar Entries]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Advanced-usage][Advanced Usage]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Khoj-menu][Khoj Menu]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade][Upgrade]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade-Khoj-Backend][Upgrade Backend]]
|
||||
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade-Khojel][Upgrade Khoj.el]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#features][Features]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Interface][Interface]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Setup][Setup]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Direct-Install][Direct Install]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Minimal-Install][Minimal Install]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Standard-Install][Standard Install]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#With-Straight.el][With Straight.el]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Use][Use]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Search][Search]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Chat][Chat]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Find-similar-entries][Find Similar Entries]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Advanced-usage][Advanced Usage]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Khoj-menu][Khoj Menu]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade][Upgrade]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade-Khoj-Backend][Upgrade Backend]]
|
||||
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade-Khojel][Upgrade Khoj.el]]
|
||||
|
||||
** Features
|
||||
- *Natural*: Advanced natural language understanding using Transformer based ML Models
|
||||
- *Local*: Your personal data stays local. All search, indexing is done on your machine*
|
||||
- *Incremental*: Incremental search for a fast, search-as-you-type experience
|
||||
- *Search*
|
||||
- *Natural*: Advanced natural language understanding using Transformer based ML Models
|
||||
- *Local*: Your personal data stays local. All search, indexing is done on your machine*
|
||||
- *Incremental*: Incremental search for a fast, search-as-you-type experience
|
||||
- *Chat*
|
||||
- *Faster answers*: Find answers faster than search
|
||||
- *Iterative discovery*: Iteratively explore and (re-)discover your notes
|
||||
- *Assisted creativity*: Smoothly weave across answer retrieval and content generation
|
||||
|
||||
** Interface
|
||||
*** Search UI
|
||||
[[/docs/khoj_on_emacs.png]]
|
||||
|
||||
*** Chat UI
|
||||
[[/docs/khoj_chat_on_emacs_0.5.0.png]]
|
||||
|
||||
** Setup
|
||||
*** 1. Setup Backend
|
||||
#+begin_src shell
|
||||
pip install khoj-assistant && khoj
|
||||
#+end_src
|
||||
- /Make sure [[https://realpython.com/installing-python/][python]] and [[https://pip.pypa.io/en/stable/installation/][pip]] are installed on your machine/
|
||||
|
||||
*** 2. Install Khoj.el
|
||||
**** Using MELPA
|
||||
#+begin_src elisp
|
||||
- /khoj.el attempts to automatically install, start and configure the khoj server./
|
||||
If this fails, follow [[https://github.com/khoj-ai/khoj/tree/master/#Setup][these instructions]] to manually setup the khoj server.
|
||||
|
||||
*** Direct Install
|
||||
#+begin_src elisp
|
||||
M-x package-install khoj
|
||||
#+end_src elisp
|
||||
#+end_src
|
||||
|
||||
Add below snippet to your Emacs config file
|
||||
#+begin_src elisp
|
||||
;; Install Khoj Package from MELPA Stable
|
||||
(use-package khoj
|
||||
:ensure t
|
||||
:pin melpa-stable
|
||||
:bind ("C-c s" . 'khoj))
|
||||
#+end_src
|
||||
*** Minimal Install
|
||||
Add below snippet to your Emacs config file.
|
||||
Indexes your org-agenda files, by default.
|
||||
|
||||
Note: Install ~khoj.el~ from MELPA (instead of MELPA Stable) if you installed the pre-release version of khoj
|
||||
- That is, use ~:pin melpa~ to install khoj.el in above snippet if khoj was installed with ~pip install --pre khoj-assistant~
|
||||
- Else use ~:pin melpa-stable~ to install khoj.el in above snippet if khoj was installed with ~pip install khoj-assistant~
|
||||
- This ensures both khoj.el and khoj app are from the same version (tagged or latest)
|
||||
#+begin_src elisp
|
||||
;; Install Khoj Package from MELPA Stable
|
||||
(use-package khoj
|
||||
:ensure t
|
||||
:pin melpa-stable
|
||||
:bind ("C-c s" . 'khoj)
|
||||
#+end_src
|
||||
|
||||
- Note: Install ~khoj.el~ from MELPA (instead of MELPA Stable) if you installed the pre-release version of khoj
|
||||
- That is, use ~:pin melpa~ to install khoj.el in above snippet if khoj server was installed with ~--pre~ flag, i.e ~pip install --pre khoj-assistant~
|
||||
- Else use ~:pin melpa-stable~ to install khoj.el in above snippet if khoj was installed with ~pip install khoj-assistant~
|
||||
- This ensures both khoj.el and khoj app are from the same version (git tagged or latest)
|
||||
|
||||
*** Standard Install
|
||||
Add below snippet to your Emacs config file.
|
||||
Indexes the specified org files, directories. Sets up OpenAI API key for Khoj Chat
|
||||
|
||||
#+begin_src elisp
|
||||
;; Install Khoj Package from MELPA Stable
|
||||
(use-package khoj
|
||||
:ensure t
|
||||
:pin melpa-stable
|
||||
:bind ("C-c s" . 'khoj)
|
||||
:config (setq khoj-org-directories '("~/docs/org-roam" "~/docs/notes")
|
||||
khoj-org-files '("~/docs/todo.org" "~/docs/work.org")
|
||||
khoj-openai-api-key "YOUR_OPENAI_API_KEY")) ; required to enable chat
|
||||
#+end_src
|
||||
|
||||
*** With [[https://github.com/raxod502/straight.el][Straight.el]]
|
||||
Add below snippet to your Emacs config file.
|
||||
Indexes the specified org files, directories. Sets up OpenAI API key for Khoj Chat
|
||||
|
||||
**** Using [[https://github.com/raxod502/straight.el][Straight.el]]
|
||||
Add below snippet to your Emacs config file
|
||||
#+begin_src elisp
|
||||
;; Install Khoj Package using Straight.el
|
||||
(use-package khoj
|
||||
:after org
|
||||
:straight (khoj :type git :host github :repo "debanjum/khoj" :files (:defaults "src/interface/emacs/khoj.el"))
|
||||
:bind ("C-c s" . 'khoj))
|
||||
:straight (khoj :type git :host github :repo "khoj-ai/khoj" :files (:defaults "src/interface/emacs/khoj.el"))
|
||||
:bind ("C-c s" . 'khoj)
|
||||
:config (setq khoj-org-directories '("~/docs/org-roam" "~/docs/notes")
|
||||
khoj-org-files '("~/docs/todo.org" "~/docs/work.org")
|
||||
khoj-openai-api-key "YOUR_OPENAI_API_KEY" ; required to enable chat)
|
||||
#+end_src
|
||||
|
||||
** Use
|
||||
@@ -70,13 +106,22 @@
|
||||
|
||||
e.g "What is the meaning of life?", "My life goals for 2023"
|
||||
|
||||
*** Chat
|
||||
1. Hit ~C-c s c~ (or ~M-x khoj RET c~) to open khoj chat
|
||||
|
||||
2. Ask questions in a natural, conversational style
|
||||
|
||||
E.g "When did I file my taxes last year?"
|
||||
|
||||
See [[https://github.com/khoj-ai/khoj/tree/master/#Khoj-Chat][Khoj Chat]] for more details
|
||||
|
||||
*** Find Similar Entries
|
||||
This feature finds entries similar to the one you are currently on.
|
||||
1. Move cursor to the org-mode entry, markdown section or text paragraph you want to find similar entries for
|
||||
2. Hit ~C-c s f~ (or ~M-x khoj RET f~) to find similar entries
|
||||
|
||||
*** Advanced Usage
|
||||
- Add [[https://github.com/debanjum/khoj/#query-filters][query filters]] during search to narrow down results further
|
||||
- Add [[https://github.com/khoj-ai/khoj/#query-filters][query filters]] during search to narrow down results further
|
||||
|
||||
e.g `What is the meaning of life? -"god" +"none" dt>"last week"`
|
||||
|
||||
|
||||
212
src/interface/emacs/tests/khoj-tests.el
Normal file
@@ -0,0 +1,212 @@
|
||||
;;; khoj-tests.el --- Test suite for khoj.el -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2023 Debanjum Singh Solanky
|
||||
|
||||
;; Author: Debanjum Singh Solanky <debanjum@gmail.com>
|
||||
;; Version: 0.0.0
|
||||
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1") (org "9.0.0"))
|
||||
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
|
||||
|
||||
;;; License:
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License
|
||||
;; as published by the Free Software Foundation; either version 3
|
||||
;; of the License, or (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; This file contains the test suite for khoj.el.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'dash)
|
||||
(require 'ert)
|
||||
(require 'khoj)
|
||||
(require 'org)
|
||||
|
||||
|
||||
|
||||
;; ----------------------------------------------------
|
||||
;; Test Extract and Render Entries of each Content Type
|
||||
;; ----------------------------------------------------
|
||||
|
||||
(ert-deftest khoj-tests--extract-entries-as-markdown ()
|
||||
"Test `json-response', `query' from API formatted as markdown."
|
||||
(let ((user-query "Become God")
|
||||
(json-response-from-khoj-backend
|
||||
(json-read-from-string
|
||||
"[\
|
||||
{\
|
||||
\"entry\": \"## Upgrade\\n\\n Penance to Immortality\",\
|
||||
\"score\": \"0.376\",\
|
||||
\"additional\": {\
|
||||
\"file\": \"/home/ravan/upgrade.md\",\
|
||||
\"compiled\": \"## Upgrade Penance to Immortality\"\
|
||||
}\
|
||||
},\
|
||||
{\
|
||||
\"entry\": \"## Act\\n\\n Rule everything\",\
|
||||
\"score\": \"0.153\",\
|
||||
\"additional\": {\
|
||||
\"file\": \"/home/ravan/act.md\",\
|
||||
\"compiled\": \"## Act Rule everything\"\
|
||||
}\
|
||||
}]\
|
||||
")))
|
||||
(should
|
||||
(equal
|
||||
(khoj--extract-entries-as-markdown json-response-from-khoj-backend user-query)
|
||||
"\
|
||||
# Become God\n\
|
||||
## Upgrade\n\
|
||||
\n\
|
||||
Penance to Immortality\n\n\
|
||||
## Act\n\
|
||||
\n\
|
||||
Rule everything\n\n"))))
|
||||
|
||||
|
||||
(ert-deftest khoj-tests--extract-entries-as-org ()
|
||||
"Test `json-response', `query' from API formatted as org."
|
||||
(let ((user-query "Become God")
|
||||
(json-response-from-khoj-backend
|
||||
(json-read-from-string
|
||||
"[\
|
||||
{\
|
||||
\"entry\": \"** Upgrade\\n\\n Penance to Immortality\\n\",\
|
||||
\"score\": \"0.42\",\
|
||||
\"additional\": {\
|
||||
\"file\": \"/home/ravan/upgrade.md\",\
|
||||
\"compiled\": \"** Upgrade Penance to Immortality\"\
|
||||
}\
|
||||
},\
|
||||
{\
|
||||
\"entry\": \"** Act\\n\\n Rule everything\\n\",\
|
||||
\"score\": \"0.42\",\
|
||||
\"additional\": {\
|
||||
\"file\": \"/home/ravan/act.md\",\
|
||||
\"compiled\": \"** Act Rule everything\"\
|
||||
}\
|
||||
}]\
|
||||
")))
|
||||
(should
|
||||
(equal
|
||||
(khoj--extract-entries-as-org json-response-from-khoj-backend user-query)
|
||||
"\
|
||||
* Become God\n\
|
||||
** Upgrade\n\
|
||||
\n\
|
||||
Penance to Immortality\n\
|
||||
** Act\n\
|
||||
\n\
|
||||
Rule everything\n\
|
||||
\n"))))
|
||||
|
||||
|
||||
|
||||
;; -------------------------------------
|
||||
;; Test Helpers for Find Similar Feature
|
||||
;; -------------------------------------
|
||||
|
||||
(ert-deftest khoj-tests--get-current-outline-entry-text ()
|
||||
"Test get current outline-mode entry text'."
|
||||
(with-temp-buffer
|
||||
(insert "\
|
||||
* Become God\n\
|
||||
** Upgrade\n\
|
||||
\n\
|
||||
Penance to Immortality\n\
|
||||
** Act\n\
|
||||
\n\
|
||||
Rule everything\\n")
|
||||
(goto-char (point-min))
|
||||
|
||||
;; Test getting current entry text from cursor at start of outline heading
|
||||
(outline-next-visible-heading 1)
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-outline-entry-text)
|
||||
"\
|
||||
** Upgrade\n\
|
||||
\n\
|
||||
Penance to Immortality"))
|
||||
|
||||
;; Test getting current entry text from cursor within outline entry
|
||||
(forward-line)
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-outline-entry-text)
|
||||
"\
|
||||
** Upgrade\n\
|
||||
\n\
|
||||
Penance to Immortality"))
|
||||
))
|
||||
|
||||
|
||||
(ert-deftest khoj-tests--get-current-paragraph-text ()
|
||||
"Test get current paragraph text'."
|
||||
(with-temp-buffer
|
||||
(insert "\
|
||||
* Become God\n\
|
||||
** Upgrade\n\
|
||||
\n\
|
||||
Penance to Immortality\n\
|
||||
** Act\n\
|
||||
\n\
|
||||
Rule everything\n")
|
||||
;; Test getting current paragraph text from cursor at start of buffer
|
||||
(goto-char (point-min))
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-paragraph-text)
|
||||
"* Become God\n\
|
||||
** Upgrade"))
|
||||
|
||||
;; Test getting current paragraph text from cursor within paragraph
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-paragraph-text)
|
||||
"* Become God\n\
|
||||
** Upgrade"))
|
||||
|
||||
;; Test getting current paragraph text from cursor at paragraph end
|
||||
(goto-char (point-min))
|
||||
(forward-line 2)
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-paragraph-text)
|
||||
"* Become God\n\
|
||||
** Upgrade"))
|
||||
|
||||
;; Test getting current paragraph text from cursor at start of middle paragraph
|
||||
(goto-char (point-min))
|
||||
(forward-line 3)
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-paragraph-text)
|
||||
"Penance to Immortality\n\
|
||||
** Act"))
|
||||
|
||||
;; Test getting current paragraph text from cursor at end of buffer
|
||||
(goto-char (point-max))
|
||||
(should
|
||||
(equal
|
||||
(khoj--get-current-paragraph-text)
|
||||
"Rule everything"))
|
||||
))
|
||||
|
||||
|
||||
(provide 'khoj-tests)
|
||||
|
||||
;;; khoj-tests.el ends here
|
||||
@@ -1,2 +1,2 @@
|
||||
npm node_modules
|
||||
build
|
||||
build
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
@@ -19,5 +19,5 @@
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
tag-version-prefix=""
|
||||
tag-version-prefix=""
|
||||
|
||||
@@ -619,4 +619,3 @@ Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
# Khoj Obsidian 🦅
|
||||
> Natural language search for your Obsidian notes using [Khoj](https://github.com/debanjum/khoj)
|
||||
<img src="/src/khoj/interface/web/assets/icons/khoj-logo-sideways.svg" width="200" alt="Khoj Logo">Obsidian
|
||||
|
||||
> An AI personal assistant for your Digital Brain in Obsidian
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#Features)
|
||||
- [Demo](#Demo)
|
||||
- [Description](#Description)
|
||||
- [Interface](#Interface)
|
||||
- [Search Demo](#Search-Demo)
|
||||
- [Interfaces](#Interfaces)
|
||||
- [Search Modal](#Search-Modal)
|
||||
- [Chat Modal](#Chat-Modal)
|
||||
- [Setup](#Setup)
|
||||
- [Setup Backend](#1-Setup-Backend)
|
||||
- [Setup Plugin](#2-Setup-Plugin)
|
||||
- [Use](#Use)
|
||||
- [Search](#search)
|
||||
- [Chat](#chat)
|
||||
- [Find Similar Notes](#find-similar-notes)
|
||||
- [Upgrade](#Upgrade)
|
||||
- [Upgrade Backend](#1-Upgrade-Backend)
|
||||
@@ -21,45 +25,84 @@
|
||||
- [Implementation](#Implementation)
|
||||
|
||||
## Features
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Local**: Your personal data stays local. All search, indexing is done on your machine[\*](https://github.com/debanjum/khoj#miscellaneous)
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
- **Search**
|
||||
- **Natural**: Advanced natural language understanding using Transformer based ML Models
|
||||
- **Local**: Your personal data stays local. All search and indexing is done on your machine. *Unlike chat which requires access to GPT.*
|
||||
- **Incremental**: Incremental search for a fast, search-as-you-type experience
|
||||
- **Chat**
|
||||
- **Faster answers**: Find answers faster and with less effort than search
|
||||
- **Iterative discovery**: Iteratively explore and (re-)discover your notes
|
||||
- **Assisted creativity**: Smoothly weave across answers retrieval and content generation
|
||||
|
||||
## Demo
|
||||
https://user-images.githubusercontent.com/6413477/210486007-36ee3407-e6aa-4185-8a26-b0bfc0a4344f.mp4
|
||||
### Search Demo
|
||||
https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b
|
||||
|
||||
<details><summary>Description</summary>
|
||||
|
||||
1. Install Khoj via `pip` and start Khoj backend in non-gui mode
|
||||
1. Install Khoj via `pip` and start Khoj backend
|
||||
```shell
|
||||
python -m pip install khoj-assistant && khoj
|
||||
```
|
||||
2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
|
||||
3. Check the new Khoj plugin settings
|
||||
4. Wait for Khoj backend to index markdown files in the current Vault
|
||||
5. Open Khoj plugin on Obsidian via Search button on Left Pane
|
||||
6. Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
|
||||
7. Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
|
||||
- Check the new Khoj plugin settings
|
||||
- Wait for Khoj backend to index markdown, PDF 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)
|
||||
|
||||
</details>
|
||||
|
||||
### Interface
|
||||

|
||||
## Interfaces
|
||||
### Search Modal
|
||||

|
||||
|
||||
### Chat Modal
|
||||

|
||||
|
||||
## Setup
|
||||
- *Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine*
|
||||
- *Ensure you follow the ordering of the setup steps. Install the plugin after starting the khoj backend. This allows the plugin to configure the khoj backend*
|
||||
|
||||
### 1. Setup Backend
|
||||
Open terminal/cmd and run below command to install and start the khoj backend
|
||||
- On Linux/MacOS
|
||||
```shell
|
||||
python -m pip install khoj-assistant && khoj
|
||||
```
|
||||
|
||||
- On Windows
|
||||
```shell
|
||||
py -m pip install khoj-assistant && khoj
|
||||
```
|
||||
|
||||
```shell
|
||||
pip install khoj-assistant && khoj --no-gui
|
||||
```
|
||||
### 2. Setup Plugin
|
||||
1. Open *Community plugins* tab in Obsidian settings panel
|
||||
2. Click Browse and Search for *Khoj*
|
||||
3. Click *Install*, after that click *Enable* on the Khoj plugin
|
||||
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. [Optional] To enable Khoj Chat, set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings
|
||||
|
||||
See [official docs](https://help.obsidian.md/Advanced+topics/Community+plugins#Discover+and+install+community+plugins) for details
|
||||
See [official Obsidian plugin docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for details
|
||||
|
||||
## Use
|
||||
### Chat
|
||||
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?"
|
||||
|
||||
Notes:
|
||||
- *Using Khoj Chat will result in query relevant notes being shared with OpenAI for ChatGPT to respond.*
|
||||
- *To use Khoj Chat, ensure you've set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings.*
|
||||
|
||||
See [Khoj Chat](https://github.com/khoj-ai/khoj/tree/master/#Khoj-Chat) for more details
|
||||
|
||||

|
||||
|
||||
### Search
|
||||
Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
|
||||
|
||||
*Note: Ensure the khoj server is running in the background before searching. Execute `khoj` in your terminal if it is not already running*
|
||||
|
||||
https://user-images.githubusercontent.com/6413477/218801155-cd67e8b4-a770-404a-8179-d6b61caa0f93.mp4
|
||||
|
||||
<details><summary>Query Filters</summary>
|
||||
|
||||
Use structured query syntax to filter the natural language search results
|
||||
@@ -105,14 +148,14 @@ To see other notes similar to the current one, run *Khoj: Find Similar Notes* fr
|
||||
So notes across multiple vaults **cannot** be searched at the same time
|
||||
|
||||
## Visualize Codebase
|
||||
<img src="https://github.com/debanjum/khoj/blob/master/src/interface/obsidian/docs/khoj_obsidian_codebase_visualization_0.2.1.png" width="700" />
|
||||
<img src="https://github.com/khoj-ai/khoj/blob/master/src/interface/obsidian/docs/khoj_obsidian_codebase_visualization_0.2.1.png" width="700" />
|
||||
|
||||
## Implementation
|
||||
The plugin implements the following functionality to search your notes with Khoj:
|
||||
- [X] Open the Khoj search modal via left ribbon icon or the *Khoj: Search* command
|
||||
- [X] Render results as Markdown preview to improve readability
|
||||
- [X] Configure Khoj via the plugin setting tab on the settings page
|
||||
- Set Obsidian Vault to Index with Khoj. Defaults to all markdown files in current Vault
|
||||
- Set Obsidian Vault to Index with Khoj. Defaults to all markdown, PDF files in current Vault
|
||||
- Set URL of Khoj backend
|
||||
- Set Number of Search Results to show in Search Modal
|
||||
- [X] Allow reranking of result to improve search quality
|
||||
|
||||
BIN
src/interface/obsidian/docs/khoj_chat_on_obsidian_0.6.0.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
@@ -31,6 +31,16 @@ esbuild.build({
|
||||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'@lezer/lr',
|
||||
'node:fs',
|
||||
'node:path',
|
||||
'node:util',
|
||||
'node:url',
|
||||
'node:http',
|
||||
'node:https',
|
||||
'node:stream',
|
||||
'node:zlib',
|
||||
'node:buffer',
|
||||
'node:net',
|
||||
...builtins],
|
||||
format: 'cjs',
|
||||
watch: !prod,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "khoj",
|
||||
"name": "Khoj",
|
||||
"version": "0.2.5",
|
||||
"version": "0.8.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Natural, Incremental Search for your Second Brain 🦅",
|
||||
"description": "An AI Personal Assistant for your Digital Brain",
|
||||
"author": "Debanjum Singh Solanky",
|
||||
"authorUrl": "https://github.com/debanjum",
|
||||
"isDesktopOnly": false
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "0.2.5",
|
||||
"description": "Natural, Incremental Search for your Second Brain 🦅",
|
||||
"version": "0.8.0",
|
||||
"description": "An AI Personal Assistant for your Digital Brain",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": ["search"],
|
||||
"keywords": [
|
||||
"search",
|
||||
"chat",
|
||||
"AI",
|
||||
"assistant"
|
||||
],
|
||||
"author": "Debanjum Singh Solanky",
|
||||
"license": "GPLv3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
@@ -20,5 +25,9 @@
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"node-fetch": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
194
src/interface/obsidian/src/chat_modal.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { App, Modal, request } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export class KhojChatModal extends Modal {
|
||||
result: string;
|
||||
setting: KhojSetting;
|
||||
|
||||
constructor(app: App, setting: KhojSetting) {
|
||||
super(app);
|
||||
this.setting = setting;
|
||||
|
||||
// Register Modal Keybindings to send user message
|
||||
this.scope.register([], 'Enter', async () => {
|
||||
// Get text in chat input elmenet
|
||||
let input_el = <HTMLInputElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
|
||||
// Clear text after extracting message to send
|
||||
let user_message = input_el.value;
|
||||
input_el.value = "";
|
||||
|
||||
// Get and render chat response to user message
|
||||
await this.getChatResponse(user_message);
|
||||
});
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
let { contentEl } = this;
|
||||
contentEl.addClass("khoj-chat");
|
||||
|
||||
// Add title to the Khoj Chat modal
|
||||
contentEl.createEl("h1", ({ attr: { id: "khoj-chat-title" }, text: "Khoj Chat" }));
|
||||
|
||||
// Create area for chat logs
|
||||
contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
|
||||
|
||||
// Get chat history from Khoj backend
|
||||
await this.getChatHistory();
|
||||
|
||||
// Add chat input field
|
||||
contentEl.createEl("input",
|
||||
{
|
||||
attr: {
|
||||
type: "text",
|
||||
id: "khoj-chat-input",
|
||||
autofocus: "autofocus",
|
||||
placeholder: "Chat with Khoj [Hit Enter to send message]",
|
||||
class: "khoj-chat-input option"
|
||||
}
|
||||
})
|
||||
.addEventListener('change', (event) => { this.result = (<HTMLInputElement>event.target).value });
|
||||
|
||||
// Scroll to bottom of modal, till the send message input box
|
||||
this.modalEl.scrollTop = this.modalEl.scrollHeight;
|
||||
}
|
||||
|
||||
generateReference(messageEl: any, reference: string, index: number) {
|
||||
// Generate HTML for Chat Reference
|
||||
// `<sup><abbr title="${escaped_ref}" tabindex="0">${index}</abbr></sup>`;
|
||||
let escaped_ref = reference.replace(/"/g, """)
|
||||
return messageEl.createEl("sup").createEl("abbr", {
|
||||
attr: {
|
||||
title: escaped_ref,
|
||||
tabindex: "0",
|
||||
},
|
||||
text: `[${index}] `,
|
||||
});
|
||||
}
|
||||
|
||||
renderMessageWithReferences(message: string, sender: string, context?: [string], dt?: Date) {
|
||||
let messageEl = this.renderMessage(message, sender, dt);
|
||||
if (context && !!messageEl) {
|
||||
context.map((reference, index) => this.generateReference(messageEl, reference, index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
renderMessage(message: string, sender: string, dt?: Date): Element | null {
|
||||
let message_time = this.formatDate(dt ?? new Date());
|
||||
let emojified_sender = sender == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
|
||||
// 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({
|
||||
attr: {
|
||||
"data-meta": `${emojified_sender} at ${message_time}`,
|
||||
class: `khoj-chat-message ${sender}`
|
||||
},
|
||||
}).createDiv({
|
||||
attr: {
|
||||
class: `khoj-chat-message-text ${sender}`
|
||||
},
|
||||
text: `${message}`
|
||||
})
|
||||
|
||||
// Remove user-select: none property to make text selectable
|
||||
chat_message_el.style.userSelect = "text";
|
||||
|
||||
// Scroll to bottom after inserting chat messages
|
||||
this.modalEl.scrollTop = this.modalEl.scrollHeight;
|
||||
|
||||
return chat_message_el
|
||||
}
|
||||
|
||||
createKhojResponseDiv(dt?: Date): HTMLDivElement {
|
||||
let message_time = 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({
|
||||
attr: {
|
||||
"data-meta": `🏮 Khoj at ${message_time}`,
|
||||
class: `khoj-chat-message khoj`
|
||||
},
|
||||
}).createDiv({
|
||||
attr: {
|
||||
class: `khoj-chat-message-text khoj`
|
||||
},
|
||||
})
|
||||
|
||||
// Scroll to bottom after inserting chat messages
|
||||
this.modalEl.scrollTop = this.modalEl.scrollHeight;
|
||||
|
||||
return chat_message_el
|
||||
}
|
||||
|
||||
renderIncrementalMessage(htmlElement: HTMLDivElement, additionalMessage: string) {
|
||||
htmlElement.innerHTML += additionalMessage;
|
||||
// Scroll to bottom of modal, till the send message input box
|
||||
this.modalEl.scrollTop = this.modalEl.scrollHeight;
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
// 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' }).replace(/-/g, ' ');
|
||||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
async getChatHistory(): Promise<void> {
|
||||
// Get chat history from Khoj backend
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat/history?client=obsidian`;
|
||||
let response = await request(chatUrl);
|
||||
let chatLogs = JSON.parse(response).response;
|
||||
chatLogs.forEach((chatLog: any) => {
|
||||
this.renderMessageWithReferences(chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created));
|
||||
});
|
||||
}
|
||||
|
||||
async getChatResponse(query: string | undefined | null): Promise<void> {
|
||||
// Exit if query is empty
|
||||
if (!query || query === "") return;
|
||||
|
||||
// Render user query as chat message
|
||||
this.renderMessage(query, "you");
|
||||
|
||||
// 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`;
|
||||
let responseElement = this.createKhojResponseDiv();
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
this.renderIncrementalMessage(responseElement, "🤔");
|
||||
|
||||
let response = await fetch(chatUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "text/event-stream"
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
if (response.body == null) {
|
||||
throw new Error("Response body is null");
|
||||
}
|
||||
// Clear thinking status message
|
||||
if (responseElement.innerHTML === "🤔") {
|
||||
responseElement.innerHTML = "";
|
||||
}
|
||||
|
||||
for await (const chunk of response.body) {
|
||||
const responseText = chunk.toString();
|
||||
if (responseText.startsWith("### compiled references:")) {
|
||||
return;
|
||||
}
|
||||
this.renderIncrementalMessage(responseElement, responseText);
|
||||
}
|
||||
} catch (err) {
|
||||
this.renderIncrementalMessage(responseElement, "Sorry, unable to get response from Khoj backend ❤️🩹. Contact developer for help at team@khoj.dev or <a href='https://discord.gg/BDgyabRM6e'>in Discord</a>")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Notice, Plugin } from 'obsidian';
|
||||
import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings'
|
||||
import { KhojModal } from 'src/modal'
|
||||
import { KhojSearchModal } from 'src/search_modal'
|
||||
import { KhojChatModal } from 'src/chat_modal'
|
||||
import { configureKhojBackend } from './utils';
|
||||
|
||||
|
||||
@@ -16,7 +17,7 @@ export default class Khoj extends Plugin {
|
||||
name: 'Search',
|
||||
checkCallback: (checking) => {
|
||||
if (!checking && this.settings.connectedToBackend)
|
||||
new KhojModal(this.app, this.settings).open();
|
||||
new KhojSearchModal(this.app, this.settings).open();
|
||||
return this.settings.connectedToBackend;
|
||||
}
|
||||
});
|
||||
@@ -24,19 +25,30 @@ export default class Khoj extends Plugin {
|
||||
// Add similar notes command. It can only be triggered from the editor
|
||||
this.addCommand({
|
||||
id: 'similar',
|
||||
name: 'Find Similar Notes',
|
||||
name: 'Find similar notes',
|
||||
editorCheckCallback: (checking) => {
|
||||
if (!checking && this.settings.connectedToBackend)
|
||||
new KhojModal(this.app, this.settings, true).open();
|
||||
new KhojSearchModal(this.app, this.settings, true).open();
|
||||
return this.settings.connectedToBackend;
|
||||
}
|
||||
});
|
||||
|
||||
// Add chat command. It can be triggered from anywhere
|
||||
this.addCommand({
|
||||
id: 'chat',
|
||||
name: 'Chat',
|
||||
checkCallback: (checking) => {
|
||||
if (!checking && this.settings.connectedToBackend && !!this.settings.openaiApiKey)
|
||||
new KhojChatModal(this.app, this.settings).open();
|
||||
return !!this.settings.openaiApiKey;
|
||||
}
|
||||
});
|
||||
|
||||
// Create an icon in the left ribbon.
|
||||
this.addRibbonIcon('search', 'Khoj', (_: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
this.settings.connectedToBackend
|
||||
? new KhojModal(this.app, this.settings).open()
|
||||
? new KhojSearchModal(this.app, this.settings).open()
|
||||
: new Notice(`❗️Ensure Khoj backend is running and Khoj URL is pointing to it in the plugin settings`);
|
||||
});
|
||||
|
||||
@@ -48,12 +60,16 @@ export default class Khoj extends Plugin {
|
||||
// Load khoj obsidian plugin settings
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
|
||||
// Load, configure khoj server settings
|
||||
await configureKhojBackend(this.settings);
|
||||
if (this.settings.autoConfigure) {
|
||||
// Load, configure khoj server settings
|
||||
await configureKhojBackend(this.app.vault, this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await configureKhojBackend(this.settings, false)
|
||||
.then(() => this.saveData(this.settings));
|
||||
if (this.settings.autoConfigure) {
|
||||
await configureKhojBackend(this.app.vault, this.settings, false);
|
||||
}
|
||||
this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import { App, SuggestModal, request, MarkdownRenderer, Instruction, Platform } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
|
||||
export interface SearchResult {
|
||||
entry: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
export class KhojModal extends SuggestModal<SearchResult> {
|
||||
setting: KhojSetting;
|
||||
rerank: boolean = false;
|
||||
find_similar_notes: boolean;
|
||||
|
||||
constructor(app: App, setting: KhojSetting, find_similar_notes: boolean = false) {
|
||||
super(app);
|
||||
this.setting = setting;
|
||||
this.find_similar_notes = find_similar_notes;
|
||||
|
||||
// Hide input element in Similar Notes mode
|
||||
this.inputEl.hidden = this.find_similar_notes;
|
||||
|
||||
// Register Modal Keybindings to Rerank Results
|
||||
this.scope.register(['Mod'], 'Enter', async () => {
|
||||
// Re-rank when explicitly triggered by user
|
||||
this.rerank = true
|
||||
// Trigger input event to get and render (reranked) results from khoj backend
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
// Rerank disabled by default to satisfy latency requirements for incremental search
|
||||
this.rerank = false
|
||||
});
|
||||
|
||||
// Add Hints to Modal for available Keybindings
|
||||
const modalInstructions: Instruction[] = [
|
||||
{
|
||||
command: '↑↓',
|
||||
purpose: 'to navigate',
|
||||
},
|
||||
{
|
||||
command: '↵',
|
||||
purpose: 'to open',
|
||||
},
|
||||
{
|
||||
command: Platform.isMacOS ? 'cmd ↵' : 'ctrl ↵',
|
||||
purpose: 'to rerank',
|
||||
},
|
||||
{
|
||||
command: 'esc',
|
||||
purpose: 'to dismiss',
|
||||
},
|
||||
]
|
||||
this.setInstructions(modalInstructions);
|
||||
|
||||
// Set Placeholder Text for Modal
|
||||
this.setPlaceholder('Search with Khoj 🦅...');
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
if (this.find_similar_notes) {
|
||||
// If markdown file is currently active
|
||||
let file = this.app.workspace.getActiveFile();
|
||||
if (file && file.extension === 'md') {
|
||||
// Enable rerank of search results
|
||||
this.rerank = true
|
||||
// Set contents of active markdown file to input element
|
||||
this.inputEl.value = await this.app.vault.read(file);
|
||||
// Trigger search to get and render similar notes from khoj backend
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
this.rerank = false
|
||||
}
|
||||
else {
|
||||
this.resultContainerEl.setText('Cannot find similar notes for non-markdown files');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getSuggestions(query: string): Promise<SearchResult[]> {
|
||||
// Query Khoj backend for search results
|
||||
let encodedQuery = encodeURIComponent(query);
|
||||
let searchUrl = `${this.setting.khojUrl}/api/search?q=${encodedQuery}&n=${this.setting.resultsCount}&r=${this.rerank}&t=markdown`
|
||||
let results = await request(searchUrl)
|
||||
.then(response => JSON.parse(response))
|
||||
.then(data => data
|
||||
.filter((result: any) => !this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path))
|
||||
.map((result: any) => { return { entry: result.entry, file: result.additional.file } as SearchResult; }));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async renderSuggestion(result: SearchResult, el: HTMLElement) {
|
||||
let words_to_render = 30;
|
||||
let entry_words = result.entry.split(' ')
|
||||
let entry_snipped_indicator = entry_words.length > words_to_render ? ' **...**' : '';
|
||||
let snipped_entry = entry_words.slice(0, words_to_render).join(' ');
|
||||
MarkdownRenderer.renderMarkdown(snipped_entry + entry_snipped_indicator, el, null, null);
|
||||
}
|
||||
|
||||
async onChooseSuggestion(result: SearchResult, _: MouseEvent | KeyboardEvent) {
|
||||
// Get all markdown files in vault
|
||||
const mdFiles = this.app.vault.getMarkdownFiles();
|
||||
|
||||
// Find the vault file matching file of result. Open file at result heading
|
||||
mdFiles
|
||||
// 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)
|
||||
.forEach((file) => {
|
||||
// Find best file match across operating systems
|
||||
// E.g Khoj Server on Linux, Obsidian Vault on Android
|
||||
if (result.file.endsWith(file.path)) {
|
||||
let resultHeading = result.entry.split('\n', 1)[0];
|
||||
let linkToEntry = `${file.path}${resultHeading}`
|
||||
this.app.workspace.openLinkText(linkToEntry, '');
|
||||
console.log(`Link: ${linkToEntry}, File: ${file.path}, Heading: ${resultHeading}`);
|
||||
return
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
169
src/interface/obsidian/src/search_modal.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { App, SuggestModal, request, MarkdownRenderer, Instruction, Platform } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import { createNoteAndCloseModal } from 'src/utils';
|
||||
|
||||
export interface SearchResult {
|
||||
entry: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
export class KhojSearchModal extends SuggestModal<SearchResult> {
|
||||
setting: KhojSetting;
|
||||
rerank: boolean = false;
|
||||
find_similar_notes: boolean;
|
||||
query: string = "";
|
||||
app: App;
|
||||
|
||||
constructor(app: App, setting: KhojSetting, find_similar_notes: boolean = false) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.setting = setting;
|
||||
this.find_similar_notes = find_similar_notes;
|
||||
|
||||
// Hide input element in Similar Notes mode
|
||||
this.inputEl.hidden = this.find_similar_notes;
|
||||
|
||||
// Register Modal Keybindings to Rerank Results
|
||||
this.scope.register(['Mod'], 'Enter', async () => {
|
||||
// Re-rank when explicitly triggered by user
|
||||
this.rerank = true
|
||||
// Trigger input event to get and render (reranked) results from khoj backend
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
// Rerank disabled by default to satisfy latency requirements for incremental search
|
||||
this.rerank = false
|
||||
});
|
||||
|
||||
// Register Modal Keybindings to Create New Note with Query as Title
|
||||
this.scope.register(['Shift'], 'Enter', async () => {
|
||||
if (this.query != "") createNoteAndCloseModal(this.query, this);
|
||||
});
|
||||
this.scope.register(['Ctrl', 'Shift'], 'Enter', async () => {
|
||||
if (this.query != "") createNoteAndCloseModal(this.query, this, { newLeaf: true });
|
||||
});
|
||||
|
||||
// Add Hints to Modal for available Keybindings
|
||||
const modalInstructions: Instruction[] = [
|
||||
{
|
||||
command: '↑↓',
|
||||
purpose: 'to navigate',
|
||||
},
|
||||
{
|
||||
command: '↵',
|
||||
purpose: 'to open',
|
||||
},
|
||||
{
|
||||
command: Platform.isMacOS ? 'cmd ↵' : 'ctrl ↵',
|
||||
purpose: 'to rerank',
|
||||
},
|
||||
{
|
||||
command: 'esc',
|
||||
purpose: 'to dismiss',
|
||||
},
|
||||
]
|
||||
this.setInstructions(modalInstructions);
|
||||
|
||||
// Set Placeholder Text for Modal
|
||||
this.setPlaceholder('Search with Khoj...');
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
if (this.find_similar_notes) {
|
||||
// If markdown file is currently active
|
||||
let file = this.app.workspace.getActiveFile();
|
||||
if (file && file.extension === 'md') {
|
||||
// Enable rerank of search results
|
||||
this.rerank = true
|
||||
// Set input element to contents of active markdown file
|
||||
// truncate to first 8,000 characters to avoid hitting query size limits
|
||||
this.inputEl.value = await this.app.vault.read(file).then(file_str => file_str.slice(0, 8000));
|
||||
// Trigger search to get and render similar notes from khoj backend
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
this.rerank = false
|
||||
}
|
||||
else {
|
||||
this.resultContainerEl.setText('Cannot find similar notes for non-markdown files');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getSuggestions(query: string): Promise<SearchResult[]> {
|
||||
// Query Khoj backend for search results
|
||||
let encodedQuery = encodeURIComponent(query);
|
||||
let searchUrl = `${this.setting.khojUrl}/api/search?q=${encodedQuery}&n=${this.setting.resultsCount}&r=${this.rerank}&client=obsidian`;
|
||||
|
||||
// Get search results for markdown and pdf files
|
||||
let mdResponse = await request(`${searchUrl}&t=markdown`);
|
||||
let pdfResponse = await request(`${searchUrl}&t=pdf`);
|
||||
|
||||
// Parse search results
|
||||
let mdData = JSON.parse(mdResponse)
|
||||
.filter((result: any) => !this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path))
|
||||
.map((result: any) => { return { entry: result.entry, score: result.score, file: result.additional.file }; });
|
||||
let pdfData = JSON.parse(pdfResponse)
|
||||
.filter((result: any) => !this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path))
|
||||
.map((result: any) => { return { entry: `## ${result.additional.compiled}`, score: result.score, file: result.additional.file } as SearchResult; })
|
||||
|
||||
// Combine markdown and PDF results and sort them by score
|
||||
let results = mdData.concat(pdfData)
|
||||
.sort((a: any, b: any) => b.score - a.score)
|
||||
.map((result: any) => { return { entry: result.entry, file: result.file } as SearchResult; })
|
||||
|
||||
this.query = query;
|
||||
return results;
|
||||
}
|
||||
|
||||
async renderSuggestion(result: SearchResult, el: HTMLElement) {
|
||||
// Max number of lines to render
|
||||
let lines_to_render = 8;
|
||||
|
||||
// Extract filename of result
|
||||
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 reindex hint on first search result
|
||||
if (this.resultContainerEl.children.length == 1) {
|
||||
let infoHintEl = createEl("div",{ cls: 'khoj-info-hint' });
|
||||
el.insertAdjacentElement("beforebegin", infoHintEl);
|
||||
setTimeout(() => {
|
||||
infoHintEl.setText('Unexpected results? Try re-index your vault from the Khoj plugin settings to fix it.');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 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' })
|
||||
|
||||
// @ts-ignore
|
||||
MarkdownRenderer.renderMarkdown(snipped_entry + entry_snipped_indicator, result_el, null, null);
|
||||
}
|
||||
|
||||
async onChooseSuggestion(result: SearchResult, _: MouseEvent | KeyboardEvent) {
|
||||
// Get all markdown and PDF files in vault
|
||||
const mdFiles = this.app.vault.getMarkdownFiles();
|
||||
const pdfFiles = this.app.vault.getFiles().filter(file => file.extension === 'pdf');
|
||||
|
||||
// Find the vault file matching file of chosen search result
|
||||
let file_match = mdFiles.concat(pdfFiles)
|
||||
// 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
|
||||
.find(file => result.file.replace(/\\/g, "/").endsWith(file.path))
|
||||
|
||||
// Open vault file at heading of chosen search result
|
||||
if (file_match) {
|
||||
let resultHeading = file_match.extension !== 'pdf' ? result.entry.split('\n', 1)[0] : '';
|
||||
let linkToEntry = resultHeading.startsWith('#') ? `${file_match.path}${resultHeading}` : file_match.path;
|
||||
this.app.workspace.openLinkText(linkToEntry, '');
|
||||
console.log(`Link: ${linkToEntry}, File: ${file_match.path}, Heading: ${resultHeading}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,19 @@ import { App, Notice, PluginSettingTab, request, Setting } from 'obsidian';
|
||||
import Khoj from 'src/main';
|
||||
|
||||
export interface KhojSetting {
|
||||
openaiApiKey: string;
|
||||
resultsCount: number;
|
||||
khojUrl: string;
|
||||
connectedToBackend: boolean;
|
||||
autoConfigure: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: KhojSetting = {
|
||||
resultsCount: 6,
|
||||
khojUrl: 'http://localhost:8000',
|
||||
khojUrl: 'http://127.0.0.1:8000',
|
||||
connectedToBackend: false,
|
||||
autoConfigure: true,
|
||||
openaiApiKey: '',
|
||||
}
|
||||
|
||||
export class KhojSettingTab extends PluginSettingTab {
|
||||
@@ -36,12 +40,21 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
.setValue(`${this.plugin.settings.khojUrl}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.khojUrl = value.trim();
|
||||
await this.plugin.saveSettings()
|
||||
.finally(() => containerEl.firstElementChild?.setText(this.getBackendStatusMessage()));
|
||||
await this.plugin.saveSettings();
|
||||
containerEl.firstElementChild?.setText(this.getBackendStatusMessage());
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
new Setting(containerEl)
|
||||
.setName('OpenAI API Key')
|
||||
.setDesc('Your OpenAI API Key for Khoj Chat')
|
||||
.addText(text => text
|
||||
.setValue(`${this.plugin.settings.openaiApiKey}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.openaiApiKey = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName('Results Count')
|
||||
.setDesc('The number of search results to show')
|
||||
.setDesc('The number of results to show in search and use for chat')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 10, 1)
|
||||
.setValue(this.plugin.settings.resultsCount)
|
||||
@@ -50,6 +63,15 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.resultsCount = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName('Auto Configure')
|
||||
.setDesc('Automatically configure the Khoj backend')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoConfigure)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoConfigure = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
let indexVaultSetting = new Setting(containerEl);
|
||||
indexVaultSetting
|
||||
.setName('Index Vault')
|
||||
@@ -59,16 +81,40 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
// Disable button while updating index
|
||||
button.setButtonText('Updating...');
|
||||
button.removeCta()
|
||||
button.setButtonText('Updating 🌑');
|
||||
button.removeCta();
|
||||
indexVaultSetting = indexVaultSetting.setDisabled(true);
|
||||
|
||||
await request(`${this.plugin.settings.khojUrl}/api/update?t=markdown&force=true`)
|
||||
.then(() => new Notice('✅ Updated Khoj index.'));
|
||||
// Show indicator for indexing in progress
|
||||
const progress_indicator = window.setInterval(() => {
|
||||
if (button.buttonEl.innerText === 'Updating 🌑') {
|
||||
button.setButtonText('Updating 🌘');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌘') {
|
||||
button.setButtonText('Updating 🌗');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌗') {
|
||||
button.setButtonText('Updating 🌖');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌖') {
|
||||
button.setButtonText('Updating 🌕');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌕') {
|
||||
button.setButtonText('Updating 🌔');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌔') {
|
||||
button.setButtonText('Updating 🌓');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌓') {
|
||||
button.setButtonText('Updating 🌒');
|
||||
} else if (button.buttonEl.innerText === 'Updating 🌒') {
|
||||
button.setButtonText('Updating 🌑');
|
||||
}
|
||||
}, 300);
|
||||
this.plugin.registerInterval(progress_indicator);
|
||||
|
||||
// Re-enable button once index is updated
|
||||
await request(`${this.plugin.settings.khojUrl}/api/update?t=markdown&force=true&client=obsidian`);
|
||||
await request(`${this.plugin.settings.khojUrl}/api/update?t=pdf&force=true&client=obsidian`);
|
||||
new Notice('✅ Updated Khoj index.');
|
||||
|
||||
// Reset button once index is updated
|
||||
window.clearInterval(progress_indicator);
|
||||
button.setButtonText('Update');
|
||||
button.setCta()
|
||||
button.setCta();
|
||||
indexVaultSetting = indexVaultSetting.setDisabled(false);
|
||||
})
|
||||
);
|
||||
@@ -76,7 +122,7 @@ export class KhojSettingTab extends PluginSettingTab {
|
||||
|
||||
getBackendStatusMessage() {
|
||||
return !this.plugin.settings.connectedToBackend
|
||||
? '❗Disconnected from Khoj backend. Ensure Khoj backend is running and Khoj URL is correctly set below.'
|
||||
: '✅ Connected to Khoj backend.';
|
||||
? '❗Disconnected from Khoj backend. Ensure Khoj backend is running and Khoj URL is correctly set below.'
|
||||
: '✅ Connected to Khoj backend.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { FileSystemAdapter, Notice, RequestUrlParam, request } from 'obsidian';
|
||||
import { FileSystemAdapter, Notice, RequestUrlParam, request, Vault, Modal } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings'
|
||||
|
||||
export function getVaultAbsolutePath(): string {
|
||||
let adaptor = this.app.vault.adapter;
|
||||
export function getVaultAbsolutePath(vault: Vault): string {
|
||||
let adaptor = vault.adapter;
|
||||
if (adaptor instanceof FileSystemAdapter) {
|
||||
return adaptor.getBasePath();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function configureKhojBackend(setting: KhojSetting, notify: boolean = true) {
|
||||
let mdInVault = `${getVaultAbsolutePath()}/**/*.md`;
|
||||
export async function configureKhojBackend(vault: Vault, setting: KhojSetting, notify: boolean = true) {
|
||||
let vaultPath = getVaultAbsolutePath(vault);
|
||||
let mdInVault = `${vaultPath}/**/*.md`;
|
||||
let pdfInVault = `${vaultPath}/**/*.pdf`;
|
||||
let khojConfigUrl = `${setting.khojUrl}/api/config/data`;
|
||||
|
||||
// Check if khoj backend is configured, note if cannot connect to backend
|
||||
@@ -28,11 +30,13 @@ export async function configureKhojBackend(setting: KhojSetting, notify: boolean
|
||||
if (!setting.connectedToBackend) return;
|
||||
|
||||
// Set index name from the path of the current vault
|
||||
let indexName = getVaultAbsolutePath().replace(/\//g, '_').replace(/ /g, '_');
|
||||
// Get default index directory from khoj backend
|
||||
let khojDefaultIndexDirectory = await request(`${khojConfigUrl}/default`)
|
||||
.then(response => JSON.parse(response))
|
||||
.then(data => { return getIndexDirectoryFromBackendConfig(data); });
|
||||
let indexName = vaultPath.replace(/\//g, '_').replace(/\\/g, '_').replace(/ /g, '_').replace(/:/g, '_');
|
||||
// Get default config fields from khoj backend
|
||||
let defaultConfig = await request(`${khojConfigUrl}/default`).then(response => JSON.parse(response));
|
||||
let khojDefaultMdIndexDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["content-type"]["markdown"]["embeddings-file"]);
|
||||
let khojDefaultPdfIndexDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["content-type"]["pdf"]["embeddings-file"]);
|
||||
let khojDefaultChatDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["processor"]["conversation"]["conversation-logfile"]);
|
||||
let khojDefaultChatModelName = defaultConfig["processor"]["conversation"]["model"];
|
||||
|
||||
// Get current config if khoj backend configured, else get default config from khoj backend
|
||||
await request(khoj_already_configured ? khojConfigUrl : `${khojConfigUrl}/default`)
|
||||
@@ -45,18 +49,22 @@ export async function configureKhojBackend(setting: KhojSetting, notify: boolean
|
||||
"markdown": {
|
||||
"input-filter": [mdInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojDefaultIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
"embeddings-file": `${khojDefaultMdIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultMdIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
}
|
||||
// Disable khoj processors, as not required
|
||||
delete data["processor"];
|
||||
|
||||
// Save new config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
console.log(`Khoj: Created khoj backend config:\n${JSON.stringify(data)}`)
|
||||
const hasPdfFiles = app.vault.getFiles().some(file => file.extension === 'pdf');
|
||||
|
||||
if (hasPdfFiles) {
|
||||
data["content-type"]["pdf"] = {
|
||||
"input-filter": [pdfInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojDefaultPdfIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultPdfIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Else if khoj config has no markdown content config
|
||||
else if (!data["content-type"]["markdown"]) {
|
||||
// Add markdown config to khoj content-type config
|
||||
@@ -64,31 +72,104 @@ export async function configureKhojBackend(setting: KhojSetting, notify: boolean
|
||||
data["content-type"]["markdown"] = {
|
||||
"input-filter": [mdInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojDefaultIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
"embeddings-file": `${khojDefaultMdIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultMdIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
|
||||
// Save updated config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
console.log(`Khoj: Added markdown config to khoj backend config:\n${JSON.stringify(data["content-type"])}`)
|
||||
}
|
||||
|
||||
// Else if khoj is not configured to index markdown files in configured obsidian vault
|
||||
else if (data["content-type"]["markdown"]["input-filter"].length != 1 ||
|
||||
else if (
|
||||
data["content-type"]["markdown"]["input-files"] != null ||
|
||||
data["content-type"]["markdown"]["input-filter"] == null ||
|
||||
data["content-type"]["markdown"]["input-filter"].length != 1 ||
|
||||
data["content-type"]["markdown"]["input-filter"][0] !== mdInVault) {
|
||||
// Update markdown config in khoj content-type config
|
||||
// Set markdown config to only index markdown files in configured obsidian vault
|
||||
let khojIndexDirectory = getIndexDirectoryFromBackendConfig(data);
|
||||
data["content-type"]["markdown"] = {
|
||||
"input-filter": [mdInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
// Save updated config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
console.log(`Khoj: Updated markdown config in khoj backend config:\n${JSON.stringify(data["content-type"]["markdown"])}`)
|
||||
// Update markdown config in khoj content-type config
|
||||
// Set markdown config to only index markdown files in configured obsidian vault
|
||||
let khojMdIndexDirectory = getIndexDirectoryFromBackendConfig(data["content-type"]["markdown"]["embeddings-file"]);
|
||||
data["content-type"]["markdown"] = {
|
||||
"input-filter": [mdInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojMdIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojMdIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
}
|
||||
|
||||
if (khoj_already_configured && !data["content-type"]["pdf"]) {
|
||||
const hasPdfFiles = app.vault.getFiles().some(file => file.extension === 'pdf');
|
||||
|
||||
if (hasPdfFiles) {
|
||||
data["content-type"]["pdf"] = {
|
||||
"input-filter": [pdfInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojDefaultPdfIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojDefaultPdfIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
} else {
|
||||
data["content-type"]["pdf"] = null;
|
||||
}
|
||||
}
|
||||
// Else if khoj is not configured to index pdf files in configured obsidian vault
|
||||
else if (khoj_already_configured &&
|
||||
(
|
||||
data["content-type"]["pdf"]["input-files"] != null ||
|
||||
data["content-type"]["pdf"]["input-filter"] == null ||
|
||||
data["content-type"]["pdf"]["input-filter"].length != 1 ||
|
||||
data["content-type"]["pdf"]["input-filter"][0] !== pdfInVault)) {
|
||||
|
||||
let hasPdfFiles = app.vault.getFiles().some(file => file.extension === 'pdf');
|
||||
|
||||
if (hasPdfFiles) {
|
||||
// Update pdf config in khoj content-type config
|
||||
// Set pdf config to only index pdf files in configured obsidian vault
|
||||
let khojPdfIndexDirectory = getIndexDirectoryFromBackendConfig(data["content-type"]["pdf"]["embeddings-file"]);
|
||||
data["content-type"]["pdf"] = {
|
||||
"input-filter": [pdfInVault],
|
||||
"input-files": null,
|
||||
"embeddings-file": `${khojPdfIndexDirectory}/${indexName}.pt`,
|
||||
"compressed-jsonl": `${khojPdfIndexDirectory}/${indexName}.jsonl.gz`,
|
||||
}
|
||||
} else {
|
||||
data["content-type"]["pdf"] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If OpenAI API key not set in Khoj plugin settings
|
||||
if (!setting.openaiApiKey) {
|
||||
// Disable khoj processors, as not required
|
||||
delete data["processor"];
|
||||
}
|
||||
// Else if khoj backend not configured yet
|
||||
else if (!khoj_already_configured || !data["processor"]) {
|
||||
data["processor"] = {
|
||||
"conversation": {
|
||||
"conversation-logfile": `${khojDefaultChatDirectory}/conversation.json`,
|
||||
"model": khojDefaultChatModelName,
|
||||
"openai-api-key": setting.openaiApiKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Else if khoj config has no conversation processor config
|
||||
else if (!data["processor"]["conversation"]) {
|
||||
data["processor"]["conversation"] = {
|
||||
"conversation-logfile": `${khojDefaultChatDirectory}/conversation.json`,
|
||||
"model": khojDefaultChatModelName,
|
||||
"openai-api-key": setting.openaiApiKey,
|
||||
}
|
||||
}
|
||||
// Else if khoj is not configured with OpenAI API key from khoj plugin settings
|
||||
else if (data["processor"]["conversation"]["openai-api-key"] !== setting.openaiApiKey) {
|
||||
data["processor"]["conversation"] = {
|
||||
"conversation-logfile": data["processor"]["conversation"]["conversation-logfile"],
|
||||
"model": data["processor"]["conversation"]["model"],
|
||||
"openai-api-key": setting.openaiApiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated config and refresh index on khoj backend
|
||||
updateKhojBackend(setting.khojUrl, data);
|
||||
if (!khoj_already_configured)
|
||||
console.log(`Khoj: Created khoj backend config:\n${JSON.stringify(data)}`)
|
||||
else
|
||||
console.log(`Khoj: Updated khoj backend config:\n${JSON.stringify(data)}`)
|
||||
})
|
||||
.catch(error => {
|
||||
if (notify)
|
||||
@@ -108,9 +189,43 @@ export async function updateKhojBackend(khojUrl: string, khojConfig: Object) {
|
||||
// Save khojConfig on khoj backend at khojConfigUrl
|
||||
await request(requestContent)
|
||||
// Refresh khoj search index after updating config
|
||||
.then(_ => request(`${khojUrl}/api/update?t=markdown`));
|
||||
.then(_ => request(`${khojUrl}/api/update?t=markdown`))
|
||||
.then(_ => request(`${khojUrl}/api/update?t=pdf`));
|
||||
}
|
||||
|
||||
function getIndexDirectoryFromBackendConfig(khojConfig: any) {
|
||||
return khojConfig["content-type"]["markdown"]["embeddings-file"].split("/").slice(0, -1).join("/");
|
||||
}
|
||||
function getIndexDirectoryFromBackendConfig(filepath: string) {
|
||||
return filepath.split("/").slice(0, -1).join("/");
|
||||
}
|
||||
|
||||
export async function createNote(name: string, newLeaf = false): Promise<void> {
|
||||
try {
|
||||
let pathPrefix: string
|
||||
// @ts-ignore
|
||||
switch (app.vault.getConfig('newFileLocation')) {
|
||||
case 'current':
|
||||
pathPrefix = (app.workspace.getActiveFile()?.parent.path ?? '') + '/'
|
||||
break
|
||||
case 'folder':
|
||||
pathPrefix = this.app.vault.getConfig('newFileFolderPath') + '/'
|
||||
break
|
||||
default: // 'root'
|
||||
pathPrefix = ''
|
||||
break
|
||||
}
|
||||
await app.workspace.openLinkText(`${pathPrefix}${name}.md`, '', newLeaf)
|
||||
} catch (e) {
|
||||
console.error('Khoj: Could not create note.\n' + (e as any).message);
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function createNoteAndCloseModal(query: string, modal: Modal, opt?: { newLeaf: boolean }): Promise<void> {
|
||||
try {
|
||||
await createNote(query, opt?.newLeaf);
|
||||
}
|
||||
catch (e) {
|
||||
new Notice((e as Error).message)
|
||||
return
|
||||
}
|
||||
modal.close();
|
||||
}
|
||||
|
||||
@@ -6,3 +6,179 @@ available in the app when your plugin is enabled.
|
||||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
||||
|
||||
:root {
|
||||
--khoj-chat-primary: #ffb300;
|
||||
--khoj-chat-dark-grey: #475569;
|
||||
}
|
||||
|
||||
.khoj-chat {
|
||||
display: grid;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
text-align: center;
|
||||
font-family: roboto, karma, segoe ui, sans-serif;
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.khoj-chat > * {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#khoj-chat-title {
|
||||
font-weight: 200;
|
||||
color: var(--khoj-chat-primary);
|
||||
}
|
||||
|
||||
#khoj-chat-body {
|
||||
font-size: var(--font-ui-medium);
|
||||
margin: 0px;
|
||||
line-height: 20px;
|
||||
overflow-y: scroll; /* Make chat body scroll to see history */
|
||||
}
|
||||
/* add chat metatdata to bottom of bubble */
|
||||
.khoj-chat-message::after {
|
||||
content: attr(data-meta);
|
||||
display: block;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
margin: -12px 7px 0 -5px;
|
||||
}
|
||||
/* move message by khoj to left */
|
||||
.khoj-chat-message.khoj {
|
||||
margin-left: auto;
|
||||
text-align: left;
|
||||
}
|
||||
/* move message by you to right */
|
||||
.khoj-chat-message.you {
|
||||
margin-right: auto;
|
||||
text-align: right;
|
||||
}
|
||||
/* basic style chat message text */
|
||||
.khoj-chat-message-text {
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
text-align: left;
|
||||
}
|
||||
/* color chat bubble by khoj blue */
|
||||
.khoj-chat-message-text.khoj {
|
||||
color: var(--text-on-accent);
|
||||
background: var(--khoj-chat-primary);
|
||||
margin-left: auto;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* add left protrusion to khoj chat bubble */
|
||||
.khoj-chat-message-text.khoj:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: -7px;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: var(--khoj-chat-primary);
|
||||
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-chat-dark-grey);
|
||||
margin-right: auto;
|
||||
}
|
||||
/* add right protrusion to you chat bubble */
|
||||
.khoj-chat-message-text.you:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 91%;
|
||||
right: -2px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: var(--khoj-chat-dark-grey);
|
||||
border-right: 0;
|
||||
margin-top: -10px;
|
||||
transform: rotate(-60deg)
|
||||
}
|
||||
|
||||
#khoj-chat-footer {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(70px, 100%);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
#khoj-chat-footer > * {
|
||||
padding: 15px;
|
||||
background: #f9fafc
|
||||
}
|
||||
#khoj-chat-input.option:hover {
|
||||
box-shadow: 0 0 11px var(--background-modifier-box-shadow);
|
||||
}
|
||||
#khoj-chat-input {
|
||||
font-size: var(--font-ui-medium);
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse), (hover: none) {
|
||||
#khoj-chat-body.abbr[title] {
|
||||
position: relative;
|
||||
padding-left: 4px; /* space references out to ease tapping */
|
||||
}
|
||||
#khoj-chat-body.abbr[title]:focus:after {
|
||||
content: attr(title);
|
||||
|
||||
/* position tooltip */
|
||||
position: absolute;
|
||||
left: 16px; /* open tooltip to right of ref link, instead of on top of it */
|
||||
width: auto;
|
||||
z-index: 1; /* show tooltip above chat messages */
|
||||
|
||||
/* style tooltip */
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-muted);
|
||||
border-radius: 2px;
|
||||
box-shadow: 1px 1px 4px 0 var(--background-modifier-box-shadow);
|
||||
font-size: var(--font-ui-small);
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.khoj-result-file {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.khoj-result-entry {
|
||||
color: var(--text-muted);
|
||||
margin-left: 2em;
|
||||
padding-left: 0.5em;
|
||||
line-height: normal;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
border-left-style: solid;
|
||||
border-left-color: var(--color-accent-2);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.khoj-result-entry > * {
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
.khoj-result-entry > p {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.khoj-result-entry p br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.khoj-info-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"0.2.1": "0.15.0",
|
||||
"0.2.5": "0.15.0"
|
||||
"0.2.1": "0.15.0",
|
||||
"0.2.5": "0.15.0",
|
||||
"0.2.6": "0.15.0",
|
||||
"0.3.0": "0.15.0",
|
||||
"0.4.0": "0.15.0",
|
||||
"0.5.0": "0.15.0",
|
||||
"0.6.0": "0.15.0",
|
||||
"0.6.1": "0.15.0",
|
||||
"0.6.2": "0.15.0",
|
||||
"0.7.0": "0.15.0",
|
||||
"0.7.1": "0.15.0",
|
||||
"0.8.0": "0.15.0"
|
||||
}
|
||||
|
||||
609
src/interface/obsidian/yarn.lock
Normal file
@@ -0,0 +1,609 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
|
||||
"@nodelib/fs.walk@^1.2.3":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
|
||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
||||
dependencies:
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@types/codemirror@0.0.108":
|
||||
version "0.0.108"
|
||||
resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz"
|
||||
integrity sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw==
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/estree@*":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz"
|
||||
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
version "7.0.11"
|
||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/node-fetch@^2.6.4":
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660"
|
||||
integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "20.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6"
|
||||
integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==
|
||||
|
||||
"@types/node@^16.11.6":
|
||||
version "16.18.12"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-16.18.12.tgz"
|
||||
integrity sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==
|
||||
|
||||
"@types/tern@*":
|
||||
version "0.23.4"
|
||||
resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz"
|
||||
integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz"
|
||||
integrity sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.29.0"
|
||||
"@typescript-eslint/type-utils" "5.29.0"
|
||||
"@typescript-eslint/utils" "5.29.0"
|
||||
debug "^4.3.4"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
ignore "^5.2.0"
|
||||
regexpp "^3.2.0"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/parser@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz"
|
||||
integrity sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.29.0"
|
||||
"@typescript-eslint/types" "5.29.0"
|
||||
"@typescript-eslint/typescript-estree" "5.29.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz"
|
||||
integrity sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.29.0"
|
||||
"@typescript-eslint/visitor-keys" "5.29.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz"
|
||||
integrity sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.29.0"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz"
|
||||
integrity sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz"
|
||||
integrity sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.29.0"
|
||||
"@typescript-eslint/visitor-keys" "5.29.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz"
|
||||
integrity sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@typescript-eslint/scope-manager" "5.29.0"
|
||||
"@typescript-eslint/types" "5.29.0"
|
||||
"@typescript-eslint/typescript-estree" "5.29.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.29.0":
|
||||
version "5.29.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz"
|
||||
integrity sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.29.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
|
||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
braces@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
builtin-modules@3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz"
|
||||
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
data-uri-to-buffer@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
|
||||
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
|
||||
|
||||
debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
|
||||
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
esbuild-android-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz#ef95b42c67bcf4268c869153fa3ad1466c4cea6b"
|
||||
integrity sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==
|
||||
|
||||
esbuild-android-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz#4ebd7ce9fb250b4695faa3ee46fd3b0754ecd9e6"
|
||||
integrity sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==
|
||||
|
||||
esbuild-darwin-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz#e0da6c244f497192f951807f003f6a423ed23188"
|
||||
integrity sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==
|
||||
|
||||
esbuild-darwin-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz"
|
||||
integrity sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==
|
||||
|
||||
esbuild-freebsd-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz#8da6a14c095b29c01fc8087a16cb7906debc2d67"
|
||||
integrity sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==
|
||||
|
||||
esbuild-freebsd-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz#ad31f9c92817ff8f33fd253af7ab5122dc1b83f6"
|
||||
integrity sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==
|
||||
|
||||
esbuild-linux-32@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz#de085e4db2e692ea30c71208ccc23fdcf5196c58"
|
||||
integrity sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==
|
||||
|
||||
esbuild-linux-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz#2a9321bbccb01f01b04cebfcfccbabeba3658ba1"
|
||||
integrity sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==
|
||||
|
||||
esbuild-linux-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz#b9da7b6fc4b0ca7a13363a0c5b7bb927e4bc535a"
|
||||
integrity sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==
|
||||
|
||||
esbuild-linux-arm@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz#56fec2a09b9561c337059d4af53625142aded853"
|
||||
integrity sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==
|
||||
|
||||
esbuild-linux-mips64le@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz#9db21561f8f22ed79ef2aedb7bbef082b46cf823"
|
||||
integrity sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==
|
||||
|
||||
esbuild-linux-ppc64le@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz#dc3a3da321222b11e96e50efafec9d2de408198b"
|
||||
integrity sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==
|
||||
|
||||
esbuild-linux-riscv64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz#9bd6dcd3dca6c0357084ecd06e1d2d4bf105335f"
|
||||
integrity sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==
|
||||
|
||||
esbuild-linux-s390x@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz#a458af939b52f2cd32fc561410d441a51f69d41f"
|
||||
integrity sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==
|
||||
|
||||
esbuild-netbsd-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz#6388e785d7e7e4420cb01348d7483ab511b16aa8"
|
||||
integrity sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==
|
||||
|
||||
esbuild-openbsd-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz#309af806db561aa886c445344d1aacab850dbdc5"
|
||||
integrity sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==
|
||||
|
||||
esbuild-sunos-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz#3f19612dcdb89ba6c65283a7ff6e16f8afbf8aaa"
|
||||
integrity sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==
|
||||
|
||||
esbuild-windows-32@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz#a92d279c8458d5dc319abcfeb30aa49e8f2e6f7f"
|
||||
integrity sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==
|
||||
|
||||
esbuild-windows-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz#2564c3fcf0c23d701edb71af8c52d3be4cec5f8a"
|
||||
integrity sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==
|
||||
|
||||
esbuild-windows-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz#86d9db1a22d83360f726ac5fba41c2f625db6878"
|
||||
integrity sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==
|
||||
|
||||
esbuild@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz"
|
||||
integrity sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==
|
||||
optionalDependencies:
|
||||
esbuild-android-64 "0.14.47"
|
||||
esbuild-android-arm64 "0.14.47"
|
||||
esbuild-darwin-64 "0.14.47"
|
||||
esbuild-darwin-arm64 "0.14.47"
|
||||
esbuild-freebsd-64 "0.14.47"
|
||||
esbuild-freebsd-arm64 "0.14.47"
|
||||
esbuild-linux-32 "0.14.47"
|
||||
esbuild-linux-64 "0.14.47"
|
||||
esbuild-linux-arm "0.14.47"
|
||||
esbuild-linux-arm64 "0.14.47"
|
||||
esbuild-linux-mips64le "0.14.47"
|
||||
esbuild-linux-ppc64le "0.14.47"
|
||||
esbuild-linux-riscv64 "0.14.47"
|
||||
esbuild-linux-s390x "0.14.47"
|
||||
esbuild-netbsd-64 "0.14.47"
|
||||
esbuild-openbsd-64 "0.14.47"
|
||||
esbuild-sunos-64 "0.14.47"
|
||||
esbuild-windows-32 "0.14.47"
|
||||
esbuild-windows-64 "0.14.47"
|
||||
esbuild-windows-arm64 "0.14.47"
|
||||
|
||||
eslint-scope@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
|
||||
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-utils@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz"
|
||||
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
eslint-visitor-keys@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz"
|
||||
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
|
||||
|
||||
eslint-visitor-keys@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
esrecurse@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
|
||||
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
|
||||
dependencies:
|
||||
estraverse "^5.2.0"
|
||||
|
||||
estraverse@^4.1.1:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
estraverse@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
version "3.2.12"
|
||||
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz"
|
||||
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz"
|
||||
integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
||||
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
|
||||
dependencies:
|
||||
node-domexception "^1.0.0"
|
||||
web-streams-polyfill "^3.0.3"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
|
||||
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
|
||||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
|
||||
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
|
||||
|
||||
glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
globby@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz"
|
||||
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
|
||||
dependencies:
|
||||
array-union "^2.1.0"
|
||||
dir-glob "^3.0.1"
|
||||
fast-glob "^3.2.9"
|
||||
ignore "^5.2.0"
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
|
||||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||
|
||||
is-glob@^4.0.1, is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-number@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
merge2@^1.3.0, merge2@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
dependencies:
|
||||
braces "^3.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
moment@2.29.4:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||
|
||||
node-fetch@^3.1.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.1.tgz#b3eea7b54b3a48020e46f4f88b9c5a7430d20b2e"
|
||||
integrity sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==
|
||||
dependencies:
|
||||
data-uri-to-buffer "^4.0.0"
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
obsidian@latest:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/obsidian/-/obsidian-1.1.1.tgz"
|
||||
integrity sha512-GcxhsHNkPEkwHEjeyitfYNBcQuYGeAHFs1pEpZIv0CnzSfui8p8bPLm2YKLgcg20B764770B1sYGtxCvk9ptxg==
|
||||
dependencies:
|
||||
"@types/codemirror" "0.0.108"
|
||||
moment "2.29.4"
|
||||
|
||||
path-type@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
regexpp@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz"
|
||||
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
|
||||
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
semver@^7.3.7:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
|
||||
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tslib@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
typescript@4.7.4:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
|
||||
web-streams-polyfill@^3.0.3:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
@@ -1,29 +0,0 @@
|
||||
:root {
|
||||
--primary-color: #ffffff;
|
||||
--bold-color: #2073ee;
|
||||
--complementary-color: #124408;
|
||||
--accent-color-0: #57f0b5;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
div.config-element {
|
||||
color: var(--bold-color);
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
div.config-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.config-element-value {
|
||||
color: var(--complementary-color);
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// Retrieve elements from the DOM.
|
||||
var showConfig = document.getElementById("show-config");
|
||||
var configForm = document.getElementById("config-form");
|
||||
var regenerateButton = document.getElementById("config-regenerate");
|
||||
|
||||
// Global variables.
|
||||
var rawConfig = {};
|
||||
var emptyValueDefault = "🖊️";
|
||||
|
||||
/**
|
||||
* Fetch the existing config file.
|
||||
*/
|
||||
fetch("/api/config/data")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
rawConfig = data;
|
||||
configForm.style.display = "block";
|
||||
processChildren(configForm, data);
|
||||
|
||||
var submitButton = document.createElement("button");
|
||||
submitButton.type = "submit";
|
||||
submitButton.innerHTML = "update";
|
||||
configForm.appendChild(submitButton);
|
||||
|
||||
// The config form's submit handler.
|
||||
configForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
console.log(rawConfig);
|
||||
fetch("/api/config/data", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(rawConfig)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log(data));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The click handler for the Regenerate button.
|
||||
*/
|
||||
regenerateButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
regenerateButton.style.cursor = "progress";
|
||||
regenerateButton.disabled = true;
|
||||
fetch("/api/update?force=true")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
regenerateButton.style.cursor = "pointer";
|
||||
regenerateButton.disabled = false;
|
||||
console.log(data);
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* Adds config elements to the DOM representing the sub-components
|
||||
* of one of the fields in the raw config file.
|
||||
* @param {the parent element} element
|
||||
* @param {the data to be rendered for this element and its children} data
|
||||
*/
|
||||
function processChildren(element, data) {
|
||||
for (let key in data) {
|
||||
var child = document.createElement("div");
|
||||
child.id = key;
|
||||
child.className = "config-element";
|
||||
child.appendChild(document.createTextNode(key + ": "));
|
||||
if (data[key] === Object(data[key]) && !Array.isArray(data[key])) {
|
||||
child.className+=" config-title";
|
||||
processChildren(child, data[key]);
|
||||
} else {
|
||||
child.appendChild(createValueNode(data, key));
|
||||
}
|
||||
element.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an element, and replaces it with an editable
|
||||
* element with the same data in place.
|
||||
* @param {the original element to be replaced} original
|
||||
* @param {the source data to be rendered for the new element} data
|
||||
* @param {the key for this input in the source data} key
|
||||
*/
|
||||
function makeElementEditable(original, data, key) {
|
||||
original.addEventListener("click", () => {
|
||||
var inputNewText = document.createElement("input");
|
||||
inputNewText.type = "text";
|
||||
inputNewText.className = "config-element-edit";
|
||||
inputNewText.value = (original.textContent == emptyValueDefault) ? "" : original.textContent;
|
||||
fixInputOnFocusOut(inputNewText, data, key);
|
||||
original.parentNode.replaceChild(inputNewText, original);
|
||||
inputNewText.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a node corresponding to the value of a config element.
|
||||
* @param {the source data} data
|
||||
* @param {the key corresponding to this node's data} key
|
||||
* @returns A new element which corresponds to the value in some field.
|
||||
*/
|
||||
function createValueNode(data, key) {
|
||||
var valueElement = document.createElement("span");
|
||||
valueElement.className = "config-element-value";
|
||||
valueElement.textContent = !data[key] ? emptyValueDefault : data[key];
|
||||
makeElementEditable(valueElement, data, key);
|
||||
return valueElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an existing input element with an element with the same data, which is not an input.
|
||||
* If the input data for this element was changed, update the corresponding data in the raw config.
|
||||
* @param {the original element to be replaced} original
|
||||
* @param {the source data} data
|
||||
* @param {the key corresponding to this node's data} key
|
||||
*/
|
||||
function fixInputOnFocusOut(original, data, key) {
|
||||
original.addEventListener("blur", () => {
|
||||
data[key] = (original.value != emptyValueDefault) ? original.value : "";
|
||||
original.parentNode.replaceChild(createValueNode(data, key), original);
|
||||
})
|
||||
}
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,261 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<title>Khoj</title>
|
||||
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 144 144%22><text y=%22.86em%22 font-size=%22144%22>🦅</text></svg>">
|
||||
<link rel="icon" type="image/png" sizes="144x144" href="/static/assets/icons/favicon-144x144.png">
|
||||
<link rel="manifest" href="/static/khoj.webmanifest">
|
||||
</head>
|
||||
<script>
|
||||
function setTypeFieldInUrl(type) {
|
||||
let url = new URL(window.location.href);
|
||||
url.searchParams.set("t", type.value);
|
||||
window.history.pushState({}, "", url.href);
|
||||
}
|
||||
|
||||
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 renderMessage(message, by, dt=null) {
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🦅 Khoj" : "🤔 You";
|
||||
// Generate HTML for Chat Message and Append to Chat Body
|
||||
document.getElementById("chat-body").innerHTML += `
|
||||
<div data-meta="${by_name} at ${message_time}" class="chat-message ${by}">
|
||||
<div class="chat-message-text ${by}">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
// Scroll to bottom of input-body element
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
}
|
||||
|
||||
function chat() {
|
||||
// Extract required fields for search from form
|
||||
query = document.getElementById("chat-input").value.trim();
|
||||
type_ = document.getElementById("chat-type").value;
|
||||
console.log(`Query: ${query}, Type: ${type_}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
if (query.length === 0)
|
||||
return;
|
||||
|
||||
// Add message by user to chat body
|
||||
renderMessage(query, "you");
|
||||
document.getElementById("chat-input").value = "";
|
||||
|
||||
// Generate backend API URL to execute query
|
||||
url = type_ === "chat"
|
||||
? `/api/beta/chat?q=${encodeURIComponent(query)}`
|
||||
: `/api/beta/summarize?q=${encodeURIComponent(query)}`;
|
||||
|
||||
// Call specified Khoj API
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => data.response)
|
||||
.then(response => {
|
||||
// Render message by Khoj to chat body
|
||||
console.log(response);
|
||||
renderMessage(response, "khoj");
|
||||
});
|
||||
}
|
||||
|
||||
function incrementalChat(event) {
|
||||
// Send chat message on 'Enter'
|
||||
if (event.key === 'Enter') {
|
||||
chat();
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// Fill type field with value passed in URL query parameters, if any.
|
||||
var type_via_url = new URLSearchParams(window.location.search).get("t");
|
||||
if (type_via_url)
|
||||
document.getElementById("chat-type").value = type_via_url;
|
||||
|
||||
fetch('/api/beta/chat')
|
||||
.then(response => response.json())
|
||||
.then(data => data.response)
|
||||
.then(chat_logs => {
|
||||
// Render conversation history, if any
|
||||
chat_logs.forEach(chat_log => {
|
||||
renderMessage(chat_log.message, chat_log.by, new Date(chat_log.created));
|
||||
});
|
||||
});
|
||||
|
||||
// Set welcome message on load
|
||||
renderMessage("Hey, what's up?", "khoj");
|
||||
|
||||
// Fill query field with value passed in URL query parameters, if any.
|
||||
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
||||
if (query_via_url) {
|
||||
document.getElementById("chat-input").value = query_via_url;
|
||||
chat();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
<!-- Chat Header -->
|
||||
<h1>Khoj</h1>
|
||||
|
||||
<!-- Chat Body -->
|
||||
<div id="chat-body"></div>
|
||||
|
||||
<!-- Chat Footer -->
|
||||
<div id="chat-footer">
|
||||
<input type="text" id="chat-input" class="option" onkeyup=incrementalChat(event) autofocus="autofocus" placeholder="What is the meaning of life?">
|
||||
|
||||
<!--Select Chat Type from: Chat, Summarize -->
|
||||
<select id="chat-type" class="option" onchange="setTypeFieldInUrl(this)">
|
||||
<option value="chat">Chat</option>
|
||||
<option value="summarize">Summarize</option>
|
||||
</select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
body {
|
||||
display: grid;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
font-family: roboto, karma, segoe ui, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
body > * {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 200;
|
||||
color: #017eff;
|
||||
}
|
||||
|
||||
#chat-body {
|
||||
font-size: medium;
|
||||
margin: 0px;
|
||||
line-height: 20px;
|
||||
overflow-y: scroll; /* Make chat body scroll to see history */
|
||||
}
|
||||
/* add chat metatdata to bottom of bubble */
|
||||
.chat-message::after {
|
||||
content: attr(data-meta);
|
||||
display: block;
|
||||
font-size: x-small;
|
||||
color: #475569;
|
||||
margin: -12px 7px 0 -5px;
|
||||
}
|
||||
/* move message by khoj to left */
|
||||
.chat-message.khoj {
|
||||
margin-left: auto;
|
||||
text-align: left;
|
||||
}
|
||||
/* move message by you to right */
|
||||
.chat-message.you {
|
||||
margin-right: auto;
|
||||
text-align: right;
|
||||
}
|
||||
/* basic style chat message text */
|
||||
.chat-message-text {
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
text-align: left;
|
||||
}
|
||||
/* color chat bubble by khoj blue */
|
||||
.chat-message-text.khoj {
|
||||
color: #f8fafc;
|
||||
background: #017eff;
|
||||
margin-left: auto;
|
||||
}
|
||||
/* add left protrusion to khoj chat bubble */
|
||||
.chat-message-text.khoj:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: -7px;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #017eff;
|
||||
border-bottom: 0;
|
||||
transform: rotate(-60deg);
|
||||
}
|
||||
/* color chat bubble by you dark grey */
|
||||
.chat-message-text.you {
|
||||
color: #f8fafc;
|
||||
background: #475569;
|
||||
margin-right: auto;
|
||||
}
|
||||
/* add right protrusion to you chat bubble */
|
||||
.chat-message-text.you:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 91%;
|
||||
right: -2px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: #475569;
|
||||
border-right: 0;
|
||||
margin-top: -10px;
|
||||
transform: rotate(-60deg)
|
||||
}
|
||||
|
||||
#chat-footer {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(70px, 85%) auto;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
#chat-footer > * {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background: #f9fafc
|
||||
}
|
||||
.option:hover {
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
}
|
||||
#chat-input {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto minmax(80px, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 1;
|
||||
}
|
||||
#chat-footer {
|
||||
padding: 0;
|
||||
margin: 4px;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
body {
|
||||
grid-template-columns: auto min(70vw, 100%) auto;
|
||||
grid-template-rows: auto minmax(80px, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🦅</text></svg>">
|
||||
<link rel="stylesheet" href="static/assets/config.css">
|
||||
<title>Khoj - Configure App</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="config-form">
|
||||
</form>
|
||||
<button id="config-regenerate">regenerate</button>
|
||||
</body>
|
||||
<script src="static/assets/config.js"></script>
|
||||
</html>
|
||||
@@ -1,326 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<title>Khoj</title>
|
||||
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 144 144%22><text y=%22.86em%22 font-size=%22144%22>🦅</text></svg>">
|
||||
<link rel="icon" type="image/png" sizes="144x144" href="/static/assets/icons/favicon-144x144.png">
|
||||
<link rel="manifest" href="/static/khoj.webmanifest">
|
||||
</head>
|
||||
<script type="text/javascript" src="static/assets/org.min.js"></script>
|
||||
<script type="text/javascript" src="static/assets/markdown-it.min.js"></script>
|
||||
|
||||
<script>
|
||||
function render_image(item) {
|
||||
return `
|
||||
<a href="${item.entry}" class="image-link">
|
||||
<img id=${item.score} src="${item.entry}?${Math.random()}"
|
||||
title="Effective Score: ${item.score}, Meta: ${item.additional.metadata_score}, Image: ${item.additional.image_score}"
|
||||
class="image">
|
||||
</a>`
|
||||
}
|
||||
|
||||
function render_org(query, data, classPrefix="") {
|
||||
var orgCode = data.map(function (item) {
|
||||
return `${item.entry}`
|
||||
}).join("\n")
|
||||
var orgParser = new Org.Parser();
|
||||
var orgDocument = orgParser.parse(orgCode);
|
||||
var orgHTMLDocument = orgDocument.convert(Org.ConverterHTML, { htmlClassPrefix: classPrefix });
|
||||
return orgHTMLDocument.toString();
|
||||
}
|
||||
|
||||
function render_markdown(query, data) {
|
||||
var md = window.markdownit();
|
||||
return md.render(data.map(function (item) {
|
||||
return `${item.entry}`
|
||||
}).join("\n"));
|
||||
}
|
||||
|
||||
function render_ledger(query, data) {
|
||||
return `<div id="results-ledger">` + data.map(function (item) {
|
||||
return `<p>${item.entry}</p>`
|
||||
}).join("\n") + `</div>`;
|
||||
}
|
||||
|
||||
function render_json(data, query, type) {
|
||||
if (type === "markdown") {
|
||||
return render_markdown(query, data);
|
||||
} else if (type === "org") {
|
||||
return render_org(query, data);
|
||||
} else if (type === "music") {
|
||||
return render_org(query, data, "music-");
|
||||
} else if (type === "image") {
|
||||
return data.map(render_image).join('');
|
||||
} else if (type === "ledger") {
|
||||
return render_ledger(query, data);
|
||||
} else {
|
||||
return `<pre id="json">${JSON.stringify(data, null, 2)}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
function search(rerank=false) {
|
||||
// Extract required fields for search from form
|
||||
query = document.getElementById("query").value.trim();
|
||||
type = document.getElementById("type").value;
|
||||
results_count = document.getElementById("results-count").value || 6;
|
||||
console.log(`Query: ${query}, Type: ${type}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
if (query.length === 0)
|
||||
return;
|
||||
|
||||
// If set query field in url query param on rerank
|
||||
if (rerank)
|
||||
setQueryFieldInUrl(query);
|
||||
|
||||
// Generate Backend API URL to execute Search
|
||||
url = type === "image"
|
||||
? `/api/search?q=${encodeURIComponent(query)}&t=${type}&n=${results_count}`
|
||||
: `/api/search?q=${encodeURIComponent(query)}&t=${type}&n=${results_count}&r=${rerank}`;
|
||||
|
||||
// Execute Search and Render Results
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
document.getElementById("results").innerHTML =
|
||||
`<div id=results-${type}>`
|
||||
+ render_json(data, query, type)
|
||||
+ `</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function updateIndex() {
|
||||
type = document.getElementById("type").value;
|
||||
fetch(`/api/update?t=${type}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
document.getElementById("results").innerHTML =
|
||||
render_json(data);
|
||||
});
|
||||
}
|
||||
|
||||
function incrementalSearch(event) {
|
||||
type = document.getElementById("type").value;
|
||||
// Search with reranking on 'Enter'
|
||||
if (event.key === 'Enter') {
|
||||
search(rerank=true);
|
||||
}
|
||||
// Limit incremental search to text types
|
||||
else if (type !== "image") {
|
||||
search(rerank=false);
|
||||
}
|
||||
}
|
||||
|
||||
function populate_type_dropdown() {
|
||||
// Populate type dropdown field with enabled search types only
|
||||
var possible_search_types = ["org", "markdown", "ledger", "music", "image"];
|
||||
fetch("/api/config/data")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("type").innerHTML =
|
||||
possible_search_types
|
||||
.filter(type => data["content-type"].hasOwnProperty(type) && data["content-type"][type])
|
||||
.map(type => `<option value="${type}">${type.slice(0,1).toUpperCase() + type.slice(1)}</option>`)
|
||||
.join('');
|
||||
})
|
||||
.then(() => {
|
||||
// Set type field to search type passed in URL query parameter, if valid
|
||||
var type_via_url = new URLSearchParams(window.location.search).get("t");
|
||||
if (type_via_url && possible_search_types.includes(type_via_url))
|
||||
document.getElementById("type").value = type_via_url;
|
||||
});
|
||||
}
|
||||
|
||||
function setTypeFieldInUrl(type) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set("t", type.value);
|
||||
window.history.pushState({}, "", url.href);
|
||||
}
|
||||
|
||||
function setCountFieldInUrl(results_count) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set("n", results_count.value);
|
||||
window.history.pushState({}, "", url.href);
|
||||
}
|
||||
|
||||
function setQueryFieldInUrl(query) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set("q", query);
|
||||
window.history.pushState({}, "", url.href);
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// Dynamically populate type dropdown based on enabled search types and type passed as URL query parameter
|
||||
populate_type_dropdown();
|
||||
|
||||
// Set results count field with value passed in URL query parameters, if any.
|
||||
var results_count = new URLSearchParams(window.location.search).get("n");
|
||||
if (results_count)
|
||||
document.getElementById("results-count").value = results_count;
|
||||
|
||||
// Fill query field with value passed in URL query parameters, if any.
|
||||
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
||||
if (query_via_url)
|
||||
document.getElementById("query").value = query_via_url;
|
||||
}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<h1>Khoj</h1>
|
||||
|
||||
<!--Add Text Box To Enter Query, Trigger Incremental Search OnChange -->
|
||||
<input type="text" id="query" class="option" onkeyup=incrementalSearch(event) autofocus="autofocus" placeholder="What is the meaning of life?">
|
||||
|
||||
<div id="options">
|
||||
<!--Add Dropdown to Select Query Type -->
|
||||
<select id="type" class="option" onchange="setTypeFieldInUrl(this)"></select>
|
||||
|
||||
<!--Add Button To Regenerate -->
|
||||
<button id="update" class="option" onclick="updateIndex()">Update</button>
|
||||
|
||||
<!--Add Results Count Input To Set Results Count -->
|
||||
<input type="number" id="results-count" min="1" max="100" value="6" placeholder="results count" onchange="setCountFieldInUrl(this)">
|
||||
</div>
|
||||
|
||||
<!-- Section to Render Results -->
|
||||
<div id="results"></div>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr minmax(80px, 100%);
|
||||
}
|
||||
body > * {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(70vw, 100%) 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr minmax(80px, 100%);
|
||||
padding-top: 60vw;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
font-family: roboto, karma, segoe ui, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
body > * {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 200;
|
||||
color: #017eff;
|
||||
}
|
||||
|
||||
#options {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr minmax(70px, 0.5fr);
|
||||
}
|
||||
#options > * {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background: #f9fafc
|
||||
}
|
||||
.option:hover {
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
}
|
||||
#options > select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#options > button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#query {
|
||||
font-size: larger;
|
||||
}
|
||||
#results {
|
||||
font-size: medium;
|
||||
margin: 0px;
|
||||
line-height: 20px;
|
||||
}
|
||||
#results-image {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.image-link {
|
||||
place-self: center;
|
||||
}
|
||||
.image {
|
||||
width: 20vw;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #475569;
|
||||
}
|
||||
#json {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#results-ledger {
|
||||
white-space: pre-line;
|
||||
text-align: left;
|
||||
}
|
||||
#results-markdown {
|
||||
text-align: left;
|
||||
}
|
||||
#results-music,
|
||||
#results-org {
|
||||
text-align: left;
|
||||
white-space: pre-line;
|
||||
}
|
||||
#results-music h3,
|
||||
#results-org h3 {
|
||||
margin: 20px 0 0 0;
|
||||
font-size: larger;
|
||||
}
|
||||
span.music-task-status,
|
||||
span.task-status {
|
||||
color: white;
|
||||
padding: 3.5px 3.5px 0;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #eab308;
|
||||
font-size: medium;
|
||||
}
|
||||
span.music-task-status.todo,
|
||||
span.task-status.todo {
|
||||
background-color: #3b82f6
|
||||
}
|
||||
span.music-task-status.done,
|
||||
span.task-status.done {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
span.music-task-tag,
|
||||
span.task-tag {
|
||||
color: white;
|
||||
padding: 3.5px 3.5px 0;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background-color: #ef4444;
|
||||
font-size: small;
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
282
src/khoj/configure.py
Normal file
@@ -0,0 +1,282 @@
|
||||
# Standard Packages
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
# External Packages
|
||||
import schedule
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
# Internal Packages
|
||||
from khoj.processor.conversation.gpt import summarize
|
||||
from khoj.processor.jsonl.jsonl_to_jsonl import JsonlToJsonl
|
||||
from khoj.processor.markdown.markdown_to_jsonl import MarkdownToJsonl
|
||||
from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl
|
||||
from khoj.processor.pdf.pdf_to_jsonl import PdfToJsonl
|
||||
from khoj.processor.github.github_to_jsonl import GithubToJsonl
|
||||
from khoj.processor.notion.notion_to_jsonl import NotionToJsonl
|
||||
from khoj.search_type import image_search, text_search
|
||||
from khoj.utils import constants, state
|
||||
from khoj.utils.config import SearchType, SearchModels, ProcessorConfigModel, ConversationProcessorConfigModel
|
||||
from khoj.utils.helpers import LRU, resolve_absolute_path, merge_dicts
|
||||
from khoj.utils.rawconfig import FullConfig, ProcessorConfig
|
||||
from khoj.search_filter.date_filter import DateFilter
|
||||
from khoj.search_filter.word_filter import WordFilter
|
||||
from khoj.search_filter.file_filter import FileFilter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def configure_server(args, required=False):
|
||||
if args.config is None:
|
||||
if required:
|
||||
logger.error(
|
||||
f"Exiting as Khoj is not configured.\nConfigure it via http://localhost:8000/config or by editing {state.config_file}."
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Khoj is not configured.\nConfigure it via http://localhost:8000/config, plugins or by editing {state.config_file}."
|
||||
)
|
||||
return
|
||||
else:
|
||||
state.config = args.config
|
||||
|
||||
# Initialize Processor from Config
|
||||
state.processor_config = configure_processor(args.config.processor)
|
||||
|
||||
# Initialize the search type and model from Config
|
||||
state.search_index_lock.acquire()
|
||||
state.SearchType = configure_search_types(state.config)
|
||||
state.model = configure_search(state.model, state.config, args.regenerate)
|
||||
state.search_index_lock.release()
|
||||
|
||||
|
||||
def configure_routes(app):
|
||||
# Import APIs here to setup search types before while configuring server
|
||||
from khoj.routers.api import api
|
||||
from khoj.routers.api_beta import api_beta
|
||||
from khoj.routers.web_client import web_client
|
||||
|
||||
app.mount("/static", StaticFiles(directory=constants.web_directory), name="static")
|
||||
app.include_router(api, prefix="/api")
|
||||
app.include_router(api_beta, prefix="/api/beta")
|
||||
app.include_router(web_client)
|
||||
|
||||
|
||||
if not state.demo:
|
||||
|
||||
@schedule.repeat(schedule.every(61).minutes)
|
||||
def update_search_index():
|
||||
state.search_index_lock.acquire()
|
||||
state.model = configure_search(state.model, state.config, regenerate=False)
|
||||
state.search_index_lock.release()
|
||||
logger.info("📬 Search index updated via Scheduler")
|
||||
|
||||
|
||||
def configure_search_types(config: FullConfig):
|
||||
# Extract core search types
|
||||
core_search_types = {e.name: e.value for e in SearchType}
|
||||
# Extract configured plugin search types
|
||||
plugin_search_types = {}
|
||||
if config.content_type and config.content_type.plugins:
|
||||
plugin_search_types = {plugin_type: plugin_type for plugin_type in config.content_type.plugins.keys()}
|
||||
|
||||
# Dynamically generate search type enum by merging core search types with configured plugin search types
|
||||
return Enum("SearchType", merge_dicts(core_search_types, plugin_search_types))
|
||||
|
||||
|
||||
def configure_search(model: SearchModels, config: FullConfig, regenerate: bool, t: Optional[state.SearchType] = None):
|
||||
if config is None or config.content_type is None or config.search_type is None:
|
||||
logger.warning("🚨 No Content or Search type is configured.")
|
||||
return
|
||||
|
||||
if model is None:
|
||||
model = SearchModels()
|
||||
|
||||
try:
|
||||
# Initialize Org Notes Search
|
||||
if (t == state.SearchType.Org or t == None) and config.content_type.org and config.search_type.asymmetric:
|
||||
logger.info("🦄 Setting up search for orgmode notes")
|
||||
# Extract Entries, Generate Notes Embeddings
|
||||
model.org_search = text_search.setup(
|
||||
OrgToJsonl,
|
||||
config.content_type.org,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()],
|
||||
)
|
||||
|
||||
# Initialize Markdown Search
|
||||
if (
|
||||
(t == state.SearchType.Markdown or t == None)
|
||||
and config.content_type.markdown
|
||||
and config.search_type.asymmetric
|
||||
):
|
||||
logger.info("💎 Setting up search for markdown notes")
|
||||
# Extract Entries, Generate Markdown Embeddings
|
||||
model.markdown_search = text_search.setup(
|
||||
MarkdownToJsonl,
|
||||
config.content_type.markdown,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()],
|
||||
)
|
||||
|
||||
# Initialize PDF Search
|
||||
if (t == state.SearchType.Pdf or t == None) and config.content_type.pdf and config.search_type.asymmetric:
|
||||
logger.info("🖨️ Setting up search for pdf")
|
||||
# Extract Entries, Generate PDF Embeddings
|
||||
model.pdf_search = text_search.setup(
|
||||
PdfToJsonl,
|
||||
config.content_type.pdf,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()],
|
||||
)
|
||||
|
||||
# Initialize Image Search
|
||||
if (t == state.SearchType.Image or t == None) and config.content_type.image and config.search_type.image:
|
||||
logger.info("🌄 Setting up search for images")
|
||||
# Extract Entries, Generate Image Embeddings
|
||||
model.image_search = image_search.setup(
|
||||
config.content_type.image, search_config=config.search_type.image, regenerate=regenerate
|
||||
)
|
||||
|
||||
if (t == state.SearchType.Github or t == None) and config.content_type.github and config.search_type.asymmetric:
|
||||
logger.info("🐙 Setting up search for github")
|
||||
# Extract Entries, Generate Github Embeddings
|
||||
model.github_search = text_search.setup(
|
||||
GithubToJsonl,
|
||||
config.content_type.github,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()],
|
||||
)
|
||||
|
||||
# Initialize External Plugin Search
|
||||
if (t == None or t in state.SearchType) and config.content_type.plugins:
|
||||
logger.info("🔌 Setting up search for plugins")
|
||||
model.plugin_search = {}
|
||||
for plugin_type, plugin_config in config.content_type.plugins.items():
|
||||
model.plugin_search[plugin_type] = text_search.setup(
|
||||
JsonlToJsonl,
|
||||
plugin_config,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()],
|
||||
)
|
||||
|
||||
# Initialize Notion Search
|
||||
if (t == None or t in state.SearchType) and config.content_type.notion:
|
||||
logger.info("🔌 Setting up search for notion")
|
||||
model.notion_search = text_search.setup(
|
||||
NotionToJsonl,
|
||||
config.content_type.notion,
|
||||
search_config=config.search_type.asymmetric,
|
||||
regenerate=regenerate,
|
||||
filters=[DateFilter(), WordFilter(), FileFilter()],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("🚨 Failed to setup search")
|
||||
raise e
|
||||
|
||||
# Invalidate Query Cache
|
||||
state.query_cache = LRU()
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def configure_processor(processor_config: ProcessorConfig):
|
||||
if not processor_config:
|
||||
return
|
||||
|
||||
processor = ProcessorConfigModel()
|
||||
|
||||
# Initialize Conversation Processor
|
||||
if processor_config.conversation:
|
||||
logger.info("💬 Setting up conversation processor")
|
||||
processor.conversation = configure_conversation_processor(processor_config.conversation)
|
||||
|
||||
return processor
|
||||
|
||||
|
||||
def configure_conversation_processor(conversation_processor_config):
|
||||
conversation_processor = ConversationProcessorConfigModel(conversation_processor_config)
|
||||
conversation_logfile = resolve_absolute_path(conversation_processor.conversation_logfile)
|
||||
|
||||
if conversation_logfile.is_file():
|
||||
# Load Metadata Logs from Conversation Logfile
|
||||
with conversation_logfile.open("r") as f:
|
||||
conversation_processor.meta_log = json.load(f)
|
||||
logger.debug(f"Loaded conversation logs from {conversation_logfile}")
|
||||
else:
|
||||
# Initialize Conversation Logs
|
||||
conversation_processor.meta_log = {}
|
||||
conversation_processor.chat_session = []
|
||||
|
||||
return conversation_processor
|
||||
|
||||
|
||||
@schedule.repeat(schedule.every(17).minutes)
|
||||
def save_chat_session():
|
||||
# No need to create empty log file
|
||||
if not (
|
||||
state.processor_config
|
||||
and state.processor_config.conversation
|
||||
and state.processor_config.conversation.meta_log
|
||||
and state.processor_config.conversation.chat_session
|
||||
):
|
||||
return
|
||||
|
||||
# Summarize Conversation Logs for this Session
|
||||
chat_session = state.processor_config.conversation.chat_session
|
||||
openai_api_key = state.processor_config.conversation.openai_api_key
|
||||
conversation_log = state.processor_config.conversation.meta_log
|
||||
chat_model = state.processor_config.conversation.chat_model
|
||||
session = {
|
||||
"summary": summarize(chat_session, model=chat_model, api_key=openai_api_key),
|
||||
"session-start": conversation_log.get("session", [{"session-end": 0}])[-1]["session-end"],
|
||||
"session-end": len(conversation_log["chat"]),
|
||||
}
|
||||
if "session" in conversation_log:
|
||||
conversation_log["session"].append(session)
|
||||
else:
|
||||
conversation_log["session"] = [session]
|
||||
|
||||
# Save Conversation Metadata Logs to Disk
|
||||
conversation_logfile = resolve_absolute_path(state.processor_config.conversation.conversation_logfile)
|
||||
conversation_logfile.parent.mkdir(parents=True, exist_ok=True) # create conversation directory if doesn't exist
|
||||
with open(conversation_logfile, "w+", encoding="utf-8") as logfile:
|
||||
json.dump(conversation_log, logfile, indent=2)
|
||||
|
||||
state.processor_config.conversation.chat_session = []
|
||||
logger.info("📩 Saved current chat session to conversation logs")
|
||||
|
||||
|
||||
@schedule.repeat(schedule.every(59).minutes)
|
||||
def upload_telemetry():
|
||||
if not state.config or not state.config.app or not state.config.app.should_log_telemetry or not state.telemetry:
|
||||
message = "📡 No telemetry to upload" if not state.telemetry else "📡 Telemetry logging disabled"
|
||||
logger.debug(message)
|
||||
return
|
||||
|
||||
try:
|
||||
logger.debug(f"📡 Upload usage telemetry to {constants.telemetry_server}:\n{state.telemetry}")
|
||||
for log in state.telemetry:
|
||||
for field in log:
|
||||
# Check if the value for the field is JSON serializable
|
||||
try:
|
||||
json.dumps(log[field])
|
||||
except TypeError:
|
||||
log[field] = str(log[field])
|
||||
requests.post(constants.telemetry_server, json=state.telemetry)
|
||||
except Exception as e:
|
||||
logger.error(f"📡 Error uploading telemetry: {e}")
|
||||
else:
|
||||
state.telemetry = []
|
||||
56
src/khoj/interface/desktop/main_window.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Standard Packages
|
||||
import webbrowser
|
||||
|
||||
# External Packages
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
# Internal Packages
|
||||
from khoj.utils import constants
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
"""Create Window to Navigate users to the web UI"""
|
||||
|
||||
def __init__(self, host: str, port: int):
|
||||
super(MainWindow, self).__init__()
|
||||
|
||||
# Initialize Configure Window
|
||||
self.setWindowTitle("Khoj")
|
||||
|
||||
# Set Window Icon
|
||||
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
|
||||
self.setWindowIcon(QtGui.QIcon(f"{icon_path.absolute()}"))
|
||||
|
||||
# Initialize Configure Window Layout
|
||||
self.wlayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
# Add a Label that says "Khoj Configuration" to the Window
|
||||
self.wlayout.addWidget(QtWidgets.QLabel("Welcome to Khoj"))
|
||||
|
||||
# Add a Button to open the Web UI at http://host:port/config
|
||||
self.open_web_ui_button = QtWidgets.QPushButton("Open Web UI")
|
||||
self.open_web_ui_button.clicked.connect(lambda: webbrowser.open(f"http://{host}:{port}/config"))
|
||||
|
||||
self.wlayout.addWidget(self.open_web_ui_button)
|
||||
|
||||
# Set the central widget of the Window. Widget will expand
|
||||
# to take up all the space in the window by default.
|
||||
self.config_window = QtWidgets.QWidget()
|
||||
self.config_window.setLayout(self.wlayout)
|
||||
self.setCentralWidget(self.config_window)
|
||||
self.position_window()
|
||||
|
||||
def position_window(self):
|
||||
"Position the window at center of X axis and near top on Y axis"
|
||||
window_rectangle = self.geometry()
|
||||
screen_center = self.screen().availableGeometry().center()
|
||||
window_rectangle.moveCenter(screen_center)
|
||||
self.move(window_rectangle.topLeft().x(), 25)
|
||||
|
||||
def show_on_top(self):
|
||||
"Bring Window on Top"
|
||||
self.show()
|
||||
self.setWindowState(Qt.WindowState.WindowActive)
|
||||
self.activateWindow() # For Bringing to Top on Windows
|
||||
self.raise_() # For Bringing to Top from Minimized State on OSX
|
||||
@@ -5,8 +5,8 @@ import webbrowser
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
|
||||
# Internal Packages
|
||||
from src.utils import constants, state
|
||||
from src.interface.desktop.main_window import MainWindow
|
||||
from khoj.utils import constants, state
|
||||
from khoj.interface.desktop.main_window import MainWindow
|
||||
|
||||
|
||||
def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
|
||||
@@ -17,23 +17,24 @@ def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
|
||||
"""
|
||||
|
||||
# Create the system tray with icon
|
||||
icon_path = constants.web_directory / 'assets/icons/favicon-144x144.png'
|
||||
icon = QtGui.QIcon(f'{icon_path.absolute()}')
|
||||
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
|
||||
icon = QtGui.QIcon(f"{icon_path.absolute()}")
|
||||
tray = QtWidgets.QSystemTrayIcon(icon)
|
||||
tray.setVisible(True)
|
||||
|
||||
# Create the menu and menu actions
|
||||
menu = QtWidgets.QMenu()
|
||||
menu_actions = [
|
||||
('Search', lambda: webbrowser.open(f'http://{state.host}:{state.port}/')),
|
||||
('Configure', main_window.show_on_top),
|
||||
('Quit', gui.quit),
|
||||
("Search", lambda: webbrowser.open(f"http://{state.host}:{state.port}/")),
|
||||
("Configure", lambda: webbrowser.open(f"http://{state.host}:{state.port}/config")),
|
||||
("App", main_window.show),
|
||||
("Quit", gui.quit),
|
||||
]
|
||||
|
||||
# Add the menu actions to the menu
|
||||
for action_text, action_function in menu_actions:
|
||||
menu_action = QtGui.QAction(action_text, menu)
|
||||
menu_action.triggered.connect(action_function)
|
||||
menu_action.triggered.connect(action_function) # type: ignore[attr-defined]
|
||||
menu.addAction(menu_action)
|
||||
|
||||
# Add the menu to the system tray
|
||||
22
src/khoj/interface/web/404.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Khoj: An AI Personal Assistant for your digital brain</title>
|
||||
<link rel=”stylesheet” href=”static/styles.css”>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
|
||||
</head>
|
||||
<body class="not-found">
|
||||
<header class=”header”>
|
||||
<h1>Oops, this is awkward. That page couldn't be found.</h1>
|
||||
</header>
|
||||
<a href="/config">Go Home</a>
|
||||
|
||||
<footer class=”footer”>
|
||||
</footer>
|
||||
</body>
|
||||
<style>
|
||||
body.not-found {
|
||||
padding: 0 10%
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
693
src/khoj/interface/web/assets/icons/chat.svg
Normal file
@@ -0,0 +1,693 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
width="299.99649"
|
||||
height="225.92412"
|
||||
viewBox="0 0 85.704 64.542"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="Speech_bubble(1).svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview3800"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0445985"
|
||||
inkscape:cx="150.77563"
|
||||
inkscape:cy="112.96206"
|
||||
inkscape:window-width="1309"
|
||||
inkscape:window-height="456"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><defs
|
||||
id="defs8" />
|
||||
<g
|
||||
transform="matrix(0.9486962,0,0,0.9486962,2.4834364,1.8361818)"
|
||||
id="g3">
|
||||
<path
|
||||
d="M 45.673,0 C 67.781,0 85.703,12.475 85.703,27.862 C 85.703,43.249 67.781,55.724 45.673,55.724 C 38.742,55.724 32.224,54.497 26.539,52.34 C 15.319,56.564 0,64.542 0,64.542 C 0,64.542 9.989,58.887 14.107,52.021 C 15.159,50.266 15.775,48.426 16.128,46.659 C 9.618,41.704 5.643,35.106 5.643,27.862 C 5.643,12.475 23.565,0 45.673,0 M 45.673,2.22 C 24.824,2.22 7.862,13.723 7.862,27.863 C 7.862,34.129 11.275,40.177 17.472,44.893 L 18.576,45.734 L 18.305,47.094 C 17.86,49.324 17.088,51.366 16.011,53.163 C 15.67,53.73 15.294,54.29 14.891,54.837 C 18.516,53.191 22.312,51.561 25.757,50.264 L 26.542,49.968 L 27.327,50.266 C 32.911,52.385 39.255,53.505 45.673,53.505 C 66.522,53.505 83.484,42.002 83.484,27.862 C 83.484,13.722 66.522,2.22 45.673,2.22 L 45.673,2.22 z "
|
||||
id="path5" />
|
||||
</g>
|
||||
<image
|
||||
width="75.991768"
|
||||
height="49.994583"
|
||||
preserveAspectRatio="none"
|
||||
xlink:href="data:image/png;base64,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
|
||||
RU5ErkJggg==
|
||||
"
|
||||
id="image3810"
|
||||
x="9.712183"
|
||||
y="3.7505856" /></svg>
|
||||
|
After Width: | Height: | Size: 50 KiB |
1
src/khoj/interface/web/assets/icons/confirm-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><defs><style>.cls-1{fill:#00a912;}.cls-1,.cls-2{fill-rule:evenodd;}.cls-2{fill:#fff;}</style></defs><title>confirm</title><path class="cls-1" d="M61.44,0A61.44,61.44,0,1,1,0,61.44,61.44,61.44,0,0,1,61.44,0Z"/><path class="cls-2" d="M42.37,51.68,53.26,62,79,35.87c2.13-2.16,3.47-3.9,6.1-1.19l8.53,8.74c2.8,2.77,2.66,4.4,0,7L58.14,85.34c-5.58,5.46-4.61,5.79-10.26.19L28,65.77c-1.18-1.28-1.05-2.57.24-3.84l9.9-10.27c1.5-1.58,2.7-1.44,4.22,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 549 B |
BIN
src/khoj/interface/web/assets/icons/favicon-128x128.ico
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
src/khoj/interface/web/assets/icons/favicon-128x128.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/khoj/interface/web/assets/icons/favicon.icns
Normal file
1
src/khoj/interface/web/assets/icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
||||
|
After Width: | Height: | Size: 964 B |
BIN
src/khoj/interface/web/assets/icons/khoj-logo-sideways-200.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
5385
src/khoj/interface/web/assets/icons/khoj-logo-sideways.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
1
src/khoj/interface/web/assets/icons/logotype.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 942.19 656.92"><defs><style>.cls-1{fill:#1e1d1e;}</style></defs><title>logotype</title><path class="cls-1" d="M122.48,234.63c7.25,7.25,3.29,38.21,3.29,57.32,0,11.86,2,21.74,2,31,0,9.88-2,19.76-2,29,2,7.9,0,36.89,4,38.87,5.93,4,11.86-15.81,13.83-17.79,5.93-13.18,2-5.27,9.23-21.08,7.9-15.16,15.81-31,21.74-42.17,5.27-11.86,13.18-29,19.11-42.83,5.93-9.22,9.88-27,19.11-34.26,5.92-5.93,25-5.93,42.82-5.93,13.18,2,27,2,31,9.22,5.27,11.86-15.82,42.83-21.74,56-7.25,15.81-13.18,25.7-21.09,38.87-2,4-2,7.91-4,13.84-7.25,19.11-17.13,32.28-27,52-4,7.25-7.25,17.13-13.18,25-4,11.2-9.89,21.08-9.89,29,0,11.2,7.91,27,11.86,34.26,13.18,25.7,27,50.73,40.19,77.75,2,5.27,2,11.2,5.93,17.13,5.93,15.15,13.84,27,23.06,46.12,5.94,13.84,21.09,38.87,17.79,50.07-5.93,13.84-21.74,9.88-38.87,9.88-21.08,0-36.89,0-44.14-4-7.91-5.93-13.84-27-19.77-38.21-13.18-31-19.11-48.76-34.26-81a152.72,152.72,0,0,0-15.81-33c-7.91-9.88-7.91,21.09-9.89,34.92v38.22c0,25,5.93,67.86-5.27,79.06-11.86,9.89-67.86,7.91-75.77-5.93-2-7.24,0-30.31,0-50.07,2-19.11,0-31,0-48.1-2-42.17-2-81-2-123.21,0-15.81,4-31,2-42.83,0-7.9-2-13.17-2-21.08,0-5.93,2-9.88,2-15.81,0-13.18-2-27-2-40.19,0-13.84,2-27,2-38.88,0-17.13-5.27-38.21-2-52,2-5.93,7.91-11.2,9.89-13.18,2,0,2,0,4-2C71.74,222.77,114.57,224.74,122.48,234.63Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M547.58,232.65c3.95,2,3.95,9.23,5.93,17.13,2,15.16,0,46.12,0,63.91-2,29,0,59.3,0,81V523.87c0,34.92,0,83-2,123.87,0,17.13,2,30.31-5.93,36.24-9.22,5.93-63.91,5.93-71.16,0-7.9-7.91-3.95-33-3.95-50.08V610.84c0-36.89-2-71.81,0-114,0-9.88,2-21.08,0-25-5.93-5.93-21.08-2-32.94-2-7.91,0-25-5.93-32.95,2-7.25,9.88-4,36.9-4,54,0,42.17,2,63.91,2,108,0,23.07,2,42.17-9.23,50.08-11.86,7.91-58,5.93-65.89-2-11.86-11.2-9.88-56-9.88-83,0-83,3.95-144.29,2-218.08,0-15.16,2-31,2-42.17,0-7.91-2-15.81-3.95-27,0-5.93,2-13.84,2-19.77,2-21.08-2-48.1,2-56,7.91-11.2,67.87-13.17,77.09-1.31,7.91,11.2,2,34.26,2,48.09-2,15.16,0,34.93,0,56v27c0,9.23,0,23.06,2,27,3.95,5.93,23.71,5.93,32.94,5.93,7.9,0,29,2,32.94-4s4-38.87,4-50.07V264.94c0-11.2-2-23.06,2-30.31,3.95-7.91,13.18-5.93,21.08-7.91C501.46,226.72,539.67,222.77,547.58,232.65Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M716,218.81c31-2,61.93,7.91,81,17.13,5.93,4,19.11,17.8,23.06,23.72,9.88,13.18,15.81,23.06,19.11,40.2,2,3.95,3.95,5.93,3.95,7.9v7.91c2,9.22,5.93,19.11,7.91,25v21.08c0,5.93,2,11.2,2,17.13,4,34.92,2,75.77,0,114,0,25,0,50.07-3.95,73.13-4,23.72-9.88,48.76-15.15,61.94-2,3.95-7.91,13.83-11.86,17.78l-2,2c-5.93,9.23-17.13,17.13-29,25-5.93,3.29-7.91,5.27-13.18,7.25-5.93,2-11.86,2-21.74,4-5.27,0-11.2,3.95-17.13,3.95-13.18,2-31,2-40.19,2-11.86-2-25.7-5.93-36.9-11.86-2,0-4-3.29-4-3.29a107,107,0,0,1-17.14-7.91c-7.9-5.93-17.78-17.79-23.71-25-13.18-23.06-23.07-58-25-90.92-2-25-2-58-2-79.07-2-15.81,0-32.94,2-48.1,0-21.74,0-44.8,2-63.91,2-9.88,4-19.1,5.94-27,0-11.86,0-21.08,2-31,5.93-17.13,19.11-46.12,33-58,3.95-2,9.22-5.93,15.15-9.22,15.81-7.91,38.87-13.84,63.91-15.82ZM677.11,348c-5.93,48.76-4,100.81-2,150.88,2,38.88,4,98.18,21.74,112,0,2,5.27,4,9.22,5.93,15.82,5.28,29,2,40.86-5.93,3.95-4,9.22-9.88,11.19-13.83,5.93-9.23,9.89-36.24,11.86-54,0-15.15,0-34.26,2-56,0-42.17-2-85-5.93-121.24-2-25-3.95-48.09-11.86-59.95C748.93,297.88,741,292,733.12,290c-9.23-3.29-13.18,0-17.13,0h-2C689,293.93,681.07,319,677.11,348Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M975.89,226.44c9.23,9.23,7.91,234.41,7.91,263.4-2,63.25-2,108.05-2,175.26,0,11.86,2,17.79,2,25.69,0,15.16-3.95,38.22-2,61.28,0,23.06,2,51.24-4,64.42-4,17.13-19.77,36.89-34.92,46.12-15.15,9.88-40.85,13.84-71.16,11.86-19.76-2-42.82-4-50.73-15.82-2-5.27-2-21.08-2-32.28,0-15.81,0-27,5.93-31,11.86-5.93,36.9,13.18,56,7.91,7.9-2,15.81-9.88,19.76-23.72,1.32-11.2,0-27.52,0-40.7,0-11.86,1.32-21.08,1.32-32.94V634.79c0-29-3.29-56-3.29-81.7,0-36.24,2-253.51,2-286.46,0-23.06-7.91-38.21,9.22-46.12C923.84,216.56,966,216.56,975.89,226.44Z" transform="translate(-41.67 -218.04)"/></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
src/khoj/interface/web/assets/icons/markdown.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg>
|
||||
|
After Width: | Height: | Size: 283 B |
4
src/khoj/interface/web/assets/icons/notion.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/khoj/interface/web/assets/icons/org.svg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
23
src/khoj/interface/web/assets/icons/pdf.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 334.371 380.563" version="1.1" viewBox="0 0 14 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
|
||||
<polygon points="51.791 356.65 51.791 23.99 204.5 23.99 282.65 102.07 282.65 356.65" fill="#fff" stroke-width="212.65"/>
|
||||
<path d="m201.19 31.99 73.46 73.393v243.26h-214.86v-316.66h141.4m6.623-16h-164.02v348.66h246.85v-265.9z" stroke-width="21.791"/>
|
||||
</g>
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
|
||||
<polygon points="282.65 356.65 51.791 356.65 51.791 23.99 204.5 23.99 206.31 25.8 206.31 100.33 280.9 100.33 282.65 102.07" fill="#fff" stroke-width="212.65"/>
|
||||
<path d="m198.31 31.99v76.337h76.337v240.32h-214.86v-316.66h138.52m9.5-16h-164.02v348.66h246.85v-265.9l-6.43-6.424h-69.907v-69.842z" stroke-width="21.791"/>
|
||||
</g>
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" stroke-width="21.791">
|
||||
<polygon points="258.31 87.75 219.64 87.75 219.64 48.667 258.31 86.38"/>
|
||||
<path d="m227.64 67.646 12.41 12.104h-12.41v-12.104m-5.002-27.229h-10.998v55.333h54.666v-12.742z"/>
|
||||
</g>
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" fill="#ed1c24" stroke-width="212.65">
|
||||
<polygon points="311.89 284.49 22.544 284.49 22.544 167.68 37.291 152.94 37.291 171.49 297.15 171.49 297.15 152.94 311.89 167.68"/>
|
||||
<path d="m303.65 168.63 1.747 1.747v107.62h-276.35v-107.62l1.747-1.747v9.362h272.85v-9.362m-12.999-31.385v27.747h-246.86v-27.747l-27.747 27.747v126h302.35v-126z"/>
|
||||
</g>
|
||||
<rect x="1.7219" y="7.9544" width="10.684" height="4.0307" fill="none"/>
|
||||
<g transform="matrix(.04589 0 0 .04589 1.7219 11.733)" fill="#fff" stroke-width="21.791"><path d="m9.216 0v-83.2h30.464q6.784 0 12.928 1.408 6.144 1.28 10.752 4.608 4.608 3.2 7.296 8.576 2.816 5.248 2.816 13.056 0 7.68-2.816 13.184-2.688 5.504-7.296 9.088-4.608 3.456-10.624 5.248-6.016 1.664-12.544 1.664h-8.96v26.368zm22.016-43.776h7.936q6.528 0 9.6-3.072 3.2-3.072 3.2-8.704t-3.456-7.936-9.856-2.304h-7.424z"/><path d="m87.04 0v-83.2h24.576q9.472 0 17.28 2.304 7.936 2.304 13.568 7.296t8.704 12.8q3.2 7.808 3.2 18.816t-3.072 18.944-8.704 13.056q-5.504 5.12-13.184 7.552-7.552 2.432-16.512 2.432zm22.016-17.664h1.28q4.48 0 8.448-1.024 3.968-1.152 6.784-3.84 2.944-2.688 4.608-7.424t1.664-12.032-1.664-11.904-4.608-7.168q-2.816-2.56-6.784-3.456-3.968-1.024-8.448-1.024h-1.28z"/><path d="m169.22 0v-83.2h54.272v18.432h-32.256v15.872h27.648v18.432h-27.648v30.464z"/></g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
101
src/khoj/interface/web/assets/khoj.css
Normal file
@@ -0,0 +1,101 @@
|
||||
/* Amber Light scheme (Default) */
|
||||
/* Can be forced with data-theme="light" */
|
||||
[data-theme="light"],
|
||||
:root:not([data-theme="dark"]) {
|
||||
--primary: #ffb300;
|
||||
--primary-hover: #ffa000;
|
||||
--primary-focus: rgba(255, 179, 0, 0.125);
|
||||
--primary-inverse: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
/* Amber Dark scheme (Auto) */
|
||||
/* Automatically enabled if user has Dark mode enabled */
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--primary: #ffb300;
|
||||
--primary-hover: #ffc107;
|
||||
--primary-focus: rgba(255, 179, 0, 0.25);
|
||||
--primary-inverse: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
}
|
||||
/* Amber Dark scheme (Forced) */
|
||||
/* Enabled if forced with data-theme="dark" */
|
||||
[data-theme="dark"] {
|
||||
--primary: #ffb300;
|
||||
--primary-hover: #ffc107;
|
||||
--primary-focus: rgba(255, 179, 0, 0.25);
|
||||
--primary-inverse: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
/* Amber (Common styles) */
|
||||
:root {
|
||||
--form-element-active-border-color: var(--primary);
|
||||
--form-element-focus-color: var(--primary-focus);
|
||||
--switch-color: var(--primary-inverse);
|
||||
--switch-checked-background-color: var(--primary);
|
||||
}
|
||||
|
||||
.khoj-configure {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.khoj-header {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 20px;
|
||||
padding: 16px 0;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
nav.khoj-nav {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 32px;
|
||||
justify-self: right;
|
||||
}
|
||||
|
||||
a.khoj-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.khoj-logo {
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.khoj-nav a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
justify-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
.khoj-nav a:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
.khoj-nav-selected {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
img.khoj-logo {
|
||||
width: min(60vw, 111px);
|
||||
max-width: 100%;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
div.khoj-header {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 20px;
|
||||
padding: 16px 10px;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
nav.khoj-nav {
|
||||
grid-gap: 0px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*! markdown-it 13.0.1 https://github.com/markdown-it/markdown-it @license MIT */
|
||||
(function(global, factory) {
|
||||
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||
global.markdownit = factory());
|
||||
})(this, (function() {
|
||||
"use strict";
|
||||
@@ -2164,7 +2164,7 @@
|
||||
var encodeCache = {};
|
||||
// Create a lookup array where anything but characters in `chars` string
|
||||
// and alphanumeric chars is percent-encoded.
|
||||
|
||||
|
||||
function getEncodeCache(exclude) {
|
||||
var i, ch, cache = encodeCache[exclude];
|
||||
if (cache) {
|
||||
@@ -2187,11 +2187,11 @@
|
||||
}
|
||||
// Encode unsafe characters with percent-encoding, skipping already
|
||||
// encoded sequences.
|
||||
|
||||
|
||||
// - string - string to encode
|
||||
// - exclude - list of characters to ignore (in addition to a-zA-Z0-9)
|
||||
// - keepEscaped - don't encode '%' in a correct escape sequence (default: true)
|
||||
|
||||
|
||||
function encode$2(string, exclude, keepEscaped) {
|
||||
var i, l, code, nextCode, cache, result = "";
|
||||
if (typeof exclude !== "string") {
|
||||
@@ -2253,7 +2253,7 @@
|
||||
return cache;
|
||||
}
|
||||
// Decode percent-encoded string.
|
||||
|
||||
|
||||
function decode$2(string, exclude) {
|
||||
var cache;
|
||||
if (typeof exclude !== "string") {
|
||||
@@ -2340,26 +2340,26 @@
|
||||
return result;
|
||||
};
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
|
||||
// Changes from joyent/node:
|
||||
|
||||
|
||||
// 1. No leading slash in paths,
|
||||
// e.g. in `url.parse('http://foo?bar')` pathname is ``, not `/`
|
||||
|
||||
|
||||
// 2. Backslashes are not replaced with slashes,
|
||||
// so `http:\\example.org\` is treated like a relative path
|
||||
|
||||
|
||||
// 3. Trailing colon is treated like a part of the path,
|
||||
// i.e. in `http://example.org:foo` pathname is `:foo`
|
||||
|
||||
|
||||
// 4. Nothing is URL-encoded in the resulting object,
|
||||
// (in joyent/node some chars in auth and paths are encoded)
|
||||
|
||||
|
||||
// 5. `url.parse()` does not have `parseQueryString` argument
|
||||
|
||||
|
||||
// 6. Removed extraneous result properties: `host`, `path`, `query`, etc.,
|
||||
// which can be constructed using other parts of the url.
|
||||
|
||||
|
||||
function Url() {
|
||||
this.protocol = null;
|
||||
this.slashes = null;
|
||||
@@ -2373,28 +2373,28 @@
|
||||
// Reference: RFC 3986, RFC 1808, RFC 2396
|
||||
// define these here so at least they only have to be
|
||||
// compiled once on the first module load.
|
||||
var protocolPattern = /^([a-z0-9.+-]+:)/i, portPattern = /:[0-9]*$/,
|
||||
var protocolPattern = /^([a-z0-9.+-]+:)/i, portPattern = /:[0-9]*$/,
|
||||
// Special case for a simple path URL
|
||||
simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,
|
||||
simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,
|
||||
// RFC 2396: characters reserved for delimiting URLs.
|
||||
// We actually just auto-escape these.
|
||||
delims = [ "<", ">", '"', "`", " ", "\r", "\n", "\t" ],
|
||||
delims = [ "<", ">", '"', "`", " ", "\r", "\n", "\t" ],
|
||||
// RFC 2396: characters not allowed for various reasons.
|
||||
unwise = [ "{", "}", "|", "\\", "^", "`" ].concat(delims),
|
||||
unwise = [ "{", "}", "|", "\\", "^", "`" ].concat(delims),
|
||||
// Allowed by RFCs, but cause of XSS attacks. Always escape these.
|
||||
autoEscape = [ "'" ].concat(unwise),
|
||||
autoEscape = [ "'" ].concat(unwise),
|
||||
// Characters that are never ever allowed in a hostname.
|
||||
// Note that any invalid chars are also handled, but these
|
||||
// are the ones that are *expected* to be seen, so we fast-path
|
||||
// them.
|
||||
nonHostChars = [ "%", "/", "?", ";", "#" ].concat(autoEscape), hostEndingChars = [ "/", "?", "#" ], hostnameMaxLen = 255, hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/, hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
|
||||
nonHostChars = [ "%", "/", "?", ";", "#" ].concat(autoEscape), hostEndingChars = [ "/", "?", "#" ], hostnameMaxLen = 255, hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/, hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
|
||||
// protocols that can allow "unsafe" and "unwise" chars.
|
||||
/* eslint-disable no-script-url */
|
||||
// protocols that never have a hostname.
|
||||
hostlessProtocol = {
|
||||
javascript: true,
|
||||
"javascript:": true
|
||||
},
|
||||
},
|
||||
// protocols that always contain a // bit.
|
||||
slashedProtocol = {
|
||||
http: true,
|
||||
@@ -2632,7 +2632,7 @@
|
||||
return _hasOwnProperty.call(object, key);
|
||||
}
|
||||
// Merge objects
|
||||
|
||||
|
||||
function assign(obj /*from1, from2, from3, ...*/) {
|
||||
var sources = Array.prototype.slice.call(arguments, 1);
|
||||
sources.forEach((function(source) {
|
||||
@@ -2798,12 +2798,12 @@
|
||||
return regex$4.test(ch);
|
||||
}
|
||||
// Markdown ASCII punctuation characters.
|
||||
|
||||
|
||||
// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, _, `, {, |, }, or ~
|
||||
// http://spec.commonmark.org/0.15/#ascii-punctuation-character
|
||||
|
||||
|
||||
// Don't confuse with unicode punctuation !!! It lacks some chars in ascii range.
|
||||
|
||||
|
||||
function isMdAsciiPunct(ch) {
|
||||
switch (ch) {
|
||||
case 33 /* ! */ :
|
||||
@@ -2845,58 +2845,58 @@
|
||||
}
|
||||
}
|
||||
// Hepler to unify [reference labels].
|
||||
|
||||
|
||||
function normalizeReference(str) {
|
||||
// Trim and collapse whitespace
|
||||
str = str.trim().replace(/\s+/g, " ");
|
||||
// In node v10 'ẞ'.toLowerCase() === 'Ṿ', which is presumed to be a bug
|
||||
// fixed in v12 (couldn't find any details).
|
||||
|
||||
|
||||
// So treat this one as a special case
|
||||
// (remove this when node v10 is no longer supported).
|
||||
|
||||
|
||||
if ("\u1e9e".toLowerCase() === "\u1e7e") {
|
||||
str = str.replace(/\u1e9e/g, "\xdf");
|
||||
}
|
||||
// .toLowerCase().toUpperCase() should get rid of all differences
|
||||
// between letter variants.
|
||||
|
||||
|
||||
// Simple .toLowerCase() doesn't normalize 125 code points correctly,
|
||||
// and .toUpperCase doesn't normalize 6 of them (list of exceptions:
|
||||
// İ, ϴ, ẞ, Ω, K, Å - those are already uppercased, but have differently
|
||||
// uppercased versions).
|
||||
|
||||
|
||||
// Here's an example showing how it happens. Lets take greek letter omega:
|
||||
// uppercase U+0398 (Θ), U+03f4 (ϴ) and lowercase U+03b8 (θ), U+03d1 (ϑ)
|
||||
|
||||
|
||||
// Unicode entries:
|
||||
// 0398;GREEK CAPITAL LETTER THETA;Lu;0;L;;;;;N;;;;03B8;
|
||||
// 03B8;GREEK SMALL LETTER THETA;Ll;0;L;;;;;N;;;0398;;0398
|
||||
// 03D1;GREEK THETA SYMBOL;Ll;0;L;<compat> 03B8;;;;N;GREEK SMALL LETTER SCRIPT THETA;;0398;;0398
|
||||
// 03F4;GREEK CAPITAL THETA SYMBOL;Lu;0;L;<compat> 0398;;;;N;;;;03B8;
|
||||
|
||||
|
||||
// Case-insensitive comparison should treat all of them as equivalent.
|
||||
|
||||
|
||||
// But .toLowerCase() doesn't change ϑ (it's already lowercase),
|
||||
// and .toUpperCase() doesn't change ϴ (already uppercase).
|
||||
|
||||
|
||||
// Applying first lower then upper case normalizes any character:
|
||||
// '\u0398\u03f4\u03b8\u03d1'.toLowerCase().toUpperCase() === '\u0398\u0398\u0398\u0398'
|
||||
|
||||
|
||||
// Note: this is equivalent to unicode case folding; unicode normalization
|
||||
// is a different step that is not required here.
|
||||
|
||||
|
||||
// Final result should be uppercased, because it's later stored in an object
|
||||
// (this avoid a conflict with Object.prototype members,
|
||||
// most notably, `__proto__`)
|
||||
|
||||
|
||||
return str.toLowerCase().toUpperCase();
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Re-export libraries commonly used in both markdown-it and its plugins,
|
||||
// so plugins won't have to depend on them explicitly, which reduces their
|
||||
// bundled size (e.g. a browser build).
|
||||
|
||||
|
||||
exports.lib = {};
|
||||
exports.lib.mdurl = mdurl;
|
||||
exports.lib.ucmicro = uc_micro;
|
||||
@@ -3129,7 +3129,7 @@
|
||||
var token = tokens[idx];
|
||||
// "alt" attr MUST be set, even if empty. Because it's mandatory and
|
||||
// should be placed on proper position for tests.
|
||||
|
||||
|
||||
// Replace content with actual value
|
||||
token.attrs[token.attrIndex("alt")][1] = slf.renderInlineAsText(token.children, options, env);
|
||||
return slf.renderToken(tokens, idx, options);
|
||||
@@ -3215,11 +3215,11 @@
|
||||
}
|
||||
// Insert a newline between hidden paragraph and subsequent opening
|
||||
// block-level tag.
|
||||
|
||||
|
||||
// For example, here we should insert a newline before blockquote:
|
||||
// - a
|
||||
// >
|
||||
|
||||
|
||||
if (token.block && token.nesting !== -1 && idx && tokens[idx - 1].hidden) {
|
||||
result += "\n";
|
||||
}
|
||||
@@ -3343,16 +3343,16 @@
|
||||
// }
|
||||
this.__rules__ = [];
|
||||
// Cached rule chains.
|
||||
|
||||
|
||||
// First level - chain name, '' for default.
|
||||
// Second level - diginal anchor for fast filtering by charcodes.
|
||||
|
||||
|
||||
this.__cache__ = null;
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Helper methods, should not be used directly
|
||||
// Find rule index by name
|
||||
|
||||
|
||||
Ruler.prototype.__find__ = function(name) {
|
||||
for (var i = 0; i < this.__rules__.length; i++) {
|
||||
if (this.__rules__[i].name === name) {
|
||||
@@ -3362,7 +3362,7 @@
|
||||
return -1;
|
||||
};
|
||||
// Build rules lookup cache
|
||||
|
||||
|
||||
Ruler.prototype.__compile__ = function() {
|
||||
var self = this;
|
||||
var chains = [ "" ];
|
||||
@@ -3726,7 +3726,7 @@
|
||||
// Linkifier might send raw hostnames like "example.com", where url
|
||||
// starts with domain name. So we prepend http:// in those cases,
|
||||
// and remove it afterwards.
|
||||
|
||||
|
||||
if (!links[ln].schema) {
|
||||
urlText = state.md.normalizeLinkText("http://" + urlText).replace(/^http:\/\//, "");
|
||||
} else if (links[ln].schema === "mailto:" && !/^mailto:/i.test(urlText)) {
|
||||
@@ -3874,7 +3874,7 @@
|
||||
isSingle = t[0] === "'";
|
||||
// Find previous character,
|
||||
// default to space if it's the beginning of the line
|
||||
|
||||
|
||||
lastChar = 32;
|
||||
if (t.index - 1 >= 0) {
|
||||
lastChar = text.charCodeAt(t.index - 1);
|
||||
@@ -3890,7 +3890,7 @@
|
||||
}
|
||||
// Find next character,
|
||||
// default to space if it's the end of the line
|
||||
|
||||
|
||||
nextChar = 32;
|
||||
if (pos < max) {
|
||||
nextChar = text.charCodeAt(pos);
|
||||
@@ -4193,7 +4193,7 @@
|
||||
// re-export Token class to use in core rules
|
||||
StateCore.prototype.Token = token;
|
||||
var state_core = StateCore;
|
||||
var _rules$2 = [ [ "normalize", normalize ], [ "block", block ], [ "inline", inline ], [ "linkify", linkify$1 ], [ "replacements", replacements ], [ "smartquotes", smartquotes ],
|
||||
var _rules$2 = [ [ "normalize", normalize ], [ "block", block ], [ "inline", inline ], [ "linkify", linkify$1 ], [ "replacements", replacements ], [ "smartquotes", smartquotes ],
|
||||
// `text_join` finds `text_special` tokens (for escape sequences)
|
||||
// and joins them with the rest of the text
|
||||
[ "text_join", text_join ] ];
|
||||
@@ -4590,12 +4590,12 @@
|
||||
oldParentType = state.parentType;
|
||||
state.parentType = "blockquote";
|
||||
// Search the end of the block
|
||||
|
||||
|
||||
// Block ends with either:
|
||||
// 1. an empty line outside:
|
||||
// ```
|
||||
// > test
|
||||
|
||||
|
||||
// ```
|
||||
// 2. an empty line inside:
|
||||
// ```
|
||||
@@ -4712,7 +4712,7 @@
|
||||
oldTShift.push(state.tShift[nextLine]);
|
||||
oldSCount.push(state.sCount[nextLine]);
|
||||
// A negative indentation means that this is a paragraph continuation
|
||||
|
||||
|
||||
state.sCount[nextLine] = -1;
|
||||
}
|
||||
oldIndent = state.blkIndent;
|
||||
@@ -4905,9 +4905,9 @@
|
||||
}
|
||||
token.map = listLines = [ startLine, 0 ];
|
||||
token.markup = String.fromCharCode(markerCharCode);
|
||||
|
||||
|
||||
// Iterate list items
|
||||
|
||||
|
||||
nextLine = startLine;
|
||||
prevEmptyEnd = false;
|
||||
terminatorRules = state.md.block.ruler.getRules("list");
|
||||
@@ -4957,7 +4957,7 @@
|
||||
// - example list
|
||||
// ^ listIndent position will be here
|
||||
// ^ blkIndent position will be here
|
||||
|
||||
|
||||
oldListIndent = state.listIndent;
|
||||
state.listIndent = state.blkIndent;
|
||||
state.blkIndent = indent;
|
||||
@@ -4995,9 +4995,9 @@
|
||||
if (nextLine >= endLine) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Try to check if list is terminated or continued.
|
||||
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) {
|
||||
break;
|
||||
}
|
||||
@@ -5245,7 +5245,7 @@
|
||||
var HTML_OPEN_CLOSE_TAG_RE = html_re.HTML_OPEN_CLOSE_TAG_RE;
|
||||
// An array of opening and corresponding closing sequences for html tags,
|
||||
// last argument defines whether it can terminate a paragraph or not
|
||||
|
||||
|
||||
var HTML_SEQUENCES = [ [ /^<(script|pre|style|textarea)(?=(\s|>|$))/i, /<\/(script|pre|style|textarea)>/i, true ], [ /^<!--/, /-->/, true ], [ /^<\?/, /\?>/, true ], [ /^<![A-Z]/, />/, true ], [ /^<!\[CDATA\[/, /\]\]>/, true ], [ new RegExp("^</?(" + html_blocks.join("|") + ")(?=(\\s|/?>|$))", "i"), /^$/, true ], [ new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + "\\s*$"), /^$/, false ] ];
|
||||
var html_block = function html_block(state, startLine, endLine, silent) {
|
||||
var i, nextLine, token, lineText, pos = state.bMarks[startLine] + state.tShift[startLine], max = state.eMarks[startLine];
|
||||
@@ -5357,9 +5357,9 @@
|
||||
if (state.sCount[nextLine] - state.blkIndent > 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Check for underline in setext header
|
||||
|
||||
|
||||
if (state.sCount[nextLine] >= state.blkIndent) {
|
||||
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
||||
max = state.eMarks[nextLine];
|
||||
@@ -5456,9 +5456,9 @@
|
||||
// link to parser instance
|
||||
this.md = md;
|
||||
this.env = env;
|
||||
|
||||
|
||||
// Internal state vartiables
|
||||
|
||||
|
||||
this.tokens = tokens;
|
||||
this.bMarks = [];
|
||||
// line begin offsets for fast jumps
|
||||
@@ -5470,14 +5470,14 @@
|
||||
// indents for each line (tabs expanded)
|
||||
// An amount of virtual spaces (tabs expanded) between beginning
|
||||
// of each line (bMarks) and real beginning of that line.
|
||||
|
||||
|
||||
// It exists only as a hack because blockquotes override bMarks
|
||||
// losing information in the process.
|
||||
|
||||
|
||||
// It's used only when expanding tabs, you can think about it as
|
||||
// an initial tab length, e.g. bsCount=21 applied to string `\t123`
|
||||
// means first tab should be expanded to 4-21%4 === 3 spaces.
|
||||
|
||||
|
||||
this.bsCount = [];
|
||||
// block parser variables
|
||||
this.blkIndent = 0;
|
||||
@@ -5543,7 +5543,7 @@
|
||||
// don't count last fake line
|
||||
}
|
||||
// Push new token to "stream".
|
||||
|
||||
|
||||
StateBlock.prototype.push = function(type, tag, nesting) {
|
||||
var token$1 = new token(type, tag, nesting);
|
||||
token$1.block = true;
|
||||
@@ -5655,7 +5655,7 @@
|
||||
// re-export Token class to use in block rules
|
||||
StateBlock.prototype.Token = token;
|
||||
var state_block = StateBlock;
|
||||
var _rules$1 = [
|
||||
var _rules$1 = [
|
||||
// First 2 params - rule name & source. Secondary array - list of rules,
|
||||
// which can be terminated by this one.
|
||||
[ "table", table, [ "paragraph", "reference" ] ], [ "code", code ], [ "fence", fence, [ "paragraph", "reference", "blockquote", "list" ] ], [ "blockquote", blockquote, [ "paragraph", "reference", "blockquote", "list" ] ], [ "hr", hr, [ "paragraph", "reference", "blockquote", "list" ] ], [ "list", list, [ "paragraph", "reference", "blockquote" ] ], [ "reference", reference ], [ "html_block", html_block, [ "paragraph", "reference", "blockquote" ] ], [ "heading", heading, [ "paragraph", "reference", "blockquote" ] ], [ "lheading", lheading ], [ "paragraph", paragraph ] ];
|
||||
@@ -5675,7 +5675,7 @@
|
||||
}
|
||||
}
|
||||
// Generate tokens for input range
|
||||
|
||||
|
||||
ParserBlock.prototype.tokenize = function(state, startLine, endLine) {
|
||||
var ok, i, rules = this.ruler.getRules(""), len = rules.length, line = startLine, hasEmptyLines = false, maxNesting = state.md.options.maxNesting;
|
||||
while (line < endLine) {
|
||||
@@ -5696,7 +5696,7 @@
|
||||
}
|
||||
// Try all possible rules.
|
||||
// On success, rule should:
|
||||
|
||||
|
||||
// - update `state.line`
|
||||
// - update `state.tokens`
|
||||
// - return true
|
||||
@@ -5961,7 +5961,7 @@
|
||||
};
|
||||
// ~~strike through~~
|
||||
// Insert each marker as a separate text token, and add it to delimiter list
|
||||
|
||||
|
||||
var tokenize$1 = function strikethrough(state, silent) {
|
||||
var i, scanned, token, len, ch, start = state.pos, marker = state.src.charCodeAt(start);
|
||||
if (silent) {
|
||||
@@ -6027,9 +6027,9 @@
|
||||
// If a marker sequence has an odd number of characters, it's splitted
|
||||
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||
// start of the sequence.
|
||||
|
||||
|
||||
// So, we have to move all those markers after subsequent s_close tags.
|
||||
|
||||
|
||||
while (loneMarkers.length) {
|
||||
i = loneMarkers.pop();
|
||||
j = i + 1;
|
||||
@@ -6045,7 +6045,7 @@
|
||||
}
|
||||
}
|
||||
// Walk through delimiter list and replace text tokens with tags
|
||||
|
||||
|
||||
var postProcess_1$1 = function strikethrough(state) {
|
||||
var curr, tokens_meta = state.tokens_meta, max = state.tokens_meta.length;
|
||||
postProcess$1(state, state.delimiters);
|
||||
@@ -6061,7 +6061,7 @@
|
||||
};
|
||||
// Process *this* and _that_
|
||||
// Insert each marker as a separate text token, and add it to delimiter list
|
||||
|
||||
|
||||
var tokenize = function emphasis(state, silent) {
|
||||
var i, scanned, token, start = state.pos, marker = state.src.charCodeAt(start);
|
||||
if (silent) {
|
||||
@@ -6107,12 +6107,12 @@
|
||||
endDelim = delimiters[startDelim.end];
|
||||
// If the previous delimiter has the same marker and is adjacent to this one,
|
||||
// merge those into one strong delimiter.
|
||||
|
||||
|
||||
// `<em><em>whatever</em></em>` -> `<strong>whatever</strong>`
|
||||
|
||||
isStrong = i > 0 && delimiters[i - 1].end === startDelim.end + 1 &&
|
||||
|
||||
isStrong = i > 0 && delimiters[i - 1].end === startDelim.end + 1 &&
|
||||
// check that first two markers match and adjacent
|
||||
delimiters[i - 1].marker === startDelim.marker && delimiters[i - 1].token === startDelim.token - 1 &&
|
||||
delimiters[i - 1].marker === startDelim.marker && delimiters[i - 1].token === startDelim.token - 1 &&
|
||||
// check that last two markers are adjacent (we can safely assume they match)
|
||||
delimiters[startDelim.end + 1].token === endDelim.token + 1;
|
||||
ch = String.fromCharCode(startDelim.marker);
|
||||
@@ -6136,7 +6136,7 @@
|
||||
}
|
||||
}
|
||||
// Walk through delimiter list and replace text tokens with tags
|
||||
|
||||
|
||||
var postProcess_1 = function emphasis(state) {
|
||||
var curr, tokens_meta = state.tokens_meta, max = state.tokens_meta.length;
|
||||
postProcess(state, state.delimiters);
|
||||
@@ -6251,10 +6251,10 @@
|
||||
href = ref.href;
|
||||
title = ref.title;
|
||||
}
|
||||
|
||||
|
||||
// We found the end of the link, and know for a fact it's a valid link;
|
||||
// so all that's left to do is to call tokenizer.
|
||||
|
||||
|
||||
if (!silent) {
|
||||
state.pos = labelStart;
|
||||
state.posMax = labelEnd;
|
||||
@@ -6375,10 +6375,10 @@
|
||||
href = ref.href;
|
||||
title = ref.title;
|
||||
}
|
||||
|
||||
|
||||
// We found the end of the link, and know for a fact it's a valid link;
|
||||
// so all that's left to do is to call tokenizer.
|
||||
|
||||
|
||||
if (!silent) {
|
||||
content = state.src.slice(labelStart, labelEnd);
|
||||
state.md.inline.parse(content, state.md, state.env, tokens = []);
|
||||
@@ -6547,7 +6547,7 @@
|
||||
// markers belong to same delimiter run if:
|
||||
// - they have adjacent tokens
|
||||
// - AND markers are the same
|
||||
|
||||
|
||||
if (delimiters[headerIdx].marker !== closer.marker || lastTokenIdx !== closer.token - 1) {
|
||||
headerIdx = closerIdx;
|
||||
}
|
||||
@@ -6555,7 +6555,7 @@
|
||||
// Length is only used for emphasis-specific "rule of 3",
|
||||
// if it's not defined (in strikethrough or 3rd party plugins),
|
||||
// we can default it to 0 to disable those checks.
|
||||
|
||||
|
||||
closer.length = closer.length || 0;
|
||||
if (!closer.close) continue;
|
||||
// Previously calculated lower bounds (previous fails)
|
||||
@@ -6574,12 +6574,12 @@
|
||||
if (opener.open && opener.end < 0) {
|
||||
isOddMatch = false;
|
||||
// from spec:
|
||||
|
||||
|
||||
// If one of the delimiters can both open and close emphasis, then the
|
||||
// sum of the lengths of the delimiter runs containing the opening and
|
||||
// closing delimiters must not be a multiple of 3 unless both lengths
|
||||
// are multiples of 3.
|
||||
|
||||
|
||||
if (opener.close || closer.open) {
|
||||
if ((opener.length + closer.length) % 3 === 0) {
|
||||
if (opener.length % 3 !== 0 || closer.length % 3 !== 0) {
|
||||
@@ -6678,7 +6678,7 @@
|
||||
this.linkLevel = 0;
|
||||
}
|
||||
// Flush pending text
|
||||
|
||||
|
||||
StateInline.prototype.pushPending = function() {
|
||||
var token$1 = new token("text", "", 0);
|
||||
token$1.content = this.pending;
|
||||
@@ -6689,7 +6689,7 @@
|
||||
};
|
||||
// Push new token to "stream".
|
||||
// If pending text exists - flush it as text token
|
||||
|
||||
|
||||
StateInline.prototype.push = function(type, tag, nesting) {
|
||||
if (this.pending) {
|
||||
this.pushPending();
|
||||
@@ -6718,10 +6718,10 @@
|
||||
};
|
||||
// Scan a sequence of emphasis-like markers, and determine whether
|
||||
// it can start an emphasis sequence or end an emphasis sequence.
|
||||
|
||||
|
||||
// - start - position to scan from (it should point at a valid marker);
|
||||
// - canSplitWord - determine if these markers can be found inside a word
|
||||
|
||||
|
||||
StateInline.prototype.scanDelims = function(start, canSplitWord) {
|
||||
var pos = start, lastChar, nextChar, count, can_open, can_close, isLastWhiteSpace, isLastPunctChar, isNextWhiteSpace, isNextPunctChar, left_flanking = true, right_flanking = true, max = this.posMax, marker = this.src.charCodeAt(start);
|
||||
// treat beginning of the line as a whitespace
|
||||
@@ -6771,10 +6771,10 @@
|
||||
var _rules = [ [ "text", text ], [ "linkify", linkify ], [ "newline", newline ], [ "escape", _escape ], [ "backticks", backticks ], [ "strikethrough", strikethrough.tokenize ], [ "emphasis", emphasis.tokenize ], [ "link", link ], [ "image", image ], [ "autolink", autolink ], [ "html_inline", html_inline ], [ "entity", entity ] ];
|
||||
// `rule2` ruleset was created specifically for emphasis/strikethrough
|
||||
// post-processing and may be changed in the future.
|
||||
|
||||
|
||||
// Don't use this for anything except pairs (plugins working with `balance_pairs`).
|
||||
|
||||
var _rules2 = [ [ "balance_pairs", balance_pairs ], [ "strikethrough", strikethrough.postProcess ], [ "emphasis", emphasis.postProcess ],
|
||||
|
||||
var _rules2 = [ [ "balance_pairs", balance_pairs ], [ "strikethrough", strikethrough.postProcess ], [ "emphasis", emphasis.postProcess ],
|
||||
// rules for pairs separate '**' into its own text tokens, which may be left unused,
|
||||
// rule below merges unused segments back with the rest of the text
|
||||
[ "fragments_join", fragments_join ] ];
|
||||
@@ -6802,7 +6802,7 @@
|
||||
}
|
||||
// Skip single token by running all rules in validation mode;
|
||||
// returns `true` if any rule reported success
|
||||
|
||||
|
||||
ParserInline.prototype.skipToken = function(state) {
|
||||
var ok, i, pos = state.pos, rules = this.ruler.getRules(""), len = rules.length, maxNesting = state.md.options.maxNesting, cache = state.cache;
|
||||
if (typeof cache[pos] !== "undefined") {
|
||||
@@ -6837,7 +6837,7 @@
|
||||
cache[pos] = state.pos;
|
||||
};
|
||||
// Generate tokens for input range
|
||||
|
||||
|
||||
ParserInline.prototype.tokenize = function(state) {
|
||||
var ok, i, rules = this.ruler.getRules(""), len = rules.length, end = state.posMax, maxNesting = state.md.options.maxNesting;
|
||||
while (state.pos < end) {
|
||||
@@ -6928,11 +6928,11 @@
|
||||
re.src_xn = "xn--[a-z0-9\\-]{1,59}";
|
||||
// More to read about domain names
|
||||
// http://serverfault.com/questions/638260/
|
||||
re.src_domain_root =
|
||||
re.src_domain_root =
|
||||
// Allow letters & digits (http://test1)
|
||||
"(?:" + re.src_xn + "|" + re.src_pseudo_letter + "{1,63}" + ")";
|
||||
re.src_domain = "(?:" + re.src_xn + "|" + "(?:" + re.src_pseudo_letter + ")" + "|" + "(?:" + re.src_pseudo_letter + "(?:-|" + re.src_pseudo_letter + "){0,61}" + re.src_pseudo_letter + ")" + ")";
|
||||
re.src_host = "(?:" +
|
||||
re.src_host = "(?:" +
|
||||
// Don't need IP check, because digits are already allowed in normal domain names
|
||||
// src_ip4 +
|
||||
// '|' +
|
||||
@@ -6949,11 +6949,11 @@
|
||||
// Rude test fuzzy links by host, for quick deny
|
||||
re.tpl_host_fuzzy_test = "localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:" + re.src_ZPCc + "|>|$))";
|
||||
re.tpl_email_fuzzy = "(^|" + text_separators + '|"|\\(|' + re.src_ZCc + ")" + "(" + re.src_email_name + "@" + re.tpl_host_fuzzy_strict + ")";
|
||||
re.tpl_link_fuzzy =
|
||||
re.tpl_link_fuzzy =
|
||||
// Fuzzy link can't be prepended with .:/\- and non punctuation.
|
||||
// but can start with > (markdown blockquote)
|
||||
"(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|" + re.src_ZPCc + "))" + "((?![$+<=>^`|\uff5c])" + re.tpl_host_port_fuzzy_strict + re.src_path + ")";
|
||||
re.tpl_link_no_ip_fuzzy =
|
||||
re.tpl_link_no_ip_fuzzy =
|
||||
// Fuzzy link can't be prepended with .:/\- and non punctuation.
|
||||
// but can start with > (markdown blockquote)
|
||||
"(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|" + re.src_ZPCc + "))" + "((?![$+<=>^`|\uff5c])" + re.tpl_host_port_no_ip_fuzzy_strict + re.src_path + ")";
|
||||
@@ -6962,7 +6962,7 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Helpers
|
||||
// Merge objects
|
||||
|
||||
|
||||
function assign(obj /*from1, from2, from3, ...*/) {
|
||||
var sources = Array.prototype.slice.call(arguments, 1);
|
||||
sources.forEach((function(source) {
|
||||
@@ -7025,7 +7025,7 @@
|
||||
var tail = text.slice(pos);
|
||||
if (!self.re.no_http) {
|
||||
// compile lazily, because "host"-containing variables can change on tlds update.
|
||||
self.re.no_http = new RegExp("^" + self.re.src_auth +
|
||||
self.re.no_http = new RegExp("^" + self.re.src_auth +
|
||||
// Don't allow single-level domains, because of false positives like '//test'
|
||||
// with code comments
|
||||
"(?:localhost|(?:(?:" + self.re.src_domain + ")\\.)+" + self.re.src_domain_root + ")" + self.re.src_port + self.re.src_host_terminator + self.re.src_path, "i");
|
||||
@@ -7082,7 +7082,7 @@
|
||||
};
|
||||
}
|
||||
// Schemas compiler. Build regexps.
|
||||
|
||||
|
||||
function compile(self) {
|
||||
// Load & clone RE patterns.
|
||||
var re$1 = self.re = re(self.__opts__);
|
||||
@@ -7101,9 +7101,9 @@
|
||||
re$1.link_fuzzy = RegExp(untpl(re$1.tpl_link_fuzzy), "i");
|
||||
re$1.link_no_ip_fuzzy = RegExp(untpl(re$1.tpl_link_no_ip_fuzzy), "i");
|
||||
re$1.host_fuzzy_test = RegExp(untpl(re$1.tpl_host_fuzzy_test), "i");
|
||||
|
||||
|
||||
// Compile each schema
|
||||
|
||||
|
||||
var aliases = [];
|
||||
self.__compiled__ = {};
|
||||
// Reset compiled data
|
||||
@@ -7144,9 +7144,9 @@
|
||||
}
|
||||
schemaError(name, val);
|
||||
}));
|
||||
|
||||
|
||||
// Compile postponed aliases
|
||||
|
||||
|
||||
aliases.forEach((function(alias) {
|
||||
if (!self.__compiled__[self.__schemas__[alias]]) {
|
||||
// Silently fail on missed schemas to avoid errons on disable.
|
||||
@@ -7156,16 +7156,16 @@
|
||||
self.__compiled__[alias].validate = self.__compiled__[self.__schemas__[alias]].validate;
|
||||
self.__compiled__[alias].normalize = self.__compiled__[self.__schemas__[alias]].normalize;
|
||||
}));
|
||||
|
||||
|
||||
// Fake record for guessed links
|
||||
|
||||
|
||||
self.__compiled__[""] = {
|
||||
validate: null,
|
||||
normalize: createNormalizer()
|
||||
};
|
||||
|
||||
|
||||
// Build schema condition
|
||||
|
||||
|
||||
var slist = Object.keys(self.__compiled__).filter((function(name) {
|
||||
// Filter disabled & fake schemas
|
||||
return name.length > 0 && self.__compiled__[name];
|
||||
@@ -7175,9 +7175,9 @@
|
||||
self.re.schema_search = RegExp("(^|(?!_)(?:[><\uff5c]|" + re$1.src_ZPCc + "))(" + slist + ")", "ig");
|
||||
self.re.schema_at_start = RegExp("^" + self.re.schema_search.source, "i");
|
||||
self.re.pretest = RegExp("(" + self.re.schema_test.source + ")|(" + self.re.host_fuzzy_test.source + ")|@", "i");
|
||||
|
||||
|
||||
// Cleanup
|
||||
|
||||
|
||||
resetScanCache(self);
|
||||
}
|
||||
/**
|
||||
@@ -7673,7 +7673,7 @@
|
||||
* @returns {String} The resulting string of Unicode symbols.
|
||||
*/ function decode(input) {
|
||||
// Don't use UCS-2
|
||||
var output = [], inputLength = input.length, out, i = 0, n = initialN, bias = initialBias, basic, j, index, oldi, w, k, digit, t,
|
||||
var output = [], inputLength = input.length, out, i = 0, n = initialN, bias = initialBias, basic, j, index, oldi, w, k, digit, t,
|
||||
/** Cached calculation results */
|
||||
baseMinusT;
|
||||
// Handle the basic code points: let `basic` be the number of input code
|
||||
@@ -7738,9 +7738,9 @@
|
||||
* @param {String} input The string of Unicode symbols.
|
||||
* @returns {String} The resulting Punycode string of ASCII-only symbols.
|
||||
*/ function encode(input) {
|
||||
var n, delta, handledCPCount, basicLength, bias, j, m, q, k, t, currentValue, output = [],
|
||||
var n, delta, handledCPCount, basicLength, bias, j, m, q, k, t, currentValue, output = [],
|
||||
/** `inputLength` will hold the number of code points in `input`. */
|
||||
inputLength,
|
||||
inputLength,
|
||||
/** Cached calculation results */
|
||||
handledCPCountPlusOne, baseMinusT, qMinusT;
|
||||
// Convert the input in UCS-2 to Unicode
|
||||
@@ -7993,13 +7993,13 @@
|
||||
commonmark: commonmark
|
||||
};
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// This validator can prohibit more than really needed to prevent XSS. It's a
|
||||
// tradeoff to keep code simple and to be secure by default.
|
||||
|
||||
|
||||
// If you need different setup - override validator method as you wish. Or
|
||||
// replace it with dummy function and use external sanitizer.
|
||||
|
||||
|
||||
var BAD_PROTO_RE = /^(vbscript|javascript|file|data):/;
|
||||
var GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/;
|
||||
function validateLink(url) {
|
||||
@@ -614,8 +614,10 @@ var Org = (function () {
|
||||
var notBlankNextToken = this.lexer.peekNextToken();
|
||||
if (blankToken && !notBlankNextToken.isListElement())
|
||||
this.lexer.pushToken(blankToken); // Recover blank token only when next line is not listElement.
|
||||
if (notBlankNextToken.indentation <= rootIndentation)
|
||||
break; // end of the list
|
||||
// End of the list if hit less indented line or end of directive
|
||||
if (notBlankNextToken.indentation <= rootIndentation ||
|
||||
(notBlankNextToken.type === Lexer.tokens.directive && notBlankNextToken.endDirective))
|
||||
break;
|
||||
|
||||
var element = this.parseElement(); // recursive
|
||||
if (element)
|
||||
5
src/khoj/interface/web/assets/pico.min.css
vendored
Normal file
171
src/khoj/interface/web/base_config.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
|
||||
<title>Khoj - Settings</title>
|
||||
<link rel="stylesheet" href="/static/assets/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/assets/khoj.css">
|
||||
</head>
|
||||
<body class="khoj-configure">
|
||||
<div class="khoj-header-wrapper">
|
||||
<div class="filler"></div>
|
||||
<div class="khoj-header">
|
||||
<a class="khoj-logo" href="https://lantern.khoj.dev" target="_blank">
|
||||
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
||||
</a>
|
||||
<nav class="khoj-nav">
|
||||
<a class="khoj-nav" href="/chat">Chat</a>
|
||||
<a class="khoj-nav" href="/">Search</a>
|
||||
<a class="khoj-nav khoj-nav-selected" href="/config">Settings</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="filler"></div>
|
||||
</div>
|
||||
<div class=”content”>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr auto auto auto minmax(80px, 100%);
|
||||
}
|
||||
body > * {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
div.filler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.khoj-configure {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img.khoj-logo {
|
||||
max-width: none!important;
|
||||
}
|
||||
div.khoj-header-wrapper{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(70vw, 100%) 1fr;
|
||||
}
|
||||
.page {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
gap: 32px;
|
||||
}
|
||||
.section {
|
||||
display: grid;
|
||||
justify-self: center;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
padding: 0 0 16px 0;
|
||||
font-size: 32;
|
||||
font-weight: normal;
|
||||
}
|
||||
.section-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
justify-items: start;
|
||||
}
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 24px 16px;
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
background: white;
|
||||
border: 1px solid rgb(229, 229, 229);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.1),0px 1px 2px -1px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-title-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
.card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-self: center;
|
||||
}
|
||||
.card-title-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.card-description {
|
||||
margin: 0;
|
||||
color: grey;
|
||||
font-size: 16px;
|
||||
}
|
||||
.card-button-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.card-button {
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
color: rgb(64,64,64);
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
text-align: right;
|
||||
}
|
||||
.primary-button {
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button.card-button {
|
||||
color: rgb(255, 136, 136);
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img.configured-icon {
|
||||
max-width: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.section-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
27
src/khoj/interface/web/base_processor_integration.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Khoj: Processor Settings</title>
|
||||
<link rel=”stylesheet” href=”static/styles.css”>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
|
||||
</head>
|
||||
<body class="data-integration">
|
||||
<header class=”header”>
|
||||
<h1>Configure your processor integrations for Khoj</h1>
|
||||
</header>
|
||||
<a href="/config">Go back</a>
|
||||
|
||||
<div class=”content”>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class=”footer”>
|
||||
</footer>
|
||||
</body>
|
||||
<style>
|
||||
body.data-integration {
|
||||
padding: 0 10%
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
463
src/khoj/interface/web/chat.html
Normal file
@@ -0,0 +1,463 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<title>Khoj - Chat</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
|
||||
<link rel="manifest" href="/static/khoj_chat.webmanifest">
|
||||
<link rel="stylesheet" href="/static/assets/khoj.css">
|
||||
</head>
|
||||
<script>
|
||||
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(reference, index) {
|
||||
// Escape reference for HTML rendering
|
||||
let escaped_ref = reference.replaceAll('"', '"');
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
return `<sup><abbr title="${escaped_ref}" tabindex="0">${index}</abbr></sup>`;
|
||||
}
|
||||
|
||||
function renderMessage(message, by, dt=null) {
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
// Generate HTML for Chat Message and Append to Chat Body
|
||||
document.getElementById("chat-body").innerHTML += `
|
||||
<div data-meta="${by_name} at ${message_time}" class="chat-message ${by}">
|
||||
<div class="chat-message-text ${by}">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
// Scroll to bottom of chat-body element
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
}
|
||||
|
||||
function renderMessageWithReference(message, by, context=null, dt=null) {
|
||||
let references = '';
|
||||
if (context) {
|
||||
references = context
|
||||
.map((reference, index) => generateReference(reference, index))
|
||||
.join("<sup>,</sup>");
|
||||
}
|
||||
|
||||
renderMessage(message+references, by, dt);
|
||||
}
|
||||
|
||||
function chat() {
|
||||
// Extract required fields for search from form
|
||||
let query = document.getElementById("chat-input").value.trim();
|
||||
let results_count = localStorage.getItem("khojResultsCount") || 5;
|
||||
console.log(`Query: ${query}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
if (query.length === 0)
|
||||
return;
|
||||
|
||||
// Add message by user to chat body
|
||||
renderMessage(query, "you");
|
||||
document.getElementById("chat-input").value = "";
|
||||
|
||||
// Generate backend API URL to execute query
|
||||
let url = `/api/chat?q=${encodeURIComponent(query)}&n=${results_count}&client=web&stream=true`;
|
||||
|
||||
let chat_body = document.getElementById("chat-body");
|
||||
let new_response = document.createElement("div");
|
||||
new_response.classList.add("chat-message", "khoj");
|
||||
new_response.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||
chat_body.appendChild(new_response);
|
||||
|
||||
let new_response_text = document.createElement("div");
|
||||
new_response_text.classList.add("chat-message-text", "khoj");
|
||||
new_response.appendChild(new_response_text);
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
new_response_text.innerHTML = "🤔";
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
|
||||
// Call specified Khoj API which returns a streamed response of type text/plain
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function readStream() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
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];
|
||||
new_response_text.innerHTML += additionalResponse;
|
||||
|
||||
const rawReference = chunk.split("### compiled references:")[1];
|
||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||
let polishedReference = rawReferenceAsJson.map((reference, index) => generateReference(reference, index))
|
||||
.join("<sup>,</sup>");
|
||||
|
||||
new_response_text.innerHTML += polishedReference;
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
} else {
|
||||
// Display response from Khoj
|
||||
if (new_response_text.innerHTML === "🤔") {
|
||||
// Clear temporary status message
|
||||
new_response_text.innerHTML = "";
|
||||
}
|
||||
new_response_text.innerHTML += chunk;
|
||||
readStream();
|
||||
}
|
||||
|
||||
// Scroll to bottom of chat window as chat response is streamed
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
});
|
||||
}
|
||||
readStream();
|
||||
});
|
||||
}
|
||||
|
||||
function incrementalChat(event) {
|
||||
// Send chat message on 'Enter'
|
||||
if (event.key === 'Enter') {
|
||||
chat();
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
fetch('/api/chat/history?client=web')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.detail) {
|
||||
// If the server returns a 500 error with detail, render it as a message.
|
||||
renderMessage("Hi 👋🏾, to get started <br/>1. Get your <a class='inline-chat-link' href='https://platform.openai.com/account/api-keys'>OpenAI API key</a><br/>2. Save it in the Khoj <a class='inline-chat-link' href='/config/processor/conversation'>chat settings</a> <br/>3. Click Configure on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>", "khoj");
|
||||
|
||||
// Disable chat input field and update placeholder text
|
||||
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
||||
document.getElementById("chat-input").setAttribute("placeholder", "Configure Khoj to enable chat");
|
||||
} else {
|
||||
// Set welcome message on load
|
||||
renderMessage("Hey 👋🏾, what's up?", "khoj");
|
||||
}
|
||||
return data.response;
|
||||
})
|
||||
.then(response => {
|
||||
// Render conversation history, if any
|
||||
response.forEach(chat_log => {
|
||||
renderMessageWithReference(chat_log.message, chat_log.by, chat_log.context, new Date(chat_log.created));
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
|
||||
// Fill query field with value passed in URL query parameters, if any.
|
||||
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
||||
if (query_via_url) {
|
||||
document.getElementById("chat-input").value = query_via_url;
|
||||
chat();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
<!--Add Header Logo and Nav Pane-->
|
||||
<div class="khoj-header">
|
||||
{% if demo %}
|
||||
<!-- Banner linking to https://khoj.dev -->
|
||||
<div class="khoj-banner-container">
|
||||
<a class="khoj-banner" href="https://khoj.dev" target="_blank">
|
||||
<p id="khoj-banner" class="khoj-banner">
|
||||
Enroll in Khoj cloud to get your own Github assistant
|
||||
</p>
|
||||
</a>
|
||||
<input type="text" id="khoj-banner-email" placeholder="email" class="khoj-banner-email"></input>
|
||||
<button id="khoj-banner-submit" class="khoj-banner-button">Submit</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if demo %}
|
||||
<a class="khoj-logo" href="https://khoj.dev" target="_blank">
|
||||
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="khoj-logo" href="/">
|
||||
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
||||
</a>
|
||||
{% endif %}
|
||||
<nav class="khoj-nav">
|
||||
<a class="khoj-nav khoj-nav-selected" href="/chat">Chat</a>
|
||||
<a class="khoj-nav" href="/">Search</a>
|
||||
{% if not demo %}
|
||||
<a class="khoj-nav" href="/config">Settings</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Chat Body -->
|
||||
<div id="chat-body"></div>
|
||||
|
||||
<!-- Chat Footer -->
|
||||
<div id="chat-footer">
|
||||
<input type="text" id="chat-input" class="option" onkeyup=incrementalChat(event) autofocus="autofocus" placeholder="What is the meaning of life?">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
body {
|
||||
display: grid;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
font-family: roboto, karma, segoe ui, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
body > * {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
#chat-body {
|
||||
font-size: medium;
|
||||
margin: 0px;
|
||||
line-height: 20px;
|
||||
overflow-y: scroll; /* Make chat body scroll to see history */
|
||||
}
|
||||
/* add chat metatdata to bottom of bubble */
|
||||
.chat-message::after {
|
||||
content: attr(data-meta);
|
||||
display: block;
|
||||
font-size: x-small;
|
||||
color: #475569;
|
||||
margin: -8px 4px 0 -5px;
|
||||
}
|
||||
/* move message by khoj to left */
|
||||
.chat-message.khoj {
|
||||
margin-left: auto;
|
||||
text-align: left;
|
||||
}
|
||||
/* move message by you to right */
|
||||
.chat-message.you {
|
||||
margin-right: auto;
|
||||
text-align: right;
|
||||
}
|
||||
/* basic style chat message text */
|
||||
.chat-message-text {
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
text-align: left;
|
||||
}
|
||||
/* color chat bubble by khoj blue */
|
||||
.chat-message-text.khoj {
|
||||
color: var(--primary-inverse);
|
||||
background: var(--primary);
|
||||
margin-left: auto;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* add left protrusion to khoj chat bubble */
|
||||
.chat-message-text.khoj:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: -7px;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: var(--primary);
|
||||
border-bottom: 0;
|
||||
transform: rotate(-60deg);
|
||||
}
|
||||
/* color chat bubble by you dark grey */
|
||||
.chat-message-text.you {
|
||||
color: #f8fafc;
|
||||
background: #475569;
|
||||
margin-right: auto;
|
||||
}
|
||||
/* add right protrusion to you chat bubble */
|
||||
.chat-message-text.you:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 91%;
|
||||
right: -2px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: #475569;
|
||||
border-right: 0;
|
||||
margin-top: -10px;
|
||||
transform: rotate(-60deg)
|
||||
}
|
||||
|
||||
#chat-footer {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(70px, 100%);
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
#chat-footer > * {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background: #f9fafc
|
||||
}
|
||||
.option:hover {
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
}
|
||||
#chat-input {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
a.inline-chat-link {
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #475569;
|
||||
}
|
||||
|
||||
@media (pointer: coarse), (hover: none) {
|
||||
abbr[title] {
|
||||
position: relative;
|
||||
padding-left: 4px; /* space references out to ease tapping */
|
||||
}
|
||||
abbr[title]:focus:after {
|
||||
content: attr(title);
|
||||
|
||||
/* position tooltip */
|
||||
position: absolute;
|
||||
left: 16px; /* open tooltip to right of ref link, instead of on top of it */
|
||||
width: auto;
|
||||
z-index: 1; /* show tooltip above chat messages */
|
||||
|
||||
/* style tooltip */
|
||||
background-color: #aaa;
|
||||
color: #f8fafc;
|
||||
border-radius: 2px;
|
||||
box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.4);
|
||||
font-size: 14px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto minmax(80px, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 1;
|
||||
}
|
||||
#chat-footer {
|
||||
padding: 0;
|
||||
margin: 4px;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
a.khoj-banner {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
body {
|
||||
grid-template-columns: auto min(70vw, 100%) auto;
|
||||
grid-template-rows: auto minmax(80px, 100%) auto;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
div.khoj-banner-container {
|
||||
background: linear-gradient(-45deg, #FFC107, #FF9800, #FF5722, #FF9800, #FFC107);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
a.khoj-banner {
|
||||
color: black;
|
||||
}
|
||||
|
||||
a.khoj-logo {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.khoj-banner {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button#khoj-banner-submit,
|
||||
input#khoj-banner-email {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background: #f9fafc;
|
||||
}
|
||||
|
||||
button#khoj-banner-submit:hover,
|
||||
input#khoj-banner-email:hover {
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
}
|
||||
|
||||
p#khoj-banner {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
a.khoj-banner {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var khojBannerSubmit = document.getElementById("khoj-banner-submit");
|
||||
|
||||
khojBannerSubmit?.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
var email = document.getElementById("khoj-banner-email").value;
|
||||
fetch("https://lantern.khoj.dev/beta/users/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: email
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
console.log(data);
|
||||
if (data.user != null) {
|
||||
document.getElementById("khoj-banner").innerHTML = "Thanks for signing up. We'll be in touch soon! 🚀";
|
||||
document.getElementById("khoj-banner-submit").remove();
|
||||
} else {
|
||||
document.getElementById("khoj-banner").innerHTML = "There was an error signing up. Please contact team@khoj.dev";
|
||||
}
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
document.getElementById("khoj-banner").innerHTML = "There was an error signing up. Please contact team@khoj.dev";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
329
src/khoj/interface/web/config.html
Normal file
@@ -0,0 +1,329 @@
|
||||
{% extends "base_config.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<h2 class="section-title">Plugins</h2>
|
||||
<div class="section-cards">
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/github.svg" alt="Github">
|
||||
<h3 class="card-title">
|
||||
Github
|
||||
{% if current_config.content_type.github %}
|
||||
<img id="configured-icon-github" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p class="card-description">Set repositories for Khoj to index</p>
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<a class="card-button" href="/config/content_type/github">
|
||||
{% if current_config.content_type.github %}
|
||||
Update
|
||||
{% else %}
|
||||
Setup
|
||||
{% endif %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_config.content_type.github %}
|
||||
<div id="clear-github" class="card-action-row">
|
||||
<button class="card-button" onclick="clearContentType('github')">
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/notion.svg" alt="Notion">
|
||||
<h3 class="card-title">
|
||||
Notion
|
||||
{% if current_config.content_type.notion %}
|
||||
<img id="configured-icon-notion" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p class="card-description">Configure your settings from Notion</p>
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<a class="card-button" href="/config/content_type/notion">
|
||||
{% if current_config.content_type.content %}
|
||||
Update
|
||||
{% else %}
|
||||
Setup
|
||||
{% endif %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_config.content_type.notion %}
|
||||
<div id="clear-notion" class="card-action-row">
|
||||
<button class="card-button" onclick="clearContentType('notion')">
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/markdown.svg" alt="markdown">
|
||||
<h3 class="card-title">
|
||||
Markdown
|
||||
{% if current_config.content_type.markdown %}
|
||||
<img id="configured-icon-markdown" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p class="card-description">Set markdown files for Khoj to index</p>
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<a class="card-button" href="/config/content_type/markdown">
|
||||
{% if current_config.content_type.markdown %}
|
||||
Update
|
||||
{% else %}
|
||||
Setup
|
||||
{% endif %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_config.content_type.markdown %}
|
||||
<div id="clear-markdown" class="card-action-row">
|
||||
<button class="card-button" onclick="clearContentType('markdown')">
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/org.svg" alt="org">
|
||||
<h3 class="card-title">
|
||||
Org
|
||||
{% if current_config.content_type.org %}
|
||||
<img id="configured-icon-org" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p class="card-description">Set org files for Khoj to index</p>
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<a class="card-button" href="/config/content_type/org">
|
||||
{% if current_config.content_type.org %}
|
||||
Update
|
||||
{% else %}
|
||||
Setup
|
||||
{% endif %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_config.content_type.org %}
|
||||
<div id="clear-org" class="card-action-row">
|
||||
<button class="card-button" onclick="clearContentType('org')">
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/pdf.svg" alt="PDF">
|
||||
<h3 class="card-title">
|
||||
PDF
|
||||
{% if current_config.content_type.pdf %}
|
||||
<img id="configured-icon-pdf" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p class="card-description">Set PDF files for Khoj to index</p>
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<a class="card-button" href="/config/content_type/pdf">
|
||||
{% if current_config.content_type.pdf %}
|
||||
Update
|
||||
{% else %}
|
||||
Setup
|
||||
{% endif %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_config.content_type.pdf %}
|
||||
<div id="clear-pdf" class="card-action-row">
|
||||
<button class="card-button" onclick="clearContentType('pdf')">
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2 class="section-title">Features</h2>
|
||||
<div class="section-cards">
|
||||
<div class="card">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat">
|
||||
<h3 class="card-title">
|
||||
Chat
|
||||
{% if current_config.processor and current_config.processor.conversation %}
|
||||
<img id="configured-icon-conversation-processor" class="configured-icon" src="/static/assets/icons/confirm-icon.svg" alt="Configured">
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p class="card-description">Setup Khoj Chat with OpenAI</p>
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<a class="card-button" href="/config/processor/conversation">
|
||||
{% if current_config.processor and current_config.processor.conversation %}
|
||||
Update
|
||||
{% else %}
|
||||
Setup
|
||||
{% endif %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
{% if current_config.processor and current_config.processor.conversation %}
|
||||
<div id="clear-conversation" class="card-action-row">
|
||||
<button class="card-button" onclick="clearConversationProcessor()">
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div id="results-count" title="Number of items to show in search and use for chat response">
|
||||
<label for="results-count-slider">Results Count: <span id="results-count-value">5</span></label>
|
||||
<input type="range" id="results-count-slider" name="results-count-slider" min="1" max="10" step="1" value="5">
|
||||
</div>
|
||||
<div id="status" style="display: none;"></div>
|
||||
<button id="configure" type="submit" title="Update index with the latest changes">⚙️ Configure</button>
|
||||
<button id="reinitialize" type="submit" title="Regenerate index from scratch">🔄 Reinitialize</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function clearContentType(content_type) {
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
fetch('/api/delete/config/data/content_type/' + content_type, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status == "ok") {
|
||||
var contentTypeClearButton = document.getElementById("clear-" + content_type);
|
||||
contentTypeClearButton.style.display = "none";
|
||||
|
||||
var configuredIcon = document.getElementById("configured-icon-" + content_type);
|
||||
configuredIcon.style.display = "none";
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
function clearConversationProcessor() {
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
fetch('/api/delete/config/data/processor/conversation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status == "ok") {
|
||||
var conversationClearButton = document.getElementById("clear-conversation");
|
||||
conversationClearButton.style.display = "none";
|
||||
|
||||
var configuredIcon = document.getElementById("configured-icon-conversation-processor");
|
||||
configuredIcon.style.display = "none";
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
var configure = document.getElementById("configure");
|
||||
configure.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
updateIndex(
|
||||
force=false,
|
||||
successText="Configured successfully!",
|
||||
errorText="Unable to configure. Raise issue on Khoj <a href='https://github.com/khoj-ai/khoj/issues'>Github</a> or <a href='https://discord.gg/BDgyabRM6e'>Discord</a>.",
|
||||
button=configure,
|
||||
loadingText="Configuring...",
|
||||
emoji="⚙️");
|
||||
});
|
||||
|
||||
var reinitialize = document.getElementById("reinitialize");
|
||||
reinitialize.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
updateIndex(
|
||||
force=true,
|
||||
successText="Reinitialized successfully!",
|
||||
errorText="Unable to reinitialize. Raise issue on Khoj <a href='https://github.com/khoj-ai/khoj/issues'>Github</a> or <a href='https://discord.gg/BDgyabRM6e'>Discord</a>.",
|
||||
button=reinitialize,
|
||||
loadingText="Reinitializing...",
|
||||
emoji="🔄");
|
||||
});
|
||||
|
||||
function updateIndex(force, successText, errorText, button, loadingText, emoji) {
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
button.disabled = true;
|
||||
button.innerHTML = emoji + loadingText;
|
||||
fetch('/api/update?&client=web&force=' + force, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
if (data.detail != null) {
|
||||
throw new Error(data.detail);
|
||||
}
|
||||
document.getElementById("status").innerHTML = emoji + successText;
|
||||
document.getElementById("status").style.display = "block";
|
||||
button.disabled = false;
|
||||
button.innerHTML = '✅ Done!';
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
document.getElementById("status").innerHTML = emoji + errorText
|
||||
document.getElementById("status").style.display = "block";
|
||||
button.disabled = false;
|
||||
button.innerHTML = '⚠️ Unsuccessful';
|
||||
});
|
||||
}
|
||||
|
||||
// Setup the results count slider
|
||||
const resultsCountSlider = document.getElementById('results-count-slider');
|
||||
const resultsCountValue = document.getElementById('results-count-value');
|
||||
|
||||
// Set the initial value of the slider
|
||||
resultsCountValue.textContent = resultsCountSlider.value;
|
||||
|
||||
// Store the slider value in localStorage when it changes
|
||||
resultsCountSlider.addEventListener('input', () => {
|
||||
resultsCountValue.textContent = resultsCountSlider.value;
|
||||
localStorage.setItem('khojResultsCount', resultsCountSlider.value);
|
||||
});
|
||||
|
||||
// Get the slider value from localStorage on page load
|
||||
const storedResultsCount = localStorage.getItem('khojResultsCount');
|
||||
if (storedResultsCount) {
|
||||
resultsCountSlider.value = storedResultsCount;
|
||||
resultsCountValue.textContent = storedResultsCount;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
170
src/khoj/interface/web/content_type_github_input.html
Normal file
@@ -0,0 +1,170 @@
|
||||
{% extends "base_config.html" %}
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<img class="card-icon" src="/static/assets/icons/github.svg" alt="Github">
|
||||
<span class="card-title-text">Github</span>
|
||||
</h2>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="pat-token">Personal Access Token</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="pat-token" name="pat" value="{{ current_config['pat_token'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>Repositories</h4>
|
||||
<div id="repositories" class="section-cards">
|
||||
{% for repo in current_config['repos'] %}
|
||||
<div class="card repo" id="repo-card-{{loop.index}}">
|
||||
<label for="repo-owner">Repository Owner</label>
|
||||
<input type="text" id="repo-owner-{{loop.index}}" name="repo_owner" value="{{ repo.owner }}">
|
||||
<label for="repo-name">Repository Name</label>
|
||||
<input type="text" id="repo-name-{{loop.index}}" name="repo_name" value="{{ repo.name}}">
|
||||
<label for="repo-branch">Repository Branch</label>
|
||||
<input type="text" id="repo-branch-{{loop.index}}" name="repo_branch" value="{{ repo.branch }}">
|
||||
<button type="button"
|
||||
class="remove-repo-button"
|
||||
onclick="remove_repo({{loop.index}})"
|
||||
id="remove-repo-button-{{loop.index}}">Remove Repository</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" id="add-repository-button">Add Repository</button>
|
||||
<table style="display: none;" >
|
||||
<tr>
|
||||
<td>
|
||||
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="embeddings-file">Embeddings File (Output)</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="section">
|
||||
<div id="success" style="display: none;"></div>
|
||||
<button id="submit" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
div.repo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-template-rows: none;
|
||||
}
|
||||
div#repositories {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
button.remove-repo-button {
|
||||
background-color: gainsboro;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const add_repo_button = document.getElementById("add-repository-button");
|
||||
add_repo_button.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
var repo = document.createElement("div");
|
||||
repo.classList.add("card");
|
||||
repo.classList.add("repo");
|
||||
const id = Date.now();
|
||||
repo.id = "repo-card-" + id;
|
||||
repo.innerHTML = `
|
||||
<label for="repo-owner">Repository Owner</label>
|
||||
<input type="text" id="repo-owner" name="repo_owner">
|
||||
<label for="repo-name">Repository Name</label>
|
||||
<input type="text" id="repo-name" name="repo_name">
|
||||
<label for="repo-branch">Repository Branch</label>
|
||||
<input type="text" id="repo-branch" name="repo_branch">
|
||||
<button type="button"
|
||||
class="remove-repo-button"
|
||||
onclick="remove_repo(${id})"
|
||||
id="remove-repo-button-${id}">Remove Repository</button>
|
||||
`;
|
||||
document.getElementById("repositories").appendChild(repo);
|
||||
})
|
||||
|
||||
function remove_repo(index) {
|
||||
document.getElementById("repo-card-" + index).remove();
|
||||
}
|
||||
|
||||
submit.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const compressed_jsonl = document.getElementById("compressed-jsonl").value;
|
||||
const embeddings_file = document.getElementById("embeddings-file").value;
|
||||
const pat_token = document.getElementById("pat-token").value;
|
||||
|
||||
if (pat_token == "") {
|
||||
document.getElementById("success").innerHTML = "❌ Please enter a Personal Access Token.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var cards = document.getElementById("repositories").getElementsByClassName("repo");
|
||||
var repos = [];
|
||||
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var card = cards[i];
|
||||
var owner = card.getElementsByTagName("input")[0].value;
|
||||
var name = card.getElementsByTagName("input")[1].value;
|
||||
var branch = card.getElementsByTagName("input")[2].value;
|
||||
|
||||
if (owner == "" || name == "" || branch == "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
repos.push({
|
||||
"owner": owner,
|
||||
"name": name,
|
||||
"branch": branch,
|
||||
});
|
||||
}
|
||||
|
||||
if (repos.length == 0) {
|
||||
document.getElementById("success").innerHTML = "❌ Please add at least one repository.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
fetch('/api/config/data/content_type/github', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"pat_token": pat_token,
|
||||
"repos": repos,
|
||||
"compressed_jsonl": compressed_jsonl,
|
||||
"embeddings_file": embeddings_file,
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data["status"] == "ok") {
|
||||
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to your <a href='/config'>settings page</a> to complete setup.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
167
src/khoj/interface/web/content_type_input.html
Normal file
@@ -0,0 +1,167 @@
|
||||
{% extends "base_config.html" %}
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<img class="card-icon" src="/static/assets/icons/{{ content_type }}.svg" alt="{{ content_type|capitalize }}">
|
||||
<span class="card-title-text">{{ content_type|capitalize }}</span>
|
||||
</h2>
|
||||
<form id="config-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="input-files" title="Add a {{content_type}} file for Khoj to index">Files</label>
|
||||
</td>
|
||||
<td id="input-files-cell">
|
||||
{% if current_config['input_files'] is none %}
|
||||
<input type="text" id="input-files" name="input-files" placeholder="~\Documents\notes.{{content_type}}">
|
||||
{% else %}
|
||||
{% for input_file in current_config['input_files'] %}
|
||||
<input type="text" id="input-files" name="input-files" value="{{ input_file }}" placeholder="~\Documents\notes.{{content_type}}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" id="input-files-button">Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="input-filter" title="Add a folder with {{content_type}} files for Khoj to index">Folders</label>
|
||||
</td>
|
||||
<td id="input-filter-cell">
|
||||
{% if current_config['input_filter'] is none %}
|
||||
<input type="text" id="input-filter" name="input-filter" placeholder="~/Documents/{{content_type}}">
|
||||
{% else %}
|
||||
{% for input_filter in current_config['input_filter'] %}
|
||||
<input type="text" id="input-filter" name="input-filter" placeholder="~/Documents/{{content_type}}" value="{{ input_filter.split('/*')[0] }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" id="input-filter-button">Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="display: none;" >
|
||||
<tr>
|
||||
<td>
|
||||
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="embeddings-file">Embeddings File (Output)</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="index-heading-entries">Index Heading Entries</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="index-heading-entries" name="index-heading-entries" value="{{ current_config['index_heading_entries'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="section">
|
||||
<div id="success" style="display: none;" ></div>
|
||||
<button id="submit" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function addButtonEventListener(fieldName) {
|
||||
var button = document.getElementById(fieldName + "-button");
|
||||
button.addEventListener("click", function(event) {
|
||||
var cell = document.getElementById(fieldName + "-cell");
|
||||
var newInput = document.createElement("input");
|
||||
newInput.setAttribute("type", "text");
|
||||
newInput.setAttribute("name", fieldName);
|
||||
cell.appendChild(newInput);
|
||||
})
|
||||
}
|
||||
|
||||
addButtonEventListener("input-files");
|
||||
addButtonEventListener("input-filter");
|
||||
|
||||
function getValidInputNodes(nodes) {
|
||||
var validNodes = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
const nodeValue = nodes[i].value;
|
||||
if (nodeValue === "" || nodeValue === null || nodeValue === undefined || nodeValue === "None") {
|
||||
continue;
|
||||
}
|
||||
validNodes.push(nodes[i]);
|
||||
}
|
||||
return validNodes;
|
||||
}
|
||||
|
||||
submit.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
let suffix = ""
|
||||
if ('{{content_type}}' == "markdown")
|
||||
suffix = "**/*.md"
|
||||
else if ('{{content_type}}' == "org")
|
||||
suffix = "**/*.org"
|
||||
else if ('{{content_type}}' === "pdf")
|
||||
suffix = "**/*.pdf"
|
||||
|
||||
var inputFileNodes = document.getElementsByName("input-files");
|
||||
var input_files = getValidInputNodes(inputFileNodes).map(node => node.value);
|
||||
|
||||
var inputFilterNodes = document.getElementsByName("input-filter");
|
||||
var input_filter = getValidInputNodes(inputFilterNodes).map(node => `${node.value}/${suffix}`);
|
||||
|
||||
if (input_files.length === 0 && input_filter.length === 0) {
|
||||
alert("You must specify at least one input file or input filter.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (input_files.length == 0) {
|
||||
input_files = null;
|
||||
}
|
||||
|
||||
if (input_filter.length == 0) {
|
||||
input_filter = null;
|
||||
}
|
||||
|
||||
var compressed_jsonl = document.getElementById("compressed-jsonl").value;
|
||||
var embeddings_file = document.getElementById("embeddings-file").value;
|
||||
var index_heading_entries = document.getElementById("index-heading-entries").value;
|
||||
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
fetch('/api/config/data/content_type/{{ content_type }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"input_files": input_files,
|
||||
"input_filter": input_filter,
|
||||
"compressed_jsonl": compressed_jsonl,
|
||||
"embeddings_file": embeddings_file,
|
||||
"index_heading_entries": index_heading_entries
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data["status"] == "ok") {
|
||||
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to your <a href='/config'>settings page</a> to complete setup.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
86
src/khoj/interface/web/content_type_notion_input.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends "base_config.html" %}
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<img class="card-icon" src="/static/assets/icons/notion.svg" alt="Notion">
|
||||
<span class="card-title-text">Notion</span>
|
||||
</h2>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="token">Token</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="token" name="pat" value="{{ current_config['token'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="display: none;" >
|
||||
<tr>
|
||||
<td>
|
||||
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="embeddings-file">Embeddings File (Output)</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="section">
|
||||
<div id="success" style="display: none;"></div>
|
||||
<button id="submit" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const submit = document.getElementById("submit");
|
||||
|
||||
submit.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const compressed_jsonl = document.getElementById("compressed-jsonl").value;
|
||||
const embeddings_file = document.getElementById("embeddings-file").value;
|
||||
const token = document.getElementById("token").value;
|
||||
|
||||
if (token == "") {
|
||||
document.getElementById("success").innerHTML = "❌ Please enter a Notion Token.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
fetch('/api/config/data/content_type/notion', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"token": token,
|
||||
"compressed_jsonl": compressed_jsonl,
|
||||
"embeddings_file": embeddings_file,
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data["status"] == "ok") {
|
||||
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to your <a href='/config'>settings page</a> to complete setup.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
517
src/khoj/interface/web/index.html
Normal file
@@ -0,0 +1,517 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
||||
<title>Khoj - Search</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
|
||||
<link rel="manifest" href="/static/khoj.webmanifest">
|
||||
<link rel="stylesheet" href="/static/assets/khoj.css">
|
||||
</head>
|
||||
<script type="text/javascript" src="/static/assets/org.min.js"></script>
|
||||
<script type="text/javascript" src="/static/assets/markdown-it.min.js"></script>
|
||||
|
||||
<script>
|
||||
function render_image(item) {
|
||||
return `
|
||||
<div class="results-image">
|
||||
<a href="${item.entry}" class="image-link">
|
||||
<img id=${item.score} src="${item.entry}?${Math.random()}"
|
||||
title="Effective Score: ${item.score}, Meta: ${item.additional.metadata_score}, Image: ${item.additional.image_score}"
|
||||
class="image">
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function render_org(query, data, classPrefix="") {
|
||||
return data.map(function (item) {
|
||||
var orgParser = new Org.Parser();
|
||||
var orgDocument = orgParser.parse(item.entry);
|
||||
var orgHTMLDocument = orgDocument.convert(Org.ConverterHTML, { htmlClassPrefix: classPrefix });
|
||||
return `<div class="results-org">` + orgHTMLDocument.toString() + `</div>`;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function render_markdown(query, data) {
|
||||
var md = window.markdownit();
|
||||
return data.map(function (item) {
|
||||
let rendered = "";
|
||||
if (item.additional.file.startsWith("http")) {
|
||||
lines = item.entry.split("\n");
|
||||
rendered = md.render(`${lines[0]}\t[*](${item.additional.file})\n${lines.slice(1).join("\n")}`);
|
||||
}
|
||||
else {
|
||||
rendered = md.render(`${item.entry}`);
|
||||
}
|
||||
return `<div class="results-markdown">` + rendered + `</div>`;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function render_pdf(query, data) {
|
||||
return data.map(function (item) {
|
||||
let compiled_lines = item.additional.compiled.split("\n");
|
||||
let filename = compiled_lines.shift();
|
||||
let text_match = compiled_lines.join("\n")
|
||||
return `<div class="results-pdf">` + `<h2>${filename}</h2>\n<p>${text_match}</p>` + `</div>`;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function render_multiple(query, data, type) {
|
||||
let html = "";
|
||||
data.forEach(item => {
|
||||
if (item.additional.file.endsWith(".org")) {
|
||||
html += render_org(query, [item], "org-");
|
||||
} else if (
|
||||
item.additional.file.endsWith(".md") ||
|
||||
item.additional.file.endsWith(".markdown") ||
|
||||
(item.additional.file.includes("issues") && item.additional.file.includes("github.com")) ||
|
||||
(item.additional.file.includes("commit") && item.additional.file.includes("github.com"))
|
||||
)
|
||||
{
|
||||
html += render_markdown(query, [item]);
|
||||
} else if (item.additional.file.endsWith(".pdf")) {
|
||||
html += render_pdf(query, [item]);
|
||||
} else if (item.additional.file.includes("notion.so")) {
|
||||
html += `<div class="results-notion">` + `<b><a href="${item.additional.file}">${item.additional.heading}</a></b>` + `<p>${item.entry}</p>` + `</div>`;
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
function render_results(data, query, type) {
|
||||
let results = "";
|
||||
if (type === "markdown") {
|
||||
results = render_markdown(query, data);
|
||||
} else if (type === "org") {
|
||||
results = render_org(query, data, "org-");
|
||||
} else if (type === "image") {
|
||||
results = data.map(render_image).join('');
|
||||
} else if (type === "pdf") {
|
||||
results = render_pdf(query, data);
|
||||
} else if (type === "github" || type === "all" || type === "notion") {
|
||||
results = render_multiple(query, data, type);
|
||||
} else {
|
||||
results = data.map((item) => `<div class="results-plugin">` + `<p>${item.entry}</p>` + `</div>`).join("\n")
|
||||
}
|
||||
|
||||
// Any POST rendering goes here.
|
||||
|
||||
let renderedResults = document.createElement("div");
|
||||
renderedResults.id = `results-${type}`;
|
||||
renderedResults.innerHTML = results;
|
||||
|
||||
// For all elements that are of type img in the results html and have a src with 'avatar' in the URL, add the class 'avatar'
|
||||
// This is used to make the avatar images round
|
||||
let images = renderedResults.querySelectorAll("img[src*='avatar']");
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
images[i].classList.add("avatar");
|
||||
}
|
||||
|
||||
return renderedResults.outerHTML;
|
||||
}
|
||||
|
||||
function search(rerank=false) {
|
||||
// Extract required fields for search from form
|
||||
query = document.getElementById("query").value.trim();
|
||||
type = document.getElementById("type").value;
|
||||
searchHint = document.getElementById("info-hint");
|
||||
results_count = localStorage.getItem("khojResultsCount") || 5;
|
||||
console.log(`Query: ${query}, Type: ${type}, Results Count: ${results_count}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
if (query.length === 0) {
|
||||
searchHint.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
// If set query field in url query param on rerank
|
||||
if (rerank)
|
||||
setQueryFieldInUrl(query);
|
||||
|
||||
// Execute Search and Render Results
|
||||
url = createRequestUrl(query, type, results_count || 5, rerank);
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
document.getElementById("results").innerHTML = render_results(data, query, type);
|
||||
});
|
||||
|
||||
setTimeout(() => { searchHint.style.display = "block"; }, 3000);
|
||||
}
|
||||
|
||||
function incrementalSearch(event) {
|
||||
type = document.getElementById("type").value;
|
||||
// Search with reranking on 'Enter'
|
||||
if (event.key === 'Enter') {
|
||||
search(rerank=true);
|
||||
}
|
||||
// Limit incremental search to text types
|
||||
else if (type !== "image") {
|
||||
search(rerank=false);
|
||||
}
|
||||
}
|
||||
|
||||
function populate_type_dropdown() {
|
||||
// Populate type dropdown field with enabled content types only
|
||||
fetch("/api/config/types")
|
||||
.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("query").setAttribute("disabled", "disabled");
|
||||
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
|
||||
return [];
|
||||
}
|
||||
|
||||
document.getElementById("type").innerHTML =
|
||||
enabled_types
|
||||
.map(type => `<option value="${type}">${type.slice(0,1).toUpperCase() + type.slice(1)}</option>`)
|
||||
.join('');
|
||||
|
||||
return enabled_types;
|
||||
})
|
||||
.then(enabled_types => {
|
||||
// Set type field to content type passed in URL query parameter, if valid
|
||||
var type_via_url = new URLSearchParams(window.location.search).get("t");
|
||||
if (type_via_url && enabled_types.includes(type_via_url))
|
||||
document.getElementById("type").value = type_via_url;
|
||||
});
|
||||
}
|
||||
|
||||
function createRequestUrl(query, type, results_count, rerank) {
|
||||
// Generate Backend API URL to execute Search
|
||||
let url = `/api/search?q=${encodeURIComponent(query)}&n=${results_count}&client=web`;
|
||||
// If type is not 'all', append type to URL
|
||||
if (type !== 'all')
|
||||
url += `&t=${type}`;
|
||||
// Rerank is only supported by text types
|
||||
if (type !== "image")
|
||||
url += `&r=${rerank}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
function setTypeFieldInUrl(type) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set("t", type.value);
|
||||
window.history.pushState({}, "", url.href);
|
||||
}
|
||||
|
||||
function setQueryFieldInUrl(query) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set("q", query);
|
||||
window.history.pushState({}, "", url.href);
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// Dynamically populate type dropdown based on enabled content types and type passed as URL query parameter
|
||||
populate_type_dropdown();
|
||||
|
||||
// Fill query field with value passed in URL query parameters, if any.
|
||||
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
||||
if (query_via_url)
|
||||
document.getElementById("query").value = query_via_url;
|
||||
}
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<!--Add Header Logo and Nav Pane-->
|
||||
<div class="khoj-header">
|
||||
{% if demo %}
|
||||
<!-- Banner linking to https://khoj.dev -->
|
||||
<div class="khoj-banner-container">
|
||||
<a class="khoj-banner" href="https://khoj.dev" target="_blank">
|
||||
<p id="khoj-banner" class="khoj-banner">
|
||||
Enroll in Khoj cloud to get your own Github assistant
|
||||
</p>
|
||||
</a>
|
||||
<input type="text" id="khoj-banner-email" placeholder="email" class="khoj-banner-email"></input>
|
||||
<button id="khoj-banner-submit" class="khoj-banner-button">Submit</button>
|
||||
</div>
|
||||
<a class="khoj-logo" href="https://khoj.dev" target="_blank">
|
||||
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="khoj-logo" href="/">
|
||||
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
|
||||
</a>
|
||||
{% endif %}
|
||||
<nav class="khoj-nav">
|
||||
<a class="khoj-nav" href="/chat">Chat</a>
|
||||
<a class="khoj-nav khoj-nav-selected" href="/">Search</a>
|
||||
{% if not demo %}
|
||||
<a class="khoj-nav" href="/config">Settings</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!--Add Text Box To Enter Query, Trigger Incremental Search OnChange -->
|
||||
<input type="text" id="query" class="option" onkeyup=incrementalSearch(event) autofocus="autofocus" placeholder="What is the meaning of life?">
|
||||
|
||||
<div id="options">
|
||||
<!--Add Dropdown to Select Query Type -->
|
||||
<select id="type" class="option" onchange="setTypeFieldInUrl(this)"></select>
|
||||
</div>
|
||||
|
||||
<!--Add Hints to Guide Search -->
|
||||
<div id="info-hint" style="display: none">
|
||||
Unexpected results? Hit Enter to get better results.
|
||||
Else click Reinitialize on the <a href="/config">settings page</a> to fix it.
|
||||
</div>
|
||||
|
||||
<!-- Section to Render Results -->
|
||||
<div id="results"></div>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
@media only screen and (max-width: 600px) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr auto auto auto minmax(80px, 100%);
|
||||
font-size: small!important;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(70vw, 100%) 1fr;
|
||||
grid-template-rows: 1fr auto auto auto minmax(80px, 100%);
|
||||
padding-top: 60vw;
|
||||
}
|
||||
body > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
font-family: roboto, karma, segoe ui, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
body > * {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
#options {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
#options > * {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background: #f9fafc
|
||||
}
|
||||
.option:hover {
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
}
|
||||
#options > button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#query {
|
||||
font-size: larger;
|
||||
}
|
||||
#info-hint {
|
||||
font-size: small;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin: 5px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
#results {
|
||||
font-size: medium;
|
||||
margin: 0px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.results-image {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.image-link {
|
||||
place-self: center;
|
||||
}
|
||||
.image {
|
||||
width: 20vw;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #475569;
|
||||
}
|
||||
#json {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.results-pdf,
|
||||
.results-notion,
|
||||
.results-plugin {
|
||||
text-align: left;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.results-markdown,
|
||||
.results-github {
|
||||
text-align: left;
|
||||
}
|
||||
.results-org {
|
||||
text-align: left;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.results-org h3 {
|
||||
margin: 20px 0 0 0;
|
||||
font-size: larger;
|
||||
}
|
||||
span.org-task-status {
|
||||
color: white;
|
||||
padding: 3.5px 3.5px 0;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #eab308;
|
||||
font-size: medium;
|
||||
}
|
||||
span.org-task-status.todo {
|
||||
background-color: #3b82f6
|
||||
}
|
||||
span.org-task-status.done {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
span.org-task-tag {
|
||||
color: white;
|
||||
padding: 3.5px 3.5px 0;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background-color: #ef4444;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-width: 100;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
div#results-error,
|
||||
div.results-markdown,
|
||||
div.results-notion,
|
||||
div.results-org,
|
||||
div.results-pdf {
|
||||
text-align: left;
|
||||
box-shadow: 2px 2px 2px var(--primary-hover);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 4px solid rgb(229, 229, 229);
|
||||
}
|
||||
|
||||
div#results-error {
|
||||
box-shadow: 2px 2px 2px #FF5722;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
div.khoj-banner-container {
|
||||
background: linear-gradient(-45deg, #FFC107, #FF9800, #FF5722, #FF9800, #FFC107);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
a.khoj-banner {
|
||||
color: black;
|
||||
}
|
||||
|
||||
a.khoj-logo {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.khoj-banner {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button#khoj-banner-submit,
|
||||
input#khoj-banner-email {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #475569;
|
||||
background: #f9fafc;
|
||||
}
|
||||
|
||||
button#khoj-banner-submit:hover,
|
||||
input#khoj-banner-email:hover {
|
||||
box-shadow: 0 0 11px #aaa;
|
||||
}
|
||||
|
||||
p#khoj-banner {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
a.khoj-banner {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
var khojBannerSubmit = document.getElementById("khoj-banner-submit");
|
||||
khojBannerSubmit?.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
var email = document.getElementById("khoj-banner-email").value;
|
||||
fetch("https://lantern.khoj.dev/beta/users/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: email
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
console.log(data);
|
||||
if (data.user != null) {
|
||||
document.getElementById("khoj-banner").innerHTML = "Thanks for signing up. We'll be in touch soon! 🚀";
|
||||
document.getElementById("khoj-banner-submit").remove();
|
||||
} else {
|
||||
document.getElementById("khoj-banner").innerHTML = "There was an error signing up. Please contact team@khoj.dev";
|
||||
}
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
document.getElementById("khoj-banner").innerHTML = "There was an error signing up. Please contact team@khoj.dev";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"short_name": "Khoj",
|
||||
"description": "A natural language search engine for your personal notes, transactions and photos",
|
||||
"description": "An AI search assistant for your digital brain",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/assets/icons/favicon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"src": "/static/assets/icons/favicon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
16
src/khoj/interface/web/khoj_chat.webmanifest
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Khoj Chat",
|
||||
"short_name": "Khoj Chat",
|
||||
"description": "An AI personal assistant for your digital brain",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/assets/icons/favicon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
"start_url": "/chat"
|
||||
}
|
||||
87
src/khoj/interface/web/processor_conversation_input.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base_config.html" %}
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat">
|
||||
<span class="card-title-text">Chat</span>
|
||||
</h2>
|
||||
<form id="config-form">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="openai-api-key" title="Get your OpenAI key from https://platform.openai.com/account/api-keys">OpenAI API key</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="openai-api-key" name="openai-api-key" value="{{ current_config['openai_api_key'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="chat-model">Chat Model</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="chat-model" name="chat-model" value="{{ current_config['chat_model'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="display: none;">
|
||||
<tr>
|
||||
<td>
|
||||
<label for="conversation-logfile">Conversation Logfile</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="conversation-logfile" name="conversation-logfile" value="{{ current_config['conversation_logfile'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="model">Model</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="model" name="model" value="{{ current_config['model'] }}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="section">
|
||||
<div id="success" style="display: none;" ></div>
|
||||
<button id="submit" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
submit.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
var openai_api_key = document.getElementById("openai-api-key").value;
|
||||
var conversation_logfile = document.getElementById("conversation-logfile").value;
|
||||
var model = document.getElementById("model").value;
|
||||
var chat_model = document.getElementById("chat-model").value;
|
||||
|
||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
||||
fetch('/api/config/data/processor/conversation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"openai_api_key": openai_api_key,
|
||||
"conversation_logfile": conversation_logfile,
|
||||
"model": model,
|
||||
"chat_model": chat_model
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data["status"] == "ok") {
|
||||
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to your <a href='/config'>settings page</a> to complete setup.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
|
||||
document.getElementById("success").style.display = "block";
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -6,44 +6,42 @@ import logging
|
||||
import threading
|
||||
import warnings
|
||||
from platform import system
|
||||
import webbrowser
|
||||
|
||||
# Ignore non-actionable warnings
|
||||
warnings.filterwarnings("ignore", message=r'snapshot_download.py has been made private', category=FutureWarning)
|
||||
warnings.filterwarnings("ignore", message=r'legacy way to download files from the HF hub,', category=FutureWarning)
|
||||
warnings.filterwarnings("ignore", message=r"snapshot_download.py has been made private", category=FutureWarning)
|
||||
warnings.filterwarnings("ignore", message=r"legacy way to download files from the HF hub,", category=FutureWarning)
|
||||
|
||||
# External Packages
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from PyQt6 import QtWidgets
|
||||
from PyQt6.QtCore import QThread, QTimer
|
||||
from rich.logging import RichHandler
|
||||
import schedule
|
||||
|
||||
# Internal Packages
|
||||
from src.configure import configure_server
|
||||
from src.routers.api import api
|
||||
from src.routers.api_beta import api_beta
|
||||
from src.routers.web_client import web_client
|
||||
from src.utils import constants, state
|
||||
from src.utils.cli import cli
|
||||
from src.utils.helpers import CustomFormatter
|
||||
from src.interface.desktop.main_window import MainWindow
|
||||
from src.interface.desktop.system_tray import create_system_tray
|
||||
from khoj.configure import configure_routes, configure_server
|
||||
from khoj.utils import state
|
||||
from khoj.utils.cli import cli
|
||||
from khoj.interface.desktop.main_window import MainWindow
|
||||
from khoj.interface.desktop.system_tray import create_system_tray
|
||||
|
||||
|
||||
# Initialize the Application Server
|
||||
app = FastAPI()
|
||||
app.mount("/static", StaticFiles(directory=constants.web_directory), name="static")
|
||||
app.include_router(api, prefix="/api")
|
||||
app.include_router(api_beta, prefix="/api/beta")
|
||||
app.include_router(web_client)
|
||||
|
||||
logger = logging.getLogger('src')
|
||||
# Setup Logger
|
||||
rich_handler = RichHandler(rich_tracebacks=True)
|
||||
rich_handler.setFormatter(fmt=logging.Formatter(fmt="%(message)s", datefmt="[%X]"))
|
||||
logging.basicConfig(handlers=[rich_handler])
|
||||
|
||||
logger = logging.getLogger("khoj")
|
||||
|
||||
|
||||
def run():
|
||||
# Turn Tokenizers Parallelism Off. App does not support it.
|
||||
os.environ["TOKENIZERS_PARALLELISM"] = 'false'
|
||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||
|
||||
# Load config from CLI
|
||||
state.cli_args = sys.argv[1:]
|
||||
@@ -53,52 +51,56 @@ def run():
|
||||
# Create app directory, if it doesn't exist
|
||||
state.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Setup Logger
|
||||
# Set Logging Level
|
||||
if args.verbose == 0:
|
||||
logger.setLevel(logging.WARN)
|
||||
elif args.verbose == 1:
|
||||
logger.setLevel(logging.INFO)
|
||||
elif args.verbose >= 2:
|
||||
elif args.verbose >= 1:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Set Log Format
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(CustomFormatter())
|
||||
logger.addHandler(ch)
|
||||
|
||||
# Set Log File
|
||||
fh = logging.FileHandler(state.config_file.parent / 'khoj.log')
|
||||
fh = logging.FileHandler(state.config_file.parent / "khoj.log", encoding="utf-8")
|
||||
fh.setLevel(logging.DEBUG)
|
||||
logger.addHandler(fh)
|
||||
|
||||
logger.info("Starting Khoj...")
|
||||
logger.info("🌘 Starting Khoj")
|
||||
|
||||
if args.no_gui:
|
||||
if not args.gui:
|
||||
# Setup task scheduler
|
||||
poll_task_scheduler()
|
||||
|
||||
# Start Server
|
||||
configure_server(args, required=False)
|
||||
configure_routes(app)
|
||||
start_server(app, host=args.host, port=args.port, socket=args.socket)
|
||||
else:
|
||||
# Setup GUI
|
||||
gui = QtWidgets.QApplication([])
|
||||
main_window = MainWindow(args.config_file)
|
||||
main_window = MainWindow(args.host, args.port)
|
||||
|
||||
# System tray is only available on Windows, MacOS.
|
||||
# On Linux (Gnome) the System tray is not supported.
|
||||
# Since only the Main Window is available
|
||||
# Quitting it should quit the application
|
||||
if system() in ['Windows', 'Darwin']:
|
||||
if system() in ["Windows", "Darwin"]:
|
||||
gui.setQuitOnLastWindowClosed(False)
|
||||
tray = create_system_tray(gui, main_window)
|
||||
tray.show()
|
||||
|
||||
# Setup Server
|
||||
configure_server(args, required=False)
|
||||
configure_routes(app)
|
||||
server = ServerThread(app, args.host, args.port, args.socket)
|
||||
|
||||
url = f"http://{args.host}:{args.port}"
|
||||
logger.info(f"🌗 Khoj is running at {url}")
|
||||
try:
|
||||
startup_url = url if args.config else f"{url}/config"
|
||||
webbrowser.open(startup_url)
|
||||
except:
|
||||
logger.warning(f"🚧 Unable to open browser. Please open {url} manually to configure or use Khoj.")
|
||||
|
||||
# Show Main Window on First Run Experience or if on Linux
|
||||
if args.config is None or system() not in ['Windows', 'Darwin']:
|
||||
if args.config is None or system() not in ["Windows", "Darwin"]:
|
||||
main_window.show()
|
||||
|
||||
# Setup Signal Handlers
|
||||
@@ -113,9 +115,10 @@ def run():
|
||||
gui.aboutToQuit.connect(server.terminate)
|
||||
|
||||
# Close Splash Screen if still open
|
||||
if system() != 'Darwin':
|
||||
if system() != "Darwin":
|
||||
try:
|
||||
import pyi_splash
|
||||
|
||||
# Update the text on the splash screen
|
||||
pyi_splash.update_text("Khoj setup complete")
|
||||
# Close Splash Screen
|
||||
@@ -127,7 +130,6 @@ def run():
|
||||
|
||||
|
||||
def sigint_handler(*args):
|
||||
print("\nShutting down Khoj...")
|
||||
QtWidgets.QApplication.quit()
|
||||
|
||||
|
||||
@@ -137,13 +139,16 @@ def set_state(args):
|
||||
state.verbose = args.verbose
|
||||
state.host = args.host
|
||||
state.port = args.port
|
||||
state.demo = args.demo
|
||||
|
||||
|
||||
def start_server(app, host=None, port=None, socket=None):
|
||||
logger.info("🌖 Khoj is ready to use")
|
||||
if socket:
|
||||
uvicorn.run(app, proxy_headers=True, uds=socket)
|
||||
uvicorn.run(app, proxy_headers=True, uds=socket, log_level="debug", use_colors=True, log_config=None)
|
||||
else:
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
uvicorn.run(app, host=host, port=port, log_level="debug", use_colors=True, log_config=None)
|
||||
logger.info("🌒 Stopping Khoj")
|
||||
|
||||
|
||||
def poll_task_scheduler():
|
||||
@@ -168,5 +173,10 @@ class ServerThread(QThread):
|
||||
start_server(self.app, self.host, self.port, self.socket)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def run_gui():
|
||||
sys.argv += ["--gui"]
|
||||
run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_gui()
|
||||
145
src/khoj/processor/conversation/gpt.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# Standard Packages
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# External Packages
|
||||
from langchain.schema import ChatMessage
|
||||
|
||||
# Internal Packages
|
||||
from khoj.utils.constants import empty_escape_sequences
|
||||
from khoj.processor.conversation import prompts
|
||||
from khoj.processor.conversation.utils import (
|
||||
chat_completion_with_backoff,
|
||||
completion_with_backoff,
|
||||
generate_chatml_messages_with_context,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def summarize(session, model, api_key=None, temperature=0.5, max_tokens=200):
|
||||
"""
|
||||
Summarize conversation session using the specified OpenAI chat model
|
||||
"""
|
||||
messages = [ChatMessage(content=prompts.summarize_chat.format(), role="system")] + session
|
||||
|
||||
# Get Response from GPT
|
||||
logger.debug(f"Prompt for GPT: {messages}")
|
||||
response = completion_with_backoff(
|
||||
messages=messages,
|
||||
model_name=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
model_kwargs={"stop": ['"""'], "frequency_penalty": 0.2},
|
||||
openai_api_key=api_key,
|
||||
)
|
||||
|
||||
# Extract, Clean Message from GPT's Response
|
||||
return str(response.content).replace("\n\n", "")
|
||||
|
||||
|
||||
def extract_questions(
|
||||
text, model: Optional[str] = "gpt-4", conversation_log={}, api_key=None, temperature=0, max_tokens=100
|
||||
):
|
||||
"""
|
||||
Infer search queries to retrieve relevant notes to answer user query
|
||||
"""
|
||||
# Extract Past User Message and Inferred Questions from Conversation Log
|
||||
chat_history = "".join(
|
||||
[
|
||||
f'Q: {chat["intent"]["query"]}\n\n{chat["intent"].get("inferred-queries") or list([chat["intent"]["query"]])}\n\n{chat["message"]}\n\n'
|
||||
for chat in conversation_log.get("chat", [])[-4:]
|
||||
if chat["by"] == "khoj"
|
||||
]
|
||||
)
|
||||
|
||||
# Get dates relative to today for prompt creation
|
||||
today = datetime.today()
|
||||
current_new_year = today.replace(month=1, day=1)
|
||||
last_new_year = current_new_year.replace(year=today.year - 1)
|
||||
|
||||
prompt = prompts.extract_questions.format(
|
||||
current_date=today.strftime("%A, %Y-%m-%d"),
|
||||
last_new_year=last_new_year.strftime("%Y"),
|
||||
last_new_year_date=last_new_year.strftime("%Y-%m-%d"),
|
||||
current_new_year_date=current_new_year.strftime("%Y-%m-%d"),
|
||||
bob_tom_age_difference={current_new_year.year - 1984 - 30},
|
||||
bob_age={current_new_year.year - 1984},
|
||||
chat_history=chat_history,
|
||||
text=text,
|
||||
)
|
||||
messages = [ChatMessage(content=prompt, role="assistant")]
|
||||
|
||||
# Get Response from GPT
|
||||
response = completion_with_backoff(
|
||||
messages=messages,
|
||||
model_name=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
model_kwargs={"stop": ["A: ", "\n"]},
|
||||
openai_api_key=api_key,
|
||||
)
|
||||
|
||||
# Extract, Clean Message from GPT's Response
|
||||
try:
|
||||
questions = (
|
||||
response.content.strip(empty_escape_sequences)
|
||||
.replace("['", '["')
|
||||
.replace("']", '"]')
|
||||
.replace("', '", '", "')
|
||||
.replace('["', "")
|
||||
.replace('"]', "")
|
||||
.split('", "')
|
||||
)
|
||||
except:
|
||||
logger.warning(f"GPT returned invalid JSON. Falling back to using user message as search query.\n{response}")
|
||||
questions = [text]
|
||||
logger.debug(f"Extracted Questions by GPT: {questions}")
|
||||
return questions
|
||||
|
||||
|
||||
def converse(
|
||||
references,
|
||||
user_query,
|
||||
conversation_log={},
|
||||
model: str = "gpt-3.5-turbo",
|
||||
api_key: Optional[str] = None,
|
||||
temperature: float = 0.2,
|
||||
completion_func=None,
|
||||
):
|
||||
"""
|
||||
Converse with user using OpenAI's ChatGPT
|
||||
"""
|
||||
# Initialize Variables
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
compiled_references = "\n\n".join({f"# {item}" for item in references})
|
||||
|
||||
# Get Conversation Primer appropriate to Conversation Type
|
||||
if compiled_references == "":
|
||||
conversation_primer = prompts.general_conversation.format(current_date=current_date, query=user_query)
|
||||
else:
|
||||
conversation_primer = prompts.notes_conversation.format(
|
||||
current_date=current_date, query=user_query, references=compiled_references
|
||||
)
|
||||
|
||||
# Setup Prompt with Primer or Conversation History
|
||||
messages = generate_chatml_messages_with_context(
|
||||
conversation_primer,
|
||||
prompts.personality.format(),
|
||||
conversation_log,
|
||||
model,
|
||||
)
|
||||
truncated_messages = "\n".join({f"{message.content[:40]}..." for message in messages})
|
||||
logger.debug(f"Conversation Context for GPT: {truncated_messages}")
|
||||
|
||||
# Get Response from GPT
|
||||
return chat_completion_with_backoff(
|
||||
messages=messages,
|
||||
compiled_references=references,
|
||||
model_name=model,
|
||||
temperature=temperature,
|
||||
openai_api_key=api_key,
|
||||
completion_func=completion_func,
|
||||
)
|
||||
152
src/khoj/processor/conversation/prompts.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# External Packages
|
||||
from langchain.prompts import PromptTemplate
|
||||
|
||||
|
||||
## Personality
|
||||
## --
|
||||
personality = PromptTemplate.from_template("You are Khoj, a friendly, smart and helpful personal assistant.")
|
||||
|
||||
|
||||
## General Conversation
|
||||
## --
|
||||
general_conversation = PromptTemplate.from_template(
|
||||
"""
|
||||
Using your general knowledge and our past conversations as context, answer the following question.
|
||||
Current Date: {current_date}
|
||||
|
||||
Question: {query}
|
||||
""".strip()
|
||||
)
|
||||
|
||||
|
||||
## Notes Conversation
|
||||
## --
|
||||
notes_conversation = PromptTemplate.from_template(
|
||||
"""
|
||||
Using the notes and our past conversations as context, answer the following question.
|
||||
Current Date: {current_date}
|
||||
|
||||
Notes:
|
||||
{references}
|
||||
|
||||
Question: {query}
|
||||
""".strip()
|
||||
)
|
||||
|
||||
|
||||
## Summarize Chat
|
||||
## --
|
||||
summarize_chat = PromptTemplate.from_template(
|
||||
f"{personality.format()} Summarize the conversation from your first person perspective"
|
||||
)
|
||||
|
||||
|
||||
## Summarize Notes
|
||||
## --
|
||||
summarize_notes = PromptTemplate.from_template(
|
||||
"""
|
||||
Summarize the below notes about {user_query}:
|
||||
|
||||
{text}
|
||||
|
||||
Summarize the notes in second person perspective:"""
|
||||
)
|
||||
|
||||
|
||||
## Answer
|
||||
## --
|
||||
answer = PromptTemplate.from_template(
|
||||
"""
|
||||
You are a friendly, helpful personal assistant.
|
||||
Using the users notes below, answer their following question. If the answer is not contained within the notes, say "I don't know."
|
||||
|
||||
Notes:
|
||||
{text}
|
||||
|
||||
Question: {user_query}
|
||||
|
||||
Answer (in second person):"""
|
||||
)
|
||||
|
||||
|
||||
## Extract Questions
|
||||
## --
|
||||
extract_questions = PromptTemplate.from_template(
|
||||
"""
|
||||
You are Khoj, an extremely smart and helpful search assistant with the ability to retrieve information from the user's notes.
|
||||
- The user will provide their questions and answers to you for context.
|
||||
- Add as much context from the previous questions and answers as required into your search queries.
|
||||
- Break messages into multiple search queries when required to retrieve the relevant information.
|
||||
- Add date filters to your search queries from questions and answers when required to retrieve the relevant information.
|
||||
|
||||
What searches, if any, will you need to perform to answer the users question?
|
||||
Provide search queries as a JSON list of strings
|
||||
Current Date: {current_date}
|
||||
|
||||
Q: How was my trip to Cambodia?
|
||||
|
||||
["How was my trip to Cambodia?"]
|
||||
|
||||
A: The trip was amazing. I went to the Angkor Wat temple and it was beautiful.
|
||||
|
||||
Q: Who did i visit that temple with?
|
||||
|
||||
["Who did I visit the Angkor Wat Temple in Cambodia with?"]
|
||||
|
||||
A: You visited the Angkor Wat Temple in Cambodia with Pablo, Namita and Xi.
|
||||
|
||||
Q: What national parks did I go to last year?
|
||||
|
||||
["National park I visited in {last_new_year} dt>='{last_new_year_date}' dt<'{current_new_year_date}'"]
|
||||
|
||||
A: You visited the Grand Canyon and Yellowstone National Park in {last_new_year}.
|
||||
|
||||
Q: How are you feeling today?
|
||||
|
||||
[]
|
||||
|
||||
A: I'm feeling a little bored. Helping you will hopefully make me feel better!
|
||||
|
||||
Q: How many tennis balls fit in the back of a 2002 Honda Civic?
|
||||
|
||||
["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"]
|
||||
|
||||
A: 1085 tennis balls will fit in the trunk of a Honda Civic
|
||||
|
||||
Q: Is Bob older than Tom?
|
||||
|
||||
["When was Bob born?", "What is Tom's age?"]
|
||||
|
||||
A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old.
|
||||
|
||||
Q: What is their age difference?
|
||||
|
||||
["What is Bob's age?", "What is Tom's age?"]
|
||||
|
||||
A: Bob is {bob_tom_age_difference} years older than Tom. As Bob is {bob_age} years old and Tom is 30 years old.
|
||||
|
||||
{chat_history}
|
||||
Q: {text}
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
## Extract Search Type
|
||||
## --
|
||||
search_type = """
|
||||
Objective: Extract search type from user query and return information as JSON
|
||||
|
||||
Allowed search types are listed below:
|
||||
- search-type=["notes", "image", "pdf"]
|
||||
|
||||
Some examples are given below for reference:
|
||||
Q:What fiction book was I reading last week about AI starship?
|
||||
A:{ "search-type": "notes" }
|
||||
Q: What did the lease say about early termination
|
||||
A: { "search-type": "pdf" }
|
||||
Q:Can you recommend a movie to watch from my notes?
|
||||
A:{ "search-type": "notes" }
|
||||
Q:When did I go surfing last?
|
||||
A:{ "search-type": "notes" }
|
||||
Q:"""
|
||||