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
3
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
29
src/interface/desktop/README.md
Normal 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)"`.
|
693
src/interface/desktop/assets/icons/chat.svg
Normal file
|
@ -0,0 +1,693 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
width="299.99649"
|
||||
height="225.92412"
|
||||
viewBox="0 0 85.704 64.542"
|
||||
id="Layer_1"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="Speech_bubble(1).svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview3800"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0445985"
|
||||
inkscape:cx="150.77563"
|
||||
inkscape:cy="112.96206"
|
||||
inkscape:window-width="1309"
|
||||
inkscape:window-height="456"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><defs
|
||||
id="defs8" />
|
||||
<g
|
||||
transform="matrix(0.9486962,0,0,0.9486962,2.4834364,1.8361818)"
|
||||
id="g3">
|
||||
<path
|
||||
d="M 45.673,0 C 67.781,0 85.703,12.475 85.703,27.862 C 85.703,43.249 67.781,55.724 45.673,55.724 C 38.742,55.724 32.224,54.497 26.539,52.34 C 15.319,56.564 0,64.542 0,64.542 C 0,64.542 9.989,58.887 14.107,52.021 C 15.159,50.266 15.775,48.426 16.128,46.659 C 9.618,41.704 5.643,35.106 5.643,27.862 C 5.643,12.475 23.565,0 45.673,0 M 45.673,2.22 C 24.824,2.22 7.862,13.723 7.862,27.863 C 7.862,34.129 11.275,40.177 17.472,44.893 L 18.576,45.734 L 18.305,47.094 C 17.86,49.324 17.088,51.366 16.011,53.163 C 15.67,53.73 15.294,54.29 14.891,54.837 C 18.516,53.191 22.312,51.561 25.757,50.264 L 26.542,49.968 L 27.327,50.266 C 32.911,52.385 39.255,53.505 45.673,53.505 C 66.522,53.505 83.484,42.002 83.484,27.862 C 83.484,13.722 66.522,2.22 45.673,2.22 L 45.673,2.22 z "
|
||||
id="path5" />
|
||||
</g>
|
||||
<image
|
||||
width="75.991768"
|
||||
height="49.994583"
|
||||
preserveAspectRatio="none"
|
||||
xlink:href="data:image/png;base64,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
|
||||
RU5ErkJggg==
|
||||
"
|
||||
id="image3810"
|
||||
x="9.712183"
|
||||
y="3.7505856" /></svg>
|
After Width: | Height: | Size: 50 KiB |
5
src/interface/desktop/assets/icons/circular-add.svg
Normal 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 |
1
src/interface/desktop/assets/icons/confirm-icon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><defs><style>.cls-1{fill:#00a912;}.cls-1,.cls-2{fill-rule:evenodd;}.cls-2{fill:#fff;}</style></defs><title>confirm</title><path class="cls-1" d="M61.44,0A61.44,61.44,0,1,1,0,61.44,61.44,61.44,0,0,1,61.44,0Z"/><path class="cls-2" d="M42.37,51.68,53.26,62,79,35.87c2.13-2.16,3.47-3.9,6.1-1.19l8.53,8.74c2.8,2.77,2.66,4.4,0,7L58.14,85.34c-5.58,5.46-4.61,5.79-10.26.19L28,65.77c-1.18-1.28-1.05-2.57.24-3.84l9.9-10.27c1.5-1.58,2.7-1.44,4.22,0Z"/></svg>
|
After Width: | Height: | Size: 549 B |
BIN
src/interface/desktop/assets/icons/favicon-128x128.ico
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
src/interface/desktop/assets/icons/favicon-128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/interface/desktop/assets/icons/favicon.icns
Normal file
28
src/interface/desktop/assets/icons/folder.svg
Normal 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 |
1
src/interface/desktop/assets/icons/github.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
After Width: | Height: | Size: 964 B |
BIN
src/interface/desktop/assets/icons/khoj-logo-sideways-200.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/interface/desktop/assets/icons/khoj-logo-sideways-500.png
Normal file
After Width: | Height: | Size: 36 KiB |
5385
src/interface/desktop/assets/icons/khoj-logo-sideways.svg
Normal file
After Width: | Height: | Size: 1.2 MiB |
5
src/interface/desktop/assets/icons/link.svg
Normal 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 |
1
src/interface/desktop/assets/icons/logotype.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 942.19 656.92"><defs><style>.cls-1{fill:#1e1d1e;}</style></defs><title>logotype</title><path class="cls-1" d="M122.48,234.63c7.25,7.25,3.29,38.21,3.29,57.32,0,11.86,2,21.74,2,31,0,9.88-2,19.76-2,29,2,7.9,0,36.89,4,38.87,5.93,4,11.86-15.81,13.83-17.79,5.93-13.18,2-5.27,9.23-21.08,7.9-15.16,15.81-31,21.74-42.17,5.27-11.86,13.18-29,19.11-42.83,5.93-9.22,9.88-27,19.11-34.26,5.92-5.93,25-5.93,42.82-5.93,13.18,2,27,2,31,9.22,5.27,11.86-15.82,42.83-21.74,56-7.25,15.81-13.18,25.7-21.09,38.87-2,4-2,7.91-4,13.84-7.25,19.11-17.13,32.28-27,52-4,7.25-7.25,17.13-13.18,25-4,11.2-9.89,21.08-9.89,29,0,11.2,7.91,27,11.86,34.26,13.18,25.7,27,50.73,40.19,77.75,2,5.27,2,11.2,5.93,17.13,5.93,15.15,13.84,27,23.06,46.12,5.94,13.84,21.09,38.87,17.79,50.07-5.93,13.84-21.74,9.88-38.87,9.88-21.08,0-36.89,0-44.14-4-7.91-5.93-13.84-27-19.77-38.21-13.18-31-19.11-48.76-34.26-81a152.72,152.72,0,0,0-15.81-33c-7.91-9.88-7.91,21.09-9.89,34.92v38.22c0,25,5.93,67.86-5.27,79.06-11.86,9.89-67.86,7.91-75.77-5.93-2-7.24,0-30.31,0-50.07,2-19.11,0-31,0-48.1-2-42.17-2-81-2-123.21,0-15.81,4-31,2-42.83,0-7.9-2-13.17-2-21.08,0-5.93,2-9.88,2-15.81,0-13.18-2-27-2-40.19,0-13.84,2-27,2-38.88,0-17.13-5.27-38.21-2-52,2-5.93,7.91-11.2,9.89-13.18,2,0,2,0,4-2C71.74,222.77,114.57,224.74,122.48,234.63Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M547.58,232.65c3.95,2,3.95,9.23,5.93,17.13,2,15.16,0,46.12,0,63.91-2,29,0,59.3,0,81V523.87c0,34.92,0,83-2,123.87,0,17.13,2,30.31-5.93,36.24-9.22,5.93-63.91,5.93-71.16,0-7.9-7.91-3.95-33-3.95-50.08V610.84c0-36.89-2-71.81,0-114,0-9.88,2-21.08,0-25-5.93-5.93-21.08-2-32.94-2-7.91,0-25-5.93-32.95,2-7.25,9.88-4,36.9-4,54,0,42.17,2,63.91,2,108,0,23.07,2,42.17-9.23,50.08-11.86,7.91-58,5.93-65.89-2-11.86-11.2-9.88-56-9.88-83,0-83,3.95-144.29,2-218.08,0-15.16,2-31,2-42.17,0-7.91-2-15.81-3.95-27,0-5.93,2-13.84,2-19.77,2-21.08-2-48.1,2-56,7.91-11.2,67.87-13.17,77.09-1.31,7.91,11.2,2,34.26,2,48.09-2,15.16,0,34.93,0,56v27c0,9.23,0,23.06,2,27,3.95,5.93,23.71,5.93,32.94,5.93,7.9,0,29,2,32.94-4s4-38.87,4-50.07V264.94c0-11.2-2-23.06,2-30.31,3.95-7.91,13.18-5.93,21.08-7.91C501.46,226.72,539.67,222.77,547.58,232.65Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M716,218.81c31-2,61.93,7.91,81,17.13,5.93,4,19.11,17.8,23.06,23.72,9.88,13.18,15.81,23.06,19.11,40.2,2,3.95,3.95,5.93,3.95,7.9v7.91c2,9.22,5.93,19.11,7.91,25v21.08c0,5.93,2,11.2,2,17.13,4,34.92,2,75.77,0,114,0,25,0,50.07-3.95,73.13-4,23.72-9.88,48.76-15.15,61.94-2,3.95-7.91,13.83-11.86,17.78l-2,2c-5.93,9.23-17.13,17.13-29,25-5.93,3.29-7.91,5.27-13.18,7.25-5.93,2-11.86,2-21.74,4-5.27,0-11.2,3.95-17.13,3.95-13.18,2-31,2-40.19,2-11.86-2-25.7-5.93-36.9-11.86-2,0-4-3.29-4-3.29a107,107,0,0,1-17.14-7.91c-7.9-5.93-17.78-17.79-23.71-25-13.18-23.06-23.07-58-25-90.92-2-25-2-58-2-79.07-2-15.81,0-32.94,2-48.1,0-21.74,0-44.8,2-63.91,2-9.88,4-19.1,5.94-27,0-11.86,0-21.08,2-31,5.93-17.13,19.11-46.12,33-58,3.95-2,9.22-5.93,15.15-9.22,15.81-7.91,38.87-13.84,63.91-15.82ZM677.11,348c-5.93,48.76-4,100.81-2,150.88,2,38.88,4,98.18,21.74,112,0,2,5.27,4,9.22,5.93,15.82,5.28,29,2,40.86-5.93,3.95-4,9.22-9.88,11.19-13.83,5.93-9.23,9.89-36.24,11.86-54,0-15.15,0-34.26,2-56,0-42.17-2-85-5.93-121.24-2-25-3.95-48.09-11.86-59.95C748.93,297.88,741,292,733.12,290c-9.23-3.29-13.18,0-17.13,0h-2C689,293.93,681.07,319,677.11,348Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M975.89,226.44c9.23,9.23,7.91,234.41,7.91,263.4-2,63.25-2,108.05-2,175.26,0,11.86,2,17.79,2,25.69,0,15.16-3.95,38.22-2,61.28,0,23.06,2,51.24-4,64.42-4,17.13-19.77,36.89-34.92,46.12-15.15,9.88-40.85,13.84-71.16,11.86-19.76-2-42.82-4-50.73-15.82-2-5.27-2-21.08-2-32.28,0-15.81,0-27,5.93-31,11.86-5.93,36.9,13.18,56,7.91,7.9-2,15.81-9.88,19.76-23.72,1.32-11.2,0-27.52,0-40.7,0-11.86,1.32-21.08,1.32-32.94V634.79c0-29-3.29-56-3.29-81.7,0-36.24,2-253.51,2-286.46,0-23.06-7.91-38.21,9.22-46.12C923.84,216.56,966,216.56,975.89,226.44Z" transform="translate(-41.67 -218.04)"/></svg>
|
After Width: | Height: | Size: 3.9 KiB |
1
src/interface/desktop/assets/icons/markdown.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg>
|
After Width: | Height: | Size: 283 B |
4
src/interface/desktop/assets/icons/notion.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
src/interface/desktop/assets/icons/openai-logomark.svg
Normal 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 |
1
src/interface/desktop/assets/icons/org.svg
Normal file
After Width: | Height: | Size: 7.8 KiB |
23
src/interface/desktop/assets/icons/pdf.svg
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 334.371 380.563" version="1.1" viewBox="0 0 14 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
|
||||
<polygon points="51.791 356.65 51.791 23.99 204.5 23.99 282.65 102.07 282.65 356.65" fill="#fff" stroke-width="212.65"/>
|
||||
<path d="m201.19 31.99 73.46 73.393v243.26h-214.86v-316.66h141.4m6.623-16h-164.02v348.66h246.85v-265.9z" stroke-width="21.791"/>
|
||||
</g>
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
|
||||
<polygon points="282.65 356.65 51.791 356.65 51.791 23.99 204.5 23.99 206.31 25.8 206.31 100.33 280.9 100.33 282.65 102.07" fill="#fff" stroke-width="212.65"/>
|
||||
<path d="m198.31 31.99v76.337h76.337v240.32h-214.86v-316.66h138.52m9.5-16h-164.02v348.66h246.85v-265.9l-6.43-6.424h-69.907v-69.842z" stroke-width="21.791"/>
|
||||
</g>
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" stroke-width="21.791">
|
||||
<polygon points="258.31 87.75 219.64 87.75 219.64 48.667 258.31 86.38"/>
|
||||
<path d="m227.64 67.646 12.41 12.104h-12.41v-12.104m-5.002-27.229h-10.998v55.333h54.666v-12.742z"/>
|
||||
</g>
|
||||
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" fill="#ed1c24" stroke-width="212.65">
|
||||
<polygon points="311.89 284.49 22.544 284.49 22.544 167.68 37.291 152.94 37.291 171.49 297.15 171.49 297.15 152.94 311.89 167.68"/>
|
||||
<path d="m303.65 168.63 1.747 1.747v107.62h-276.35v-107.62l1.747-1.747v9.362h272.85v-9.362m-12.999-31.385v27.747h-246.86v-27.747l-27.747 27.747v126h302.35v-126z"/>
|
||||
</g>
|
||||
<rect x="1.7219" y="7.9544" width="10.684" height="4.0307" fill="none"/>
|
||||
<g transform="matrix(.04589 0 0 .04589 1.7219 11.733)" fill="#fff" stroke-width="21.791"><path d="m9.216 0v-83.2h30.464q6.784 0 12.928 1.408 6.144 1.28 10.752 4.608 4.608 3.2 7.296 8.576 2.816 5.248 2.816 13.056 0 7.68-2.816 13.184-2.688 5.504-7.296 9.088-4.608 3.456-10.624 5.248-6.016 1.664-12.544 1.664h-8.96v26.368zm22.016-43.776h7.936q6.528 0 9.6-3.072 3.2-3.072 3.2-8.704t-3.456-7.936-9.856-2.304h-7.424z"/><path d="m87.04 0v-83.2h24.576q9.472 0 17.28 2.304 7.936 2.304 13.568 7.296t8.704 12.8q3.2 7.808 3.2 18.816t-3.072 18.944-8.704 13.056q-5.504 5.12-13.184 7.552-7.552 2.432-16.512 2.432zm22.016-17.664h1.28q4.48 0 8.448-1.024 3.968-1.152 6.784-3.84 2.944-2.688 4.608-7.424t1.664-12.032-1.664-11.904-4.608-7.168q-2.816-2.56-6.784-3.456-3.968-1.024-8.448-1.024h-1.28z"/><path d="m169.22 0v-83.2h54.272v18.432h-32.256v15.872h27.648v18.432h-27.648v30.464z"/></g>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
1
src/interface/desktop/assets/icons/plaintext.svg
Normal 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 |
|
@ -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 |
116
src/interface/desktop/assets/khoj.css
Normal 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;
|
||||
}
|
||||
}
|
8476
src/interface/desktop/assets/markdown-it.min.js
vendored
Normal file
1823
src/interface/desktop/assets/org.min.js
vendored
Normal file
5
src/interface/desktop/assets/pico.min.css
vendored
Normal file
559
src/interface/desktop/chat.html
Normal 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('"', '"');
|
||||
|
||||
// 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>
|
342
src/interface/desktop/config.html
Normal 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>
|
499
src/interface/desktop/index.html
Normal 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>
|
359
src/interface/desktop/main.js
Normal 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()
|
||||
})
|
25
src/interface/desktop/package.json
Normal 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"
|
||||
}
|
||||
}
|
49
src/interface/desktop/preload.js
Normal 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')
|
||||
})
|
177
src/interface/desktop/renderer.js
Normal 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();
|
||||
});
|
6
src/interface/desktop/todesktop.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"id": "",
|
||||
"icon": "./assets/icons/favicon-128x128.png",
|
||||
"appPath": ".",
|
||||
"schemaVersion": 1
|
||||
}
|
1205
src/interface/desktop/yarn.lock
Normal 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)
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|