Add front-end Electron application for Khoj local file syncing (#473)

* Initial version - setup a file-push architecture for generating embeddings with Khoj
* Use state.host and state.port for configuring the URL for the indexer
* Fix parsing of PDF files
* Read markdown files from streamed data and update unit tests
* On application startup, load in embeddings from configurations files, rather than regenerating the corpus based on file system
* Init: refactor indexer/batch endpoint to support a generic file ingestion format
* Add features to better support indexing from files sent by the desktop client
* Initial commit with Electron application
- Adds electron app
* Add import for pymupdf, remove import for pypdf
* Allow user to configure khoj host URL
* Remove search type configuration from index.html
* Use v1 path for current indexer routes
This commit is contained in:
sabaimran 2023-09-06 12:04:18 -07:00 committed by GitHub
parent 205dc90746
commit 76562f4250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 20132 additions and 82 deletions

3
.gitignore vendored
View file

@ -12,6 +12,7 @@ __pycache__
.vscode
.env
.venv/*
todesktop.json
# Build artifacts
/src/khoj/interface/web/images
@ -29,7 +30,7 @@ node_modules
# Don't include the compiled obsidian main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
src/interface/obsidian/main.js
# Exclude sourcemaps
*.map

View file

@ -56,11 +56,11 @@ dependencies = [
"uvicorn == 0.17.6",
"aiohttp == 3.8.5",
"langchain >= 0.0.187",
"pypdf >= 3.9.0",
"requests >= 2.26.0",
"bs4 >= 0.0.1",
"gpt4all >= 1.0.7",
"anyio == 3.7.1",
"pymupdf >= 1.23.3",
]
dynamic = ["version"]

View file

@ -0,0 +1,29 @@
# Run it locally
## Prerequisites
Install the runtime dependencies. This command should install all dev dependencies.
```bash
yarn add
```
Run the application
```bash
yarn start
```
# Deploying the Electron App
## Prerequisites
Install the ToDesktop CLI. Full documentation can be found here: https://www.npmjs.com/package/@todesktop/cli
```bash
yarn global add @todesktop/cli
```
Configure the `todesktop.json` file. Fill in the `id` based on the application ID.
## Build
This will prompt you to login. It triggers builds for all platforms.
```bash
todesktop build
```
If you get an error saying the command is not found, make sure that your `yarn` global bin directory is in your `PATH` environment variable. You can find the location of the global bin directory by running `yarn global bin`. Add this line to your `.bashrc` or `.zshrc` file: `export PATH="$PATH:$(yarn global bin)"`.

View 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

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12L12 12M12 12L9 12M12 12L12 9M12 12L12 15" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7 3.33782C8.47087 2.48697 10.1786 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 10.1786 2.48697 8.47087 3.33782 7" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path id="SVGCleanerId_0" style="fill:#FFC36E;" d="M183.295,123.586H55.05c-6.687,0-12.801-3.778-15.791-9.76l-12.776-25.55
l12.776-25.55c2.99-5.982,9.103-9.76,15.791-9.76h128.246c6.687,0,12.801,3.778,15.791,9.76l12.775,25.55l-12.776,25.55
C196.096,119.808,189.983,123.586,183.295,123.586z"/>
<g>
<path id="SVGCleanerId_0_1_" style="fill:#FFC36E;" d="M183.295,123.586H55.05c-6.687,0-12.801-3.778-15.791-9.76l-12.776-25.55
l12.776-25.55c2.99-5.982,9.103-9.76,15.791-9.76h128.246c6.687,0,12.801,3.778,15.791,9.76l12.775,25.55l-12.776,25.55
C196.096,119.808,189.983,123.586,183.295,123.586z"/>
</g>
<path style="fill:#EFF2FA;" d="M485.517,70.621H26.483c-4.875,0-8.828,3.953-8.828,8.828v44.138h476.69V79.448
C494.345,74.573,490.392,70.621,485.517,70.621z"/>
<rect x="17.655" y="105.931" style="fill:#E1E6F2;" width="476.69" height="17.655"/>
<path style="fill:#FFD782;" d="M494.345,88.276H217.318c-3.343,0-6.4,1.889-7.895,4.879l-10.336,20.671
c-2.99,5.982-9.105,9.76-15.791,9.76H55.05c-6.687,0-12.801-3.778-15.791-9.76L28.922,93.155c-1.495-2.99-4.552-4.879-7.895-4.879
h-3.372C7.904,88.276,0,96.18,0,105.931v335.448c0,9.751,7.904,17.655,17.655,17.655h476.69c9.751,0,17.655-7.904,17.655-17.655
V105.931C512,96.18,504.096,88.276,494.345,88.276z"/>
<path style="fill:#FFC36E;" d="M485.517,441.379H26.483c-4.875,0-8.828-3.953-8.828-8.828l0,0c0-4.875,3.953-8.828,8.828-8.828
h459.034c4.875,0,8.828,3.953,8.828,8.828l0,0C494.345,437.427,490.392,441.379,485.517,441.379z"/>
<path style="fill:#EFF2FA;" d="M326.621,220.69h132.414c4.875,0,8.828-3.953,8.828-8.828v-70.621c0-4.875-3.953-8.828-8.828-8.828
H326.621c-4.875,0-8.828,3.953-8.828,8.828v70.621C317.793,216.737,321.746,220.69,326.621,220.69z"/>
<path style="fill:#C7CFE2;" d="M441.379,167.724h-97.103c-4.875,0-8.828-3.953-8.828-8.828l0,0c0-4.875,3.953-8.828,8.828-8.828
h97.103c4.875,0,8.828,3.953,8.828,8.828l0,0C450.207,163.772,446.254,167.724,441.379,167.724z"/>
<path style="fill:#D7DEED;" d="M441.379,203.034h-97.103c-4.875,0-8.828-3.953-8.828-8.828l0,0c0-4.875,3.953-8.828,8.828-8.828
h97.103c4.875,0,8.828,3.953,8.828,8.828l0,0C450.207,199.082,446.254,203.034,441.379,203.034z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.197 3.35462C16.8703 1.67483 19.4476 1.53865 20.9536 3.05046C22.4596 4.56228 22.3239 7.14956 20.6506 8.82935L18.2268 11.2626M10.0464 14C8.54044 12.4882 8.67609 9.90087 10.3494 8.22108L12.5 6.06212" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.9536 10C15.4596 11.5118 15.3239 14.0991 13.6506 15.7789L11.2268 18.2121L8.80299 20.6454C7.12969 22.3252 4.55237 22.4613 3.0464 20.9495C1.54043 19.4377 1.67609 16.8504 3.34939 15.1706L5.77323 12.7373" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 777 B

View 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

View 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

View 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

View file

@ -0,0 +1 @@
<svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg"><path d="m297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View 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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100px" height="100px"><path fill="#fefdef" d="M29.614,12.307h-1.268c-4.803,0-8.732,3.93-8.732,8.732v61.535c0,4.803,3.93,8.732,8.732,8.732h43.535c4.803,0,8.732-3.93,8.732-8.732v-50.02C72.74,24.68,68.241,20.182,60.367,12.307H41.614"/><path fill="#1f212b" d="M71.882,92.307H28.347c-5.367,0-9.732-4.366-9.732-9.732V21.04c0-5.367,4.366-9.732,9.732-9.732h1.268c0.552,0,1,0.448,1,1s-0.448,1-1,1h-1.268c-4.264,0-7.732,3.469-7.732,7.732v61.535c0,4.264,3.469,7.732,7.732,7.732h43.535c4.264,0,7.732-3.469,7.732-7.732V32.969L59.953,13.307H41.614c-0.552,0-1-0.448-1-1s0.448-1,1-1h18.752c0.265,0,0.52,0.105,0.707,0.293l20.248,20.248c0.188,0.188,0.293,0.442,0.293,0.707v50.02C81.614,87.941,77.248,92.307,71.882,92.307z"/><path fill="#fef6aa" d="M60.114,12.807v10.986c0,4.958,4.057,9.014,9.014,9.014h11.986"/><path fill="#1f212b" d="M81.114 33.307H69.129c-5.247 0-9.515-4.268-9.515-9.515V12.807c0-.276.224-.5.5-.5s.5.224.5.5v10.985c0 4.695 3.82 8.515 8.515 8.515h11.985c.276 0 .5.224.5.5S81.391 33.307 81.114 33.307zM75.114 51.307c-.276 0-.5-.224-.5-.5v-3c0-.276.224-.5.5-.5s.5.224.5.5v3C75.614 51.083 75.391 51.307 75.114 51.307zM75.114 59.307c-.276 0-.5-.224-.5-.5v-6c0-.276.224-.5.5-.5s.5.224.5.5v6C75.614 59.083 75.391 59.307 75.114 59.307zM67.956 86.307H32.272c-4.223 0-7.658-3.45-7.658-7.689V25.955c0-2.549 1.264-4.931 3.382-6.371.228-.156.54-.095.695.132.155.229.096.54-.132.695-1.844 1.254-2.944 3.326-2.944 5.544v52.663c0 3.688 2.987 6.689 6.658 6.689h35.685c3.671 0 6.658-3.001 6.658-6.689V60.807c0-.276.224-.5.5-.5s.5.224.5.5v17.811C75.614 82.857 72.179 86.307 67.956 86.307z"/><path fill="#1f212b" d="M39.802 14.307l-.117 11.834c0 2.21-2.085 3.666-4.036 3.666-1.951 0-4.217-1.439-4.217-3.649l.037-12.58c0-1.307 1.607-2.451 2.801-2.451 1.194 0 2.345 1.149 2.345 2.456l.021 10.829c0 0-.083.667-1.005.645-.507-.012-1.145-.356-1.016-.906v-9.843h-.813l-.021 9.708c0 1.38.54 1.948 1.875 1.948s1.959-.714 1.959-2.094V13.665c0-2.271-1.36-3.5-3.436-3.5s-3.564 1.261-3.564 3.532l.032 12.11c0 3.04 2.123 4.906 4.968 4.906 2.845 0 5-1.71 5-4.75V14.307H39.802zM53.114 52.307h-23c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h23c.276 0 .5.224.5.5S53.391 52.307 53.114 52.307zM44.114 59.307h-14c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h14c.276 0 .5.224.5.5S44.391 59.307 44.114 59.307zM70.114 59.307h-24c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h24c.276 0 .5.224.5.5S70.391 59.307 70.114 59.307zM61.114 66.307h-11c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h11c.276 0 .5.224.5.5S61.391 66.307 61.114 66.307zM71.114 66.307h-8c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h8c.276 0 .5.224.5.5S71.391 66.307 71.114 66.307zM48.114 66.307h-18c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h18c.276 0 .5.224.5.5S48.391 66.307 48.114 66.307zM70.114 73.307h-13c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h13c.276 0 .5.224.5.5S70.391 73.307 70.114 73.307zM54.114 73.307h-24c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h24c.276 0 .5.224.5.5S54.391 73.307 54.114 73.307z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 512"><path fill-rule="nonzero" d="M256 0c70.69 0 134.7 28.66 181.02 74.98C483.34 121.31 512 185.31 512 256c0 70.69-28.66 134.7-74.98 181.02C390.7 483.34 326.69 512 256 512c-70.69 0-134.69-28.66-181.02-74.98C28.66 390.7 0 326.69 0 256c0-70.69 28.66-134.69 74.98-181.02C121.31 28.66 185.31 0 256 0zm-21.49 301.51v-2.03c.16-13.46 1.48-24.12 4.07-32.05 2.54-7.92 6.19-14.37 10.97-19.25 4.77-4.92 10.51-9.39 17.22-13.46 4.31-2.74 8.22-5.78 11.68-9.18 3.45-3.36 6.19-7.27 8.23-11.69 2.02-4.37 3.04-9.24 3.04-14.62 0-6.4-1.52-11.94-4.57-16.66-3-4.68-7.06-8.28-12.04-10.87-5.03-2.54-10.61-3.81-16.76-3.81-5.53 0-10.81 1.11-15.89 3.45-5.03 2.29-9.25 5.89-12.55 10.77-3.3 4.87-5.23 11.12-5.74 18.74h-32.91c.51-12.95 3.81-23.92 9.85-32.91 6.1-8.99 14.13-15.8 24.08-20.42 10.01-4.62 21.08-6.9 33.16-6.9 13.31 0 24.89 2.43 34.84 7.41 9.96 4.93 17.73 11.83 23.27 20.67 5.48 8.84 8.28 19.1 8.28 30.88 0 8.08-1.27 15.34-3.81 21.79-2.54 6.45-6.1 12.24-10.77 17.27-4.68 5.08-10.21 9.54-16.71 13.41-6.15 3.86-11.12 7.82-14.88 11.93-3.81 4.11-6.56 8.99-8.28 14.58-1.73 5.63-2.69 12.59-2.84 20.92v2.03h-30.94zm16.36 65.82c-5.94-.04-11.02-2.13-15.29-6.35-4.26-4.21-6.35-9.34-6.35-15.33 0-5.89 2.09-10.97 6.35-15.19 4.27-4.21 9.35-6.35 15.29-6.35 5.84 0 10.92 2.14 15.18 6.35 4.32 4.22 6.45 9.3 6.45 15.19 0 3.96-1.01 7.62-2.99 10.87-1.98 3.3-4.57 5.94-7.82 7.87-3.25 1.93-6.86 2.9-10.82 2.94zM417.71 94.29C376.33 52.92 319.15 27.32 256 27.32c-63.15 0-120.32 25.6-161.71 66.97C52.92 135.68 27.32 192.85 27.32 256c0 63.15 25.6 120.33 66.97 161.71 41.39 41.37 98.56 66.97 161.71 66.97 63.15 0 120.33-25.6 161.71-66.97 41.37-41.38 66.97-98.56 66.97-161.71 0-63.15-25.6-120.32-66.97-161.71z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,116 @@
/* 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: small;
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;
}
a.khoj-banner {
color: black;
text-decoration: none;
}
p.khoj-banner {
font-size: small;
margin: 0;
padding: 10px;
}
p#khoj-banner {
display: inline;
}
@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;
}
}

File diff suppressed because it is too large Load diff

1823
src/interface/desktop/assets/org.min.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,559 @@
<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="./assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj_chat.webmanifest">
<link rel="stylesheet" href="./assets/khoj.css">
</head>
<script>
let chatOptions = [];
function copyProgrammaticOutput(event) {
// Remove the first 4 characters which are the "Copy" button
const programmaticOutput = event.target.parentNode.textContent.trim().slice(4);
navigator.clipboard.writeText(programmaticOutput).then(() => {
console.log("Programmatic output copied to clipboard");
}).catch((error) => {
console.error("Error copying programmatic output to clipboard:", error);
});
}
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('"', '&quot;');
// 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";
let formattedMessage = formatHTMLMessage(message);
// 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}">${formattedMessage}</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 formatHTMLMessage(htmlMessage) {
// Replace any ``` with <div class="programmatic-output">
let newHTML = htmlMessage.replace(/```([\s\S]*?)```/g, '<div class="programmatic-output"><button class="copy-button" onclick="copyProgrammaticOutput(event)">Copy</button>$1</div>');
// Replace any ** with <b> and __ with <u>
newHTML = newHTML.replace(/\*\*([\s\S]*?)\*\*/g, '<b>$1</b>');
newHTML = newHTML.replace(/__([\s\S]*?)__/g, '<u>$1</u>');
return newHTML;
}
async function chat() {
// Extract required fields for search from form
let query = document.getElementById("chat-input").value.trim();
let resultsCount = 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 = "";
autoResize();
document.getElementById("chat-input").setAttribute("disabled", "disabled");
let hostURL = await window.hostURLAPI.getURL();
// Generate backend API URL to execute query
let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&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 newResponseText = document.createElement("div");
newResponseText.classList.add("chat-message-text", "khoj");
new_response.appendChild(newResponseText);
// Temporary status message to indicate that Khoj is thinking
let loadingSpinner = document.createElement("div");
loadingSpinner.classList.add("spinner");
newResponseText.appendChild(loadingSpinner);
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
let chatTooltip = document.getElementById("chat-tooltip");
chatTooltip.style.display = "none";
let chatInput = document.getElementById("chat-input");
chatInput.classList.remove("option-enabled");
// 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) {
// Evaluate the contents of new_response_text.innerHTML after all the data has been streamed
const currentHTML = newResponseText.innerHTML;
newResponseText.innerHTML = formatHTMLMessage(currentHTML);
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];
newResponseText.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>");
newResponseText.innerHTML += polishedReference;
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
readStream();
} else {
// Display response from Khoj
if (newResponseText.getElementsByClassName("spinner").length > 0) {
newResponseText.removeChild(loadingSpinner);
}
newResponseText.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();
document.getElementById("chat-input").removeAttribute("disabled");
});
}
function incrementalChat(event) {
if (!event.shiftKey && event.key === 'Enter') {
chat();
}
}
function onChatInput() {
let chatInput = document.getElementById("chat-input");
chatInput.value = chatInput.value.trimStart();
if (chatInput.value.startsWith("/") && chatInput.value.split(" ").length === 1) {
let chatTooltip = document.getElementById("chat-tooltip");
chatTooltip.style.display = "block";
let helpText = "<div>";
const command = chatInput.value.split(" ")[0].substring(1);
for (let key in chatOptions) {
if (!!!command || key.startsWith(command)) {
helpText += "<b>/" + key + "</b>: " + chatOptions[key] + "<br>";
}
}
chatTooltip.innerHTML = helpText;
} else if (chatInput.value.startsWith("/")) {
const firstWord = chatInput.value.split(" ")[0];
if (firstWord.substring(1) in chatOptions) {
chatInput.classList.add("option-enabled");
} else {
chatInput.classList.remove("option-enabled");
}
let chatTooltip = document.getElementById("chat-tooltip");
chatTooltip.style.display = "none";
} else {
let chatTooltip = document.getElementById("chat-tooltip");
chatTooltip.style.display = "none";
chatInput.classList.remove("option-enabled");
}
autoResize();
}
function autoResize() {
const textarea = document.getElementById('chat-input');
const scrollTop = textarea.scrollTop;
textarea.style.height = '0';
const scrollHeight = textarea.scrollHeight;
textarea.style.height = Math.min(scrollHeight, 200) + 'px';
textarea.scrollTop = scrollTop;
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
}
window.addEventListener('load', async() => {
await loadChat();
});
async function loadChat() {
const hostURL = await window.hostURLAPI.getURL();
fetch(`${hostURL}/api/chat/history?client=web`)
.then(response => response.json())
.then(data => {
if (data.detail) {
// If the server returns a 500 error with detail, render a setup hint.
renderMessage("Hi 👋🏾, to get started you have two options:<ol><li><b>Use OpenAI</b>: <ol><li>Get your <a class='inline-chat-link' href='https://platform.openai.com/account/api-keys'>OpenAI API key</a></li><li>Save it in the Khoj <a class='inline-chat-link' href='/config/processor/conversation/openai'>chat settings</a></li><li>Click Configure on the Khoj <a class='inline-chat-link' href='/config'>settings page</a></li></ol></li><li><b>Enable offline chat</b>: <ol><li>Go to the Khoj <a class='inline-chat-link' href='/config'>settings page</a> and enable offline chat</li></ol></li></ol>", "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;
});
fetch(`${hostURL}/api/chat/options`)
.then(response => response.json())
.then(data => {
// Render chat options, if any
if (data) {
chatOptions = data;
}
})
.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>
<div id="khoj-banner-container" class="khoj-banner-container">
</div>
<!--Add Header Logo and Nav Pane-->
<div class="khoj-header">
<a class="khoj-logo" href="/">
<img class="khoj-logo" src="./assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
</a>
<nav class="khoj-nav">
<a class="khoj-nav khoj-nav-selected" href="./chat.html">Chat</a>
<a class="khoj-nav" href="./index.html">Search</a>
<a class="khoj-nav" href="./config.html">⚙️</a>
</nav>
</div>
<!-- Chat Body -->
<div id="chat-body"></div>
<!-- Chat Footer -->
<div id="chat-footer">
<div id="chat-tooltip" style="display: none;"></div>
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeyup=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter.">
</textarea>
</div>
</body>
<style>
html, body {
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
}
body {
display: grid;
background: #fff;
color: #475569;
text-align: center;
font-family: roboto, karma, segoe ui, sans-serif;
font-size: small;
font-weight: 300;
line-height: 1.5em;
}
body > * {
padding: 10px;
margin: 10px;
}
#chat-body {
font-size: small;
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;
white-space: pre-line;
}
/* 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;
}
/* Spinner symbol when the chat message is loading */
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-inverse);
border-radius: 50%;
width: 12px;
height: 12px;
animation: spin 2s linear infinite;
margin: 0px 0px 0px 10px;
display: inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 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-family: roboto, karma, segoe ui, sans-serif;
font-size: small;
height: 54px;
resize: none;
overflow-y: hidden;
max-height: 200px;
box-sizing: border-box;
padding: 15px;
line-height: 1.5em;
margin: 0;
}
#chat-input:focus {
outline: none !important;
}
.option-enabled {
box-shadow: 0 0 12px rgb(119, 156, 46);
}
.option-enabled:focus {
outline: none !important;
border:1px solid #475569;
box-shadow: 0 0 16px var(--primary);
}
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 small;
padding: 2px 4px;
}
}
@media only screen and (max-width: 600px) {
body {
grid-template-columns: 1fr;
grid-template-rows: auto auto minmax(80px, 100%) auto;
}
body > * {
grid-column: 1;
}
#chat-footer {
padding: 0;
margin: 4px;
grid-template-columns: auto;
}
a.khoj-banner {
display: block;
}
p.khoj-banner {
padding: 0;
}
}
@media only screen and (min-width: 600px) {
body {
grid-template-columns: auto min(70vw, 100%) auto;
grid-template-rows: auto auto minmax(80px, 100%) auto;
}
body > * {
grid-column: 2;
}
}
div.khoj-banner-container {
padding: 0px;
margin: 0px;
}
div#chat-tooltip {
text-align: left;
font-size: medium;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
a.khoj-logo {
text-align: center;
}
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;
}
div.khoj-banner-container-hidden {
margin: 0px;
padding: 0px;
}
div.programmatic-output {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 3px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
color: #333;
font-family: monospace;
font-size: small;
line-height: 1.5;
margin: 10px 0;
overflow-x: auto;
padding: 10px;
white-space: pre-wrap;
}
</style>
</html>

View file

@ -0,0 +1,342 @@
<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="./assets/icons/favicon-128x128.png">
<link rel="manifest" href="./khoj.webmanifest">
<link rel="stylesheet" href="./assets/khoj.css">
</head>
<script type="text/javascript" src="./assets/org.min.js"></script>
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
<body>
<div class="page">
<!--Add Header Logo and Nav Pane-->
<div class="khoj-header">
<a class="khoj-logo" href="./index.html">
<img class="khoj-logo" src="./assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
</a>
<nav class="khoj-nav">
<a class="khoj-nav" href="./chat.html">Chat</a>
<a class="khoj-nav" href="./index.html">Search</a>
<a class="khoj-nav khoj-nav-selected" href="./config.html">⚙️</a>
</nav>
</div>
</div>
<div class="section-cards">
<div class="card configuration">
<div class="card-title-row">
<img class="card-icon" src="./assets/icons/link.svg" alt="File">
<h3 class="card-title">
Host
</h3>
</div>
<div class="card-description-row">
<input id="khoj-host-url" class="card-input" type="text">
</div>
<div class="card-title-row">
<img class="card-icon" src="./assets/icons/plaintext.svg" alt="File">
<h3 class="card-title">
Files
<button id="toggle-files" class="card-button">
<svg id="toggle-files-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="M12 5v14M5 12l7 7 7-7"></path></svg>
</button>
</h3>
</div>
<div class="card-description-row">
<div id="current-files"></div>
</div>
<div class="card-title-row">
<img class="card-icon" src="./assets/icons/folder.svg" alt="Folder">
<h3 class="card-title">
Folders
<button id="toggle-folders" class="card-button">
<svg id="toggle-folders-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="M12 5v14M5 12l7 7 7-7"></path></svg>
</button>
</h3>
</div>
<div class="card-description-row">
<div id="current-folders"></div>
</div>
<div class="card-action-row">
<button id="update-data" class="card-button">
Add
<img class="add-files-icon" src="./assets/icons/circular-add.svg" alt="Add">
</button>
</div>
<div class="card-description-row">
<button id="sync-data">Sync</button>
</div>
<div class="card-description-row">
<div id="sync-status"></div>
</div>
</div>
</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, input {
padding: 0px;
margin: 0px;
background: #fff;
color: #475569;
font-family: roboto, karma, segoe ui, sans-serif;
font-size: small;
font-weight: 300;
line-height: 1.5em;
}
body > * {
padding: 10px;
margin: 10px;
}
div.page {
padding: 0px;
margin: 0px;
}
svg {
transition: transform 0.3s ease-in-out;
}
a.khoj-logo {
text-align: center;
}
#khoj-host-url {
padding: 4px;
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2);
border: none;
}
.card {
display: grid;
/* grid-template-rows: repeat(3, 1fr); */
gap: 8px;
padding: 24px 16px;
width: 100%;
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;
}
.section-cards {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 16px;
justify-items: start;
margin: 0;
width: auto;
}
div.configuration {
width: auto;
}
.card-title-row {
display: grid;
grid-template-columns: auto 1fr;
padding: 0;
gap: 12px;
}
.card-icon {
width: 28px;
height: 28px;
}
.add-files-icon {
width: 16px;
height: 16px;
}
.card-title {
font-size: medium;
font-weight: normal;
margin: 0;
padding: 0;
align-self: center;
}
.card-title-text {
vertical-align: middle;
}
.card-description {
margin: 0;
color: grey;
font-size: small;
}
.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: small;
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: small;
}
button.card-button.disabled {
color: rgb(255, 136, 136);
background: transparent;
font-size: small;
cursor: pointer;
margin: 0;
padding: 0;
height: 32px;
text-align: right;
text-align: left;
}
button.card-button.happy {
color: rgb(0, 146, 0);
}
button.card-button.happy {
color: rgb(0, 146, 0);
}
img.configured-icon {
max-width: 16px;
}
div.card-action-row.enabled{
display: block;
}
img.configured-icon.enabled {
display: inline;
}
div.card-action-row.disabled,
img.configured-icon.disabled {
display: none;
}
div.file-element,
div.folder-element {
display: grid;
grid-template-columns: auto 1fr;
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2);
padding: 4px;
margin-bottom: 8px;
}
div.content-name {
width: 500px;
overflow-wrap: break-word;
}
div.remove-button-container {
text-align: right;
}
button.remove-folder-button,
button.remove-file-button {
background-color: rgb(253 214 214);
border-radius: 3px;
border: none;
color: rgb(207, 67, 59);
padding: 4px;
}
button.remove-folder-button:hover,
button.remove-file-button:hover {
background-color: rgb(255 235 235);
border-radius: 3px;
border: none;
color: rgb(207, 67, 59);
padding: 4px;
cursor: pointer;
}
#sync-data {
background-color: #ffb300;
border: none;
color: white;
padding: 12px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 5px 0px #f9f5de;
}
#sync-data:hover {
background-color: #ffcc00;
box-shadow: 0px 3px 0px #f9f5de;
}
</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://app.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>
<script src="./renderer.js"></script>
</html>

View file

@ -0,0 +1,499 @@
<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="./assets/icons/favicon-128x128.png">
<link rel="manifest" href="./khoj.webmanifest">
<link rel="stylesheet" href="./assets/khoj.css">
</head>
<script type="text/javascript" src="./assets/org.min.js"></script>
<script type="text/javascript" src="./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, suppressNewLines: true });
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_html(query, data) {
return data.map(function (item) {
let document = new DOMParser().parseFromString(item.entry, "text/html");
// Scrub the HTML to remove any script tags and associated content
let script_tags = document.querySelectorAll("script");
for (let i = 0; i < script_tags.length; i++) {
script_tags[i].remove();
}
// Scrub the HTML to remove any style tags and associated content
let style_tags = document.querySelectorAll("style");
for (let i = 0; i < style_tags.length; i++) {
style_tags[i].remove();
}
// Scrub the HTML to remove any noscript tags and associated content
let noscript_tags = document.querySelectorAll("noscript");
for (let i = 0; i < noscript_tags.length; i++) {
noscript_tags[i].remove();
}
// Scrub the HTML to remove any iframe tags and associated content
let iframe_tags = document.querySelectorAll("iframe");
for (let i = 0; i < iframe_tags.length; i++) {
iframe_tags[i].remove();
}
// Scrub the HTML to remove any object tags and associated content
let object_tags = document.querySelectorAll("object");
for (let i = 0; i < object_tags.length; i++) {
object_tags[i].remove();
}
// Scrub the HTML to remove any embed tags and associated content
let embed_tags = document.querySelectorAll("embed");
for (let i = 0; i < embed_tags.length; i++) {
embed_tags[i].remove();
}
let scrubbedHTML = document.body.outerHTML;
return `<div class="results-html">` + scrubbedHTML + `</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>`;
} else if (item.additional.file.endsWith(".html")) {
html += render_html(query, [item]);
} else {
html += `<div class="results-plugin">` + `<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;
}
async function search(rerank=false) {
// Extract required fields for search from form
query = document.getElementById("query").value.trim();
type = 'all';
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) {
return;
}
// If set query field in url query param on rerank
if (rerank)
setQueryFieldInUrl(query);
// Execute Search and Render Results
url = await 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);
});
}
function incrementalSearch(event) {
type = 'all';
// 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);
}
}
async function populate_type_dropdown() {
const hostURL = await window.hostURLAPI.getURL();
// Populate type dropdown field with enabled content types only
fetch(`${hostURL}/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 [];
}
return enabled_types;
});
}
async function createRequestUrl(query, type, results_count, rerank) {
// Generate Backend API URL to execute Search
const hostURL = await window.hostURLAPI.getURL();
let url = `${hostURL}/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 setQueryFieldInUrl(query) {
var url = new URL(window.location.href);
url.searchParams.set("q", query);
window.history.pushState({}, "", url.href);
}
window.addEventListener("load", async function() {
// Dynamically populate type dropdown based on enabled content types and type passed as URL query parameter
await 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">
<a class="khoj-logo" href="./index.html">
<img class="khoj-logo" src="./assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
</a>
<nav class="khoj-nav">
<a class="khoj-nav" href="./chat.html">Chat</a>
<a class="khoj-nav khoj-nav-selected" href="./index.html">Search</a>
<a class="khoj-nav" href="./config.html">⚙️</a>
</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="Search your knowledge base using natural language">
<!-- 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: small;
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: small;
}
#results {
font-size: small;
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-html,
.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: small;
}
span.org-task-status {
color: white;
padding: 3.5px 3.5px 0;
margin-right: 5px;
border-radius: 5px;
background-color: #eab308;
font-size: small;
}
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-plugin,
div.results-html,
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-logo {
text-align: center;
}
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;
}
@media only screen and (max-width: 600px) {
a.khoj-banner {
display: block;
}
p.khoj-banner {
padding: 0;
}
}
</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://app.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>

View file

@ -0,0 +1,359 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const todesktop = require("@todesktop/runtime");
todesktop.init();
const fs = require('fs');
const {dialog} = require('electron');
const cron = require('cron').CronJob;
const axios = require('axios');
const { Readable } = require('stream');
const KHOJ_URL = 'http://127.0.0.1:42110'
const Store = require('electron-store');
const validFileTypes = ['org', 'md', 'markdown', 'txt', 'html', 'xml', 'pdf']
const binaryFileTypes = ['pdf', 'png', 'jpg', 'jpeg']
const schema = {
files: {
type: 'array',
items: {
type: 'object',
properties: {
path: {
type: 'string'
}
}
},
default: []
},
folders: {
type: 'array',
items: {
type: 'object',
properties: {
path: {
type: 'string'
}
}
},
default: []
},
hostURL: {
type: 'string',
default: KHOJ_URL
},
lastSync: {
type: 'array',
items: {
type: 'object',
properties: {
path: {
type: 'string'
},
datetime: {
type: 'string'
}
}
}
}
};
var state = {}
const store = new Store({schema});
console.log(store);
// include the Node.js 'path' module at the top of your file
const path = require('path');
function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
dialog.showOpenDialog({properties: ['openFile', 'openDirectory'] }).then(function (response) {
if (!response.canceled) {
// handle fully qualified file name
console.log(response.filePaths[0]);
} else {
console.log("no file selected");
}
});
}
function pushDataToKhoj () {
let filesToPush = [];
const files = store.get('files');
const folders = store.get('folders');
state = {
completed: true
}
if (files) {
for (file of files) {
filesToPush.push(file.path);
}
}
if (folders) {
for (folder of folders) {
const files = fs.readdirSync(folder.path, { withFileTypes: true });
for (file of files) {
if (file.isFile() && validFileTypes.includes(file.name.split('.').pop())) {
filesToPush.push(path.join(folder.path, file.name));
}
}
}
}
let data = {
files: []
}
const lastSync = store.get('lastSync') || [];
for (file of filesToPush) {
const stats = fs.statSync(file);
if (stats.mtime.toISOString() < lastSync.find((syncedFile) => syncedFile.path === file)?.datetime) {
continue;
}
try {
let rawData;
// If the file is a PDF or IMG file, read it as a binary file
if (binaryFileTypes.includes(file.split('.').pop())) {
rawData = fs.readFileSync(file).toString('base64');
} else {
rawData = fs.readFileSync(file, 'utf8');
}
data.files.push({
path: file,
content: rawData
});
state[file] = {
success: true,
}
} catch (err) {
console.error(err);
state[file] = {
success: false,
error: err
}
}
}
for (const syncedFile of lastSync) {
if (!filesToPush.includes(syncedFile.path)) {
data.files.push({
path: syncedFile.path,
content: ""
});
}
}
const headers = { 'x-api-key': 'secret', 'Content-Type': 'application/json' };
const stream = new Readable({
read() {
this.push(JSON.stringify(data));
this.push(null);
}
});
const hostURL = store.get('hostURL') || KHOJ_URL;
axios.post(`${hostURL}/v1/indexer/batch`, stream, { headers })
.then(response => {
console.log(response.data);
const win = BrowserWindow.getAllWindows()[0];
win.webContents.send('update-state', state);
let lastSync = [];
for (const file of filesToPush) {
lastSync.push({
path: file,
datetime: new Date().toISOString()
});
}
store.set('lastSync', lastSync);
})
.catch(error => {
console.error(error);
state['completed'] = false
const win = BrowserWindow.getAllWindows()[0];
win.webContents.send('update-state', state);
});
}
pushDataToKhoj();
async function handleFileOpen (event, key) {
const { canceled, filePaths } = await dialog.showOpenDialog({properties: ['openFile', 'openDirectory'], filters: [{ name: "Valid Khoj Files", extensions: validFileTypes}] });
if (!canceled) {
const files = store.get('files') || [];
const folders = store.get('folders') || [];
for (const filePath of filePaths) {
console.log(filePath);
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
if (stats.isFile()) {
console.log(`${filePath} is a file.`);
if (files.find((file) => file.path === filePath)) {
continue;
}
files.push({path: filePath});
store.set('files', files);
} else if (stats.isDirectory()) {
console.log(`${filePath} is a directory.`);
if (folders.find((folder) => folder.path === filePath)) {
continue;
}
folders.push({path: filePath});
store.set('folders', folders);
}
} else {
console.log(`${filePath} does not exist.`);
}
}
return {
files: store.get('files'),
folders: store.get('folders')
}
}
}
async function getFiles () {
return store.get('files');
}
async function getFolders () {
return store.get('folders');
}
async function setURL (event, url) {
store.set('hostURL', url);
return store.get('hostURL');
}
async function getURL () {
return store.get('hostURL');
}
async function removeFile (event, filePath) {
const files = store.get('files');
const newFiles = files.filter((file) => file.path !== filePath);
store.set('files', newFiles);
return newFiles;
}
async function removeFolder (event, folderPath) {
const folders = store.get('folders');
const newFolders = folders.filter((folder) => folder.path !== folderPath);
store.set('folders', newFolders);
return newFolders;
}
async function syncData () {
try {
pushDataToKhoj();
const date = new Date();
console.log('Pushing data to Khoj at: ', date);
} catch (err) {
console.error(err);
}
}
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 800,
// titleBarStyle: 'hidden',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
}
})
const job = new cron('0 */10 * * * *', function() {
try {
pushDataToKhoj();
const date = new Date();
console.log('Pushing data to Khoj at: ', date);
win.webContents.send('update-state', state);
} catch (err) {
console.error(err);
}
});
win.setResizable(true);
win.setOpacity(0.95);
win.setBackgroundColor('#FFFFFF');
win.setHasShadow(true);
job.start();
win.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.on('set-title', handleSetTitle);
ipcMain.handle('getStoreValue', handleFileOpen);
ipcMain.on('update-state', (event, arg) => {
console.log(arg);
event.reply('update-state', arg);
});
ipcMain.handle('getFiles', getFiles);
ipcMain.handle('getFolders', getFolders);
ipcMain.handle('removeFile', removeFile);
ipcMain.handle('removeFolder', removeFolder);
ipcMain.handle('setURL', setURL);
ipcMain.handle('getURL', getURL);
ipcMain.handle('syncData', syncData);
createWindow()
app.setAboutPanelOptions({
applicationName: "Khoj",
applicationVersion: "0.0.1",
version: "0.0.1",
authors: "Khoj Team",
website: "https://khoj.dev",
iconPath: path.join(__dirname, 'assets', 'khoj.png')
});
app.on('ready', async() => {
try {
const result = await todesktop.autoUpdater.checkForUpdates();
if (result.updateInfo) {
console.log("Update found:", result.updateInfo.version);
todesktop.autoUpdater.restartAndInstall();
}
} catch (e) {
console.log("Update check failed:", e);
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

View file

@ -0,0 +1,25 @@
{
"name": "Khoj",
"homepage": ".",
"productName": "Khoj",
"version": "1.0.0",
"description": "Scaffolding for the desktop entrypoint to Khoj",
"main": "main.js",
"repository": "\"https://github.com/khoj-ai/khoj\"",
"author": "Khoj <team@khoj.dev>",
"license": "MIT",
"private": false,
"devDependencies": {
"electron": "26.1.0"
},
"scripts": {
"start": "yarn electron ."
},
"dependencies": {
"@todesktop/runtime": "^1.3.0",
"axios": "^1.5.0",
"cron": "^2.4.3",
"electron-store": "^8.1.0",
"fs": "^0.0.1-security"
}
}

View file

@ -0,0 +1,49 @@
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
contextBridge.exposeInMainWorld('storeValueAPI', {
getStoreValue: (key) => ipcRenderer.invoke('getStoreValue', key)
})
contextBridge.exposeInMainWorld('getFilesAPI', {
getFiles: () => ipcRenderer.invoke('getFiles')
})
contextBridge.exposeInMainWorld('getFoldersAPI', {
getFolders: () => ipcRenderer.invoke('getFolders')
})
contextBridge.exposeInMainWorld('updateStateAPI', {
onUpdateState: (callback) => ipcRenderer.on('update-state', callback)
})
contextBridge.exposeInMainWorld('removeFileAPI', {
removeFile: (filePath) => ipcRenderer.invoke('removeFile', filePath)
})
contextBridge.exposeInMainWorld('removeFolderAPI', {
removeFolder: (folderPath) => ipcRenderer.invoke('removeFolder', folderPath)
})
contextBridge.exposeInMainWorld('hostURLAPI', {
setURL: (url) => ipcRenderer.invoke('setURL', url),
getURL: () => ipcRenderer.invoke('getURL')
})
contextBridge.exposeInMainWorld('syncDataAPI', {
syncData: () => ipcRenderer.invoke('syncData')
})

View file

@ -0,0 +1,177 @@
const getButton = document.getElementById('update-data')
const showKey = document.getElementById('show-key');
async function removeFile(filePath) {
const updatedFiles = await window.removeFileAPI.removeFile(filePath);
let currentFilesElement = document.getElementById("current-files");
currentFilesElement.innerHTML = '';
for (const file of updatedFiles) {
console.log(file);
let fileElement = makeFileElement(file);
currentFilesElement.appendChild(fileElement);
}
}
async function removeFolder(folderPath) {
const updatedFolders = await window.removeFolderAPI.removeFolder(folderPath);
let currentFoldersElement = document.getElementById("current-folders");
currentFoldersElement.innerHTML = '';
for (const folder of updatedFolders) {
console.log(folder);
let folderElement = makeFolderElement(folder);
currentFoldersElement.appendChild(folderElement);
}
}
const toggleFilesButton = document.getElementById('toggle-files');
const currentFiles = document.getElementById('current-files');
const toggleFilesSVG = document.getElementById('toggle-files-svg');
toggleFilesButton.addEventListener('click', () => {
if (currentFiles.style.display === 'none') {
currentFiles.style.display = 'block';
toggleFilesSVG.style.transform = 'rotate(0deg)';
} else {
currentFiles.style.display = 'none';
toggleFilesSVG.style.transform = 'rotate(180deg)';
}
});
const toggleFoldersButton = document.getElementById('toggle-folders');
const currentFolders = document.getElementById('current-folders');
const toggleFoldersSVG = document.getElementById('toggle-folders-svg');
toggleFoldersButton.addEventListener('click', () => {
if (currentFolders.style.display === 'none') {
currentFolders.style.display = 'block';
toggleFoldersSVG.style.transform = 'rotate(0deg)';
} else {
currentFolders.style.display = 'none';
toggleFoldersSVG.style.transform = 'rotate(180deg)';
}
});
function makeFileElement(file) {
let fileElement = document.createElement("div");
fileElement.classList.add("file-element");
let fileNameElement = document.createElement("div");
fileNameElement.classList.add("content-name");
fileNameElement.innerHTML = file.path;
fileElement.appendChild(fileNameElement);
let buttonContainer = document.createElement("div");
buttonContainer.classList.add("remove-button-container");
let removeFileButton = document.createElement("button");
removeFileButton.classList.add("remove-file-button");
removeFileButton.innerHTML = "🗑️";
removeFileButton.addEventListener("click", () => {
removeFile(file.path);
});
buttonContainer.appendChild(removeFileButton);
fileElement.appendChild(buttonContainer);
return fileElement;
}
function makeFolderElement(folder) {
let folderElement = document.createElement("div");
folderElement.classList.add("folder-element");
let folderNameElement = document.createElement("div");
folderNameElement.classList.add("content-name");
folderNameElement.innerHTML = folder.path;
folderElement.appendChild(folderNameElement);
let buttonContainer = document.createElement("div");
buttonContainer.classList.add("remove-button-container");
let removeFolderButton = document.createElement("button");
removeFolderButton.classList.add("remove-folder-button");
removeFolderButton.innerHTML = "🗑️";
removeFolderButton.addEventListener("click", () => {
removeFolder(folder.path);
});
buttonContainer.appendChild(removeFolderButton);
folderElement.appendChild(buttonContainer);
return folderElement;
}
(async function() {
const files = await window.getFilesAPI.getFiles();
let currentFilesElement = document.getElementById("current-files");
for (const file of files) {
console.log(file);
let fileElement = makeFileElement(file);
currentFilesElement.appendChild(fileElement);
}
const folders = await window.getFoldersAPI.getFolders();
let currentFoldersElement = document.getElementById("current-folders");
for (const folder of folders) {
let folderElement = makeFolderElement(folder);
currentFoldersElement.appendChild(folderElement);
}
})();
getButton.addEventListener('click', async () => {
const key = 'foo';
const value = await window.storeValueAPI.getStoreValue(key);
console.log(value);
let currentFilesElement = document.getElementById("current-files");
let currentFoldersElement = document.getElementById("current-folders");
if (value.files) {
currentFilesElement.innerHTML = '';
value.files.forEach((file) => {
let fileElement = makeFileElement(file);
currentFilesElement.appendChild(fileElement);
});
}
if (value.folders) {
currentFoldersElement.innerHTML = '';
value.folders.forEach((folder) => {
let folderElement = makeFolderElement(folder);
currentFoldersElement.appendChild(folderElement);
});
}
});
window.updateStateAPI.onUpdateState((event, state) => {
console.log("state was updated", state);
let syncStatusElement = document.getElementById("sync-status");
const currentTime = new Date();
if (state.completed == false) {
syncStatusElement.innerHTML = `Sync was unsuccessful at ${currentTime.toLocaleTimeString()}. Contact team@khoj.dev to report this issue.`;
return;
}
syncStatusElement.innerHTML = `Last synced at ${currentTime.toLocaleTimeString()}`;
});
const urlInput = document.getElementById('khoj-host-url');
(async function() {
const url = await window.hostURLAPI.getURL();
urlInput.value = url;
})();
urlInput.addEventListener('blur', async () => {
const urlInputValue = urlInput.value;
// Check if it's a valid URL
try {
new URL(urlInputValue);
} catch (e) {
console.log(e);
return;
}
const url = await window.hostURLAPI.setURL(urlInput.value.trim());
urlInput.value = url;
});
const syncButton = document.getElementById('sync-data');
syncButton.addEventListener('click', async () => {
await window.syncDataAPI.syncData();
});

View file

@ -0,0 +1,6 @@
{
"id": "",
"icon": "./assets/icons/favicon-128x128.png",
"appPath": ".",
"schemaVersion": 1
}

File diff suppressed because it is too large Load diff

View file

@ -105,7 +105,7 @@ def configure_routes(app):
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(indexer, prefix="/indexer")
app.include_router(indexer, prefix="/v1/indexer")
app.include_router(web_client)

View file

@ -37,7 +37,7 @@ class GithubToJsonl(TextToJsonl):
else:
return
def process(self, previous_entries=[], files=None):
def process(self, previous_entries=[], files=None, full_corpus=True):
if self.config.pat_token is None or self.config.pat_token == "":
logger.error(f"Github PAT token is not set. Skipping github content")
raise ValueError("Github PAT token is not set. Skipping github content")

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
class JsonlToJsonl(TextToJsonl):
# Define Functions
def process(self, previous_entries=[], files: dict[str, str] = {}):
def process(self, previous_entries=[], files: dict[str, str] = {}, full_corpus: bool = True):
# Extract required fields from config
input_jsonl_files, input_jsonl_filter, output_file = (
self.config.input_files,

View file

@ -22,10 +22,17 @@ class MarkdownToJsonl(TextToJsonl):
self.config = config
# Define Functions
def process(self, previous_entries=[], files=None):
def process(self, previous_entries=[], files=None, full_corpus: bool = True):
# Extract required fields from config
output_file = self.config.compressed_jsonl
if not full_corpus:
deletion_file_names = set([file for file in files if files[file] == ""])
files_to_process = set(files) - deletion_file_names
files = {file: files[file] for file in files_to_process}
else:
deletion_file_names = None
# Extract Entries from specified Markdown files
with timer("Parse entries from Markdown files into dictionaries", logger):
current_entries = MarkdownToJsonl.convert_markdown_entries_to_maps(
@ -39,7 +46,7 @@ class MarkdownToJsonl(TextToJsonl):
# Identify, mark and merge any new entries with previous entries
with timer("Identify new or updated entries", logger):
entries_with_ids = TextToJsonl.mark_entries_for_update(
current_entries, previous_entries, key="compiled", logger=logger
current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names
)
with timer("Write markdown entries to JSONL file", logger):

View file

@ -80,7 +80,7 @@ class NotionToJsonl(TextToJsonl):
self.body_params = {"page_size": 100}
def process(self, previous_entries=[], files=None):
def process(self, previous_entries=[], files=None, full_corpus=True):
current_entries = []
# Get all pages

View file

@ -21,11 +21,20 @@ class OrgToJsonl(TextToJsonl):
self.config = config
# Define Functions
def process(self, previous_entries: List[Entry] = [], files: dict[str, str] = None) -> List[Tuple[int, Entry]]:
def process(
self, previous_entries: List[Entry] = [], files: dict[str, str] = None, full_corpus: bool = True
) -> List[Tuple[int, Entry]]:
# Extract required fields from config
output_file = self.config.compressed_jsonl
index_heading_entries = self.config.index_heading_entries
if not full_corpus:
deletion_file_names = set([file for file in files if files[file] == ""])
files_to_process = set(files) - deletion_file_names
files = {file: files[file] for file in files_to_process}
else:
deletion_file_names = None
# Extract Entries from specified Org files
with timer("Parse entries from org files into OrgNode objects", logger):
entry_nodes, file_to_entries = self.extract_org_entries(files)
@ -39,7 +48,7 @@ class OrgToJsonl(TextToJsonl):
# Identify, mark and merge any new entries with previous entries
with timer("Identify new or updated entries", logger):
entries_with_ids = TextToJsonl.mark_entries_for_update(
current_entries, previous_entries, key="compiled", logger=logger
current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names
)
# Process Each Entry from All Notes Files

View file

@ -2,9 +2,10 @@
import os
import logging
from typing import List
import base64
# External Packages
from langchain.document_loaders import PyPDFLoader
from langchain.document_loaders import PyMuPDFLoader
# Internal Packages
from khoj.processor.text_to_jsonl import TextToJsonl
@ -18,10 +19,17 @@ logger = logging.getLogger(__name__)
class PdfToJsonl(TextToJsonl):
# Define Functions
def process(self, previous_entries=[], files=dict[str, str]):
def process(self, previous_entries=[], files: dict[str, str] = None, full_corpus: bool = True):
# Extract required fields from config
output_file = self.config.compressed_jsonl
if not full_corpus:
deletion_file_names = set([file for file in files if files[file] == ""])
files_to_process = set(files) - deletion_file_names
files = {file: files[file] for file in files_to_process}
else:
deletion_file_names = None
# Extract Entries from specified Pdf files
with timer("Parse entries from PDF files into dictionaries", logger):
current_entries = PdfToJsonl.convert_pdf_entries_to_maps(*PdfToJsonl.extract_pdf_entries(files))
@ -33,7 +41,7 @@ class PdfToJsonl(TextToJsonl):
# Identify, mark and merge any new entries with previous entries
with timer("Identify new or updated entries", logger):
entries_with_ids = TextToJsonl.mark_entries_for_update(
current_entries, previous_entries, key="compiled", logger=logger
current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names
)
with timer("Write PDF entries to JSONL file", logger):
@ -55,9 +63,11 @@ class PdfToJsonl(TextToJsonl):
for pdf_file in pdf_files:
try:
# Write the PDF file to a temporary file, as it is stored in byte format in the pdf_file object and the PyPDFLoader expects a file path
with open(f"{pdf_file}.pdf", "wb") as f:
f.write(pdf_files[pdf_file])
loader = PyPDFLoader(f"{pdf_file}.pdf")
tmp_file = f"tmp_pdf_file.pdf"
with open(f"{tmp_file}", "wb") as f:
bytes = base64.b64decode(pdf_files[pdf_file])
f.write(bytes)
loader = PyMuPDFLoader(f"{tmp_file}")
pdf_entries_per_file = [page.page_content for page in loader.load()]
entry_to_location_map += zip(pdf_entries_per_file, [pdf_file] * len(pdf_entries_per_file))
entries.extend(pdf_entries_per_file)
@ -65,8 +75,8 @@ class PdfToJsonl(TextToJsonl):
logger.warning(f"Unable to process file: {pdf_file}. This file will not be indexed.")
logger.warning(e)
finally:
if os.path.exists(f"{pdf_file}.pdf"):
os.remove(f"{pdf_file}.pdf")
if os.path.exists(f"{tmp_file}"):
os.remove(f"{tmp_file}")
return entries, dict(entry_to_location_map)

View file

@ -15,9 +15,18 @@ logger = logging.getLogger(__name__)
class PlaintextToJsonl(TextToJsonl):
# Define Functions
def process(self, previous_entries: List[Entry] = [], files: dict[str, str] = None) -> List[Tuple[int, Entry]]:
def process(
self, previous_entries: List[Entry] = [], files: dict[str, str] = None, full_corpus: bool = True
) -> List[Tuple[int, Entry]]:
output_file = self.config.compressed_jsonl
if not full_corpus:
deletion_file_names = set([file for file in files if files[file] == ""])
files_to_process = set(files) - deletion_file_names
files = {file: files[file] for file in files_to_process}
else:
deletion_file_names = None
# Extract Entries from specified plaintext files
with timer("Parse entries from plaintext files", logger):
current_entries = PlaintextToJsonl.convert_plaintext_entries_to_maps(files)
@ -29,7 +38,7 @@ class PlaintextToJsonl(TextToJsonl):
# Identify, mark and merge any new entries with previous entries
with timer("Identify new or updated entries", logger):
entries_with_ids = TextToJsonl.mark_entries_for_update(
current_entries, previous_entries, key="compiled", logger=logger
current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names
)
with timer("Write entries to JSONL file", logger):

View file

@ -2,7 +2,7 @@
from abc import ABC, abstractmethod
import hashlib
import logging
from typing import Callable, List, Tuple
from typing import Callable, List, Tuple, Set
from khoj.utils.helpers import timer
# Internal Packages
@ -17,7 +17,9 @@ class TextToJsonl(ABC):
self.config = config
@abstractmethod
def process(self, previous_entries: List[Entry] = [], files: dict[str, str] = None) -> List[Tuple[int, Entry]]:
def process(
self, previous_entries: List[Entry] = [], files: dict[str, str] = None, full_corpus: bool = True
) -> List[Tuple[int, Entry]]:
...
@staticmethod
@ -62,12 +64,21 @@ class TextToJsonl(ABC):
@staticmethod
def mark_entries_for_update(
current_entries: List[Entry], previous_entries: List[Entry], key="compiled", logger: logging.Logger = None
) -> List[Tuple[int, Entry]]:
current_entries: List[Entry],
previous_entries: List[Entry],
key="compiled",
logger: logging.Logger = None,
deletion_filenames: Set[str] = None,
):
# Hash all current and previous entries to identify new entries
with timer("Hash previous, current entries", logger):
current_entry_hashes = list(map(TextToJsonl.hash_func(key), current_entries))
previous_entry_hashes = list(map(TextToJsonl.hash_func(key), previous_entries))
if deletion_filenames is not None:
deletion_entries = [entry for entry in previous_entries if entry.file in deletion_filenames]
deletion_entry_hashes = list(map(TextToJsonl.hash_func(key), deletion_entries))
else:
deletion_entry_hashes = []
with timer("Identify, Mark, Combine new, existing entries", logger):
hash_to_current_entries = dict(zip(current_entry_hashes, current_entries))
@ -77,6 +88,19 @@ class TextToJsonl(ABC):
new_entry_hashes = set(current_entry_hashes) - set(previous_entry_hashes)
# All entries that exist in both current and previous sets are kept
existing_entry_hashes = set(current_entry_hashes) & set(previous_entry_hashes)
# All entries that exist in the previous set but not in the current set should be preserved
remaining_entry_hashes = set(previous_entry_hashes) - set(current_entry_hashes)
# All entries that exist in the previous set and also in the deletions set should be removed
to_delete_entry_hashes = set(previous_entry_hashes) & set(deletion_entry_hashes)
preserving_entry_hashes = existing_entry_hashes
if deletion_filenames is not None:
preserving_entry_hashes = (
(existing_entry_hashes | remaining_entry_hashes)
if len(deletion_entry_hashes) == 0
else (set(previous_entry_hashes) - to_delete_entry_hashes)
)
# load new entries in the order in which they are processed for a stable sort
new_entries = [
@ -90,7 +114,7 @@ class TextToJsonl(ABC):
# Set id of existing entries to their previous ids to reuse their existing encoded embeddings
existing_entries = [
(previous_entry_hashes.index(entry_hash), hash_to_previous_entries[entry_hash])
for entry_hash in existing_entry_hashes
for entry_hash in preserving_entry_hashes
]
existing_entries_sorted = sorted(existing_entries, key=lambda e: e[0])

View file

@ -1,6 +1,7 @@
# Standard Packages
import logging
from typing import Optional, Union
import sys
from typing import Optional, Union, Dict
# External Packages
from fastapi import APIRouter, HTTPException, Header, Request, Body, Response
@ -15,10 +16,11 @@ 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.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl
from khoj.utils.rawconfig import ContentConfig
from khoj.utils.rawconfig import ContentConfig, TextContentConfig
from khoj.search_type import text_search, image_search
from khoj.utils.config import SearchModels
from khoj.utils.helpers import LRU
from khoj.utils.constants import default_config
from khoj.utils.helpers import LRU, get_file_type
from khoj.utils.rawconfig import (
ContentConfig,
)
@ -35,11 +37,20 @@ logger = logging.getLogger(__name__)
indexer = APIRouter()
class File(BaseModel):
path: str
content: Union[str, bytes]
class IndexBatchRequest(BaseModel):
org: Optional[dict[str, str]]
pdf: Optional[dict[str, str]]
plaintext: Optional[dict[str, str]]
markdown: Optional[dict[str, str]]
files: list[File]
class IndexerInput(BaseModel):
org: Optional[dict[str, str]] = None
markdown: Optional[dict[str, str]] = None
pdf: Optional[dict[str, bytes]] = None
plaintext: Optional[dict[str, str]] = None
@indexer.post("/batch")
@ -54,24 +65,65 @@ async def index_batch(
state.config_lock.acquire()
try:
logger.info(f"Received batch indexing request")
index_batch_request_acc = ""
index_batch_request_acc = b""
async for chunk in request.stream():
index_batch_request_acc += chunk.decode()
index_batch_request_acc += chunk
data_bytes = sys.getsizeof(index_batch_request_acc)
unit = "KB"
data_size = data_bytes / 1024
if data_size > 1000:
unit = "MB"
data_size = data_size / 1024
if data_size > 1000:
unit = "GB"
data_size = data_size / 1024
data_size_metric = f"{data_size:.2f} {unit}"
logger.info(f"Received {data_size_metric} of data")
index_batch_request = IndexBatchRequest.parse_raw(index_batch_request_acc)
logger.info(f"Received batch indexing request size: {len(index_batch_request.dict())}")
logger.info(f"Received {len(index_batch_request.files)} files")
org_files: Dict[str, str] = {}
markdown_files: Dict[str, str] = {}
pdf_files: Dict[str, str] = {}
plaintext_files: Dict[str, str] = {}
for file in index_batch_request.files:
file_type = get_file_type(file.path)
dict_to_update = None
if file_type == "org":
dict_to_update = org_files
elif file_type == "markdown":
dict_to_update = markdown_files
elif file_type == "pdf":
dict_to_update = pdf_files
elif file_type == "plaintext":
dict_to_update = plaintext_files
if dict_to_update is not None:
dict_to_update[file.path] = file.content
else:
logger.info(f"Skipping unsupported streamed file: {file.path}")
indexer_input = IndexerInput(
org=org_files,
markdown=markdown_files,
pdf=pdf_files,
plaintext=plaintext_files,
)
# Extract required fields from config
state.content_index = configure_content(
state.content_index,
state.config.content_type,
index_batch_request.dict(),
indexer_input.dict(),
state.search_models,
regenerate=regenerate,
t=search_type,
full_corpus=False,
)
except Exception as e:
logger.error(f"Failed to process batch indexing request: {e}")
logger.error(f"Failed to process batch indexing request: {e}", exc_info=True)
finally:
state.config_lock.release()
return Response(content="OK", status_code=200)
@ -84,6 +136,7 @@ def configure_content(
search_models: SearchModels,
regenerate: bool = False,
t: Optional[Union[state.SearchType, str]] = None,
full_corpus: bool = True,
) -> Optional[ContentIndex]:
# Run Validation Checks
if content_config is None:
@ -105,10 +158,17 @@ def configure_content(
# Initialize Org Notes Search
if (
(t == None or t == state.SearchType.Org.value)
and content_config.org
and (content_config.org or files["org"])
and search_models.text_search
and files["org"]
):
if content_config.org == None:
logger.info("🦄 No configuration for orgmode notes. Using default configuration.")
default_configuration = default_config["content-type"]["org"] # type: ignore
content_config.org = TextContentConfig(
compressed_jsonl=default_configuration["compressed-jsonl"],
embeddings_file=default_configuration["embeddings-file"],
)
logger.info("🦄 Setting up search for orgmode notes")
# Extract Entries, Generate Notes Embeddings
content_index.org = text_search.setup(
@ -118,15 +178,27 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup org: {e}", exc_info=True)
try:
# Initialize Markdown Search
if (
(t == None or t == state.SearchType.Markdown.value)
and content_config.markdown
and (content_config.markdown or files["markdown"])
and search_models.text_search
and files["markdown"]
):
if content_config.markdown == None:
logger.info("💎 No configuration for markdown notes. Using default configuration.")
default_configuration = default_config["content-type"]["markdown"] # type: ignore
content_config.markdown = TextContentConfig(
compressed_jsonl=default_configuration["compressed-jsonl"],
embeddings_file=default_configuration["embeddings-file"],
)
logger.info("💎 Setting up search for markdown notes")
# Extract Entries, Generate Markdown Embeddings
content_index.markdown = text_search.setup(
@ -136,15 +208,28 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup markdown: {e}", exc_info=True)
try:
# Initialize PDF Search
if (
(t == None or t == state.SearchType.Pdf.value)
and content_config.pdf
and (content_config.pdf or files["pdf"])
and search_models.text_search
and files["pdf"]
):
if content_config.pdf == None:
logger.info("🖨️ No configuration for pdf notes. Using default configuration.")
default_configuration = default_config["content-type"]["pdf"] # type: ignore
content_config.pdf = TextContentConfig(
compressed_jsonl=default_configuration["compressed-jsonl"],
embeddings_file=default_configuration["embeddings-file"],
)
logger.info("🖨️ Setting up search for pdf")
# Extract Entries, Generate PDF Embeddings
content_index.pdf = text_search.setup(
@ -154,15 +239,28 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup PDF: {e}", exc_info=True)
try:
# Initialize Plaintext Search
if (
(t == None or t == state.SearchType.Plaintext.value)
and content_config.plaintext
and (content_config.plaintext or files["plaintext"])
and search_models.text_search
and files["plaintext"]
):
if content_config.plaintext == None:
logger.info("📄 No configuration for plaintext notes. Using default configuration.")
default_configuration = default_config["content-type"]["plaintext"] # type: ignore
content_config.plaintext = TextContentConfig(
compressed_jsonl=default_configuration["compressed-jsonl"],
embeddings_file=default_configuration["embeddings-file"],
)
logger.info("📄 Setting up search for plaintext")
# Extract Entries, Generate Plaintext Embeddings
content_index.plaintext = text_search.setup(
@ -172,8 +270,13 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup plaintext: {e}", exc_info=True)
try:
# Initialize Image Search
if (t == None or t == state.SearchType.Image.value) and content_config.image and search_models.image_search:
logger.info("🌄 Setting up search for images")
@ -182,6 +285,10 @@ def configure_content(
content_config.image, search_models.image_search.image_encoder, regenerate=regenerate
)
except Exception as e:
logger.error(f"🚨 Failed to setup images: {e}", exc_info=True)
try:
if (t == None or t == state.SearchType.Github.value) and content_config.github and search_models.text_search:
logger.info("🐙 Setting up search for github")
# Extract Entries, Generate Github Embeddings
@ -192,8 +299,13 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True)
try:
# Initialize Notion Search
if (t == None or t in state.SearchType.Notion.value) and content_config.notion and search_models.text_search:
logger.info("🔌 Setting up search for notion")
@ -204,8 +316,13 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True)
try:
# Initialize External Plugin Search
if (t == None or t in state.SearchType) and content_config.plugins and search_models.text_search:
logger.info("🔌 Setting up search for plugins")
@ -218,11 +335,11 @@ def configure_content(
search_models.text_search.bi_encoder,
regenerate=regenerate,
filters=[DateFilter(), WordFilter(), FileFilter()],
full_corpus=full_corpus,
)
except Exception as e:
logger.error(f"🚨 Failed to setup search: {e}", exc_info=True)
raise e
logger.error(f"🚨 Failed to setup Plugin: {e}", exc_info=True)
# Invalidate Query Cache
state.query_cache = LRU()

View file

@ -192,19 +192,24 @@ def setup(
regenerate: bool,
filters: List[BaseFilter] = [],
normalize: bool = True,
full_corpus: bool = True,
) -> TextContent:
# Map notes in text files to (compressed) JSONL formatted file
config.compressed_jsonl = resolve_absolute_path(config.compressed_jsonl)
previous_entries = []
if config.compressed_jsonl.exists() and not regenerate:
previous_entries = extract_entries(config.compressed_jsonl)
entries_with_indices = text_to_jsonl(config).process(previous_entries=previous_entries, files=files)
entries_with_indices = text_to_jsonl(config).process(
previous_entries=previous_entries, files=files, full_corpus=full_corpus
)
# Extract Updated Entries
entries = extract_entries(config.compressed_jsonl)
if is_none_or_empty(entries):
config_params = ", ".join([f"{key}={value}" for key, value in config.dict().items()])
raise ValueError(f"No valid entries found in specified files: {config_params}")
raise ValueError(
f"No valid entries found in specified configuration: {config_params}, with files: {files.keys()}"
)
# Compute or Load Embeddings
config.embeddings_file = resolve_absolute_path(config.embeddings_file)

View file

@ -1,5 +1,6 @@
import logging
import glob
import base64
from typing import Optional
from bs4 import BeautifulSoup
@ -209,7 +210,7 @@ def get_pdf_files(config: TextContentConfig):
for file in all_pdf_files:
with open(file, "rb") as f:
try:
filename_to_content_map[file] = f.read()
filename_to_content_map[file] = base64.b64encode(f.read()).decode("utf-8")
except Exception as e:
logger.warning(f"Unable to read file: {file} as PDF. Skipping file.")
logger.warning(e, exc_info=True)

View file

@ -66,6 +66,22 @@ def merge_dicts(priority_dict: dict, default_dict: dict):
return merged_dict
def get_file_type(filepath: str) -> str:
"Get file type from file path"
file_type = Path(filepath).suffix[1:]
if file_type in ["md", "markdown"]:
return "markdown"
elif file_type in ["org", "orgmode"]:
return "org"
elif file_type in ["txt", "text", "html", "xml", "htm", "rst"]:
return "plaintext"
elif file_type in ["pdf"]:
return "pdf"
return file_type
def load_model(
model_name: str, model_type, model_dir=None, device: str = None
) -> Union[BaseEncoder, SentenceTransformer, CrossEncoder]:

View file

@ -32,14 +32,6 @@ class TextContentConfig(TextConfigBase):
input_filter: Optional[List[str]]
index_heading_entries: Optional[bool] = False
@validator("input_filter")
def input_filter_or_files_required(cls, input_filter, values, **kwargs):
if is_none_or_empty(input_filter) and ("input_files" not in values or values["input_files"] is None):
raise ValueError(
"Either input_filter or input_files required in all content-type.<text_search> section of Khoj config file"
)
return input_filter
class GithubRepoConfig(ConfigBase):
name: str
@ -63,16 +55,6 @@ class ImageContentConfig(ConfigBase):
use_xmp_metadata: bool
batch_size: int
@validator("input_filter")
def input_filter_or_directories_required(cls, input_filter, values, **kwargs):
if is_none_or_empty(input_filter) and (
"input_directories" not in values or values["input_directories"] is None
):
raise ValueError(
"Either input_filter or input_directories required in all content-type.image section of Khoj config file"
)
return input_filter
class ContentConfig(ConfigBase):
org: Optional[TextContentConfig]

View file

@ -66,7 +66,7 @@ def test_index_batch(client):
headers = {"x-api-key": "secret"}
# Act
response = client.post("/indexer/batch", json=request_body, headers=headers)
response = client.post("/v1/indexer/batch", json=request_body, headers=headers)
# Assert
assert response.status_code == 200
@ -81,7 +81,7 @@ def test_regenerate_with_valid_content_type(client):
headers = {"x-api-key": "secret"}
# Act
response = client.post(f"/indexer/batch?search_type={content_type}", json=request_body, headers=headers)
response = client.post(f"/v1/indexer/batch?search_type={content_type}", json=request_body, headers=headers)
# Assert
assert response.status_code == 200, f"Returned status: {response.status_code} for content type: {content_type}"
@ -97,7 +97,7 @@ def test_regenerate_with_github_fails_without_pat(client):
headers = {"x-api-key": "secret"}
# Act
response = client.post(f"/indexer/batch?search_type=github", json=request_body, headers=headers)
response = client.post(f"/v1/indexer/batch?search_type=github", json=request_body, headers=headers)
# Assert
assert response.status_code == 200, f"Returned status: {response.status_code} for content type: github"

View file

@ -1,6 +1,7 @@
# Standard Packages
import json
import os
import base64
# Internal Packages
from khoj.processor.pdf.pdf_to_jsonl import PdfToJsonl
@ -15,7 +16,7 @@ def test_single_page_pdf_to_jsonl():
# Extract Entries from specified Pdf files
# Read singlepage.pdf into memory as bytes
with open("tests/data/pdf/singlepage.pdf", "rb") as f:
pdf_bytes = f.read()
pdf_bytes = base64.b64encode(f.read()).decode("utf-8")
data = {"tests/data/pdf/singlepage.pdf": pdf_bytes}
entries, entry_to_file_map = PdfToJsonl.extract_pdf_entries(pdf_files=data)
@ -35,7 +36,7 @@ def test_multi_page_pdf_to_jsonl():
# Act
# Extract Entries from specified Pdf files
with open("tests/data/pdf/multipage.pdf", "rb") as f:
pdf_bytes = f.read()
pdf_bytes = base64.b64encode(f.read()).decode("utf-8")
data = {"tests/data/pdf/multipage.pdf": pdf_bytes}
entries, entry_to_file_map = PdfToJsonl.extract_pdf_entries(pdf_files=data)

View file

@ -6,18 +6,6 @@ from khoj.utils.rawconfig import TextContentConfig, ImageContentConfig
# Test
# ----------------------------------------------------------------------------------------------------
def test_input_file_or_filter_required_in_text_content_config():
# Act
with pytest.raises(ValueError):
TextContentConfig(
input_files=None,
input_filter=None,
compressed_jsonl="notes.jsonl",
embeddings_file="note_embeddings.pt",
)
# ----------------------------------------------------------------------------------------------------
def test_input_filter_or_directories_required_in_image_content_config():
# Act

View file

@ -131,6 +131,65 @@ def test_entry_chunking_by_max_tokens(org_config_with_only_new_file: TextContent
assert len(initial_notes_model.corpus_embeddings) == 2
# ----------------------------------------------------------------------------------------------------
# @pytest.mark.skip(reason="Flaky due to compressed_jsonl file being rewritten by other tests")
def test_entry_chunking_by_max_tokens_not_full_corpus(
org_config_with_only_new_file: TextContentConfig, search_models: SearchModels
):
# Arrange
# Insert org-mode entry with size exceeding max token limit to new org file
data = {
"readme.org": """
* Khoj
/Allow natural language search on user content like notes, images using transformer based models/
All data is processed locally. User can interface with khoj app via [[./interface/emacs/khoj.el][Emacs]], API or Commandline
** Dependencies
- Python3
- [[https://docs.conda.io/en/latest/miniconda.html#latest-miniconda-installer-links][Miniconda]]
** Install
#+begin_src shell
git clone https://github.com/khoj-ai/khoj && cd khoj
conda env create -f environment.yml
conda activate khoj
#+end_src"""
}
text_search.setup(
OrgToJsonl,
data,
org_config_with_only_new_file,
search_models.text_search.bi_encoder,
regenerate=False,
)
max_tokens = 256
new_file_to_index = Path(org_config_with_only_new_file.input_files[0])
with open(new_file_to_index, "w") as f:
f.write(f"* Entry more than {max_tokens} words\n")
for index in range(max_tokens + 1):
f.write(f"{index} ")
data = get_org_files(org_config_with_only_new_file)
# Act
# reload embeddings, entries, notes model after adding new org-mode file
initial_notes_model = text_search.setup(
OrgToJsonl,
data,
org_config_with_only_new_file,
search_models.text_search.bi_encoder,
regenerate=False,
full_corpus=False,
)
# Assert
# verify newly added org-mode entry is split by max tokens
assert len(initial_notes_model.entries) == 5
assert len(initial_notes_model.corpus_embeddings) == 5
# ----------------------------------------------------------------------------------------------------
def test_regenerate_index_with_new_entry(
content_config: ContentConfig, search_models: SearchModels, new_org_file: Path