Compare commits

...

475 Commits

Author SHA1 Message Date
sabaimran
387b7c7887 Release Khoj version 1.21.3 2024-08-23 11:15:15 -07:00
sabaimran
7b8b3a66ae Revert django version to previous patch 2024-08-23 11:12:41 -07:00
Debanjum Singh Solanky
5927ca8032 Properly close chat stream iterator even if response generation fails
Previously chat stream iterator wasn't closed when response streaming
for offline chat model threw an exception.

This would require restarting the application. Now application doesn't
hang even if current response generation fails with exception
2024-08-23 02:06:26 -07:00
Debanjum Singh Solanky
bdb81260ac Update docs to mention using Llama 3.1 and 20K max prompt size for it
Update stale credits to better reflect bigger open source dependencies
2024-08-22 20:27:58 -07:00
Debanjum Singh Solanky
238bc11a50 Fix, improve openai chat actor, director tests & online search prompt 2024-08-22 19:09:33 -07:00
Debanjum Singh Solanky
9986c183ea Default to gpt-4o-mini instead of gpt-3.5-turbo in tests, func args
GPT-4o-mini is cheaper, smarter and can hold more context than
GPT-3.5-turbo. In production, we also default to gpt-4o-mini, so makes
sense to upgrade defaults and tests to work with it
2024-08-22 19:04:49 -07:00
Debanjum Singh Solanky
8a4c20d59a Enforce json response by offline models when requested by chat actors
- Background
  Llama.cpp allows enforcing response as json object similar to OpenAI
  API. Pass expected response format to offline chat models as well.

- Overview
  Enforce json output to improve intermediate step performance by
  offline chat models. This is especially helpful when working with
  smaller models like Phi-3.5-mini and Gemma-2 2B, that do not
  consistently respond with structured output, even when requested

- Details
  Enforce json response by extract questions, infer output offline
  chat actors
  - Convert prompts to output json objects when offline chat models
    extract document search questions or infer output mode
  - Make llama.cpp enforce response as json object

- Result
  - Improve all intermediate steps by offline chat actors via json
    response enforcement
  - Avoid the manual, ad-hoc and flaky output schema enforcement and
    simplify the code
2024-08-22 18:07:44 -07:00
Debanjum Singh Solanky
ab7fb5117c Release Khoj version 1.21.2 2024-08-20 12:38:54 -07:00
Debanjum Singh Solanky
de24ffcf0d Upgrade Axios, a desktop app dependency, to version 1.7.4 2024-08-20 12:32:36 -07:00
Debanjum Singh Solanky
a60baa55fb Upgrade Django, a Khoj server dependency, to version 5.0.8 2024-08-20 12:32:00 -07:00
sabaimran
1ac8de6c3a Release Khoj version 1.21.1 2024-08-20 11:55:34 -07:00
Debanjum Singh Solanky
5d59acd1f4 Stop pushing deprecated khoj-assistant package to pypi
- Also skip uploading package version to it already exists on pypi
  This happens when a release is new khoj tagged release is created
2024-08-20 11:43:02 -07:00
sabaimran
f6ce2fd432 Handle end of chunk logic in openai stream processor 2024-08-20 10:50:09 -07:00
sabaimran
029775420c Release Khoj version 1.21.0 2024-08-20 10:01:56 -07:00
sabaimran
4808ce778a Merge pull request #892 from khoj-ai/upgrade-offline-chat-models-support
Upgrade offline chat model support. Default to Llama 3.1
2024-08-20 11:51:20 -05:00
Debanjum Singh Solanky
58c8068079 Upgrade default offline chat model to llama 3.1 2024-08-20 09:28:56 -07:00
sabaimran
2d9dd81e76 Re-add authenticated decorator to api_chat.py /chat endpoint 2024-08-19 05:37:18 -05:00
sabaimran
2c5350329a Remove the hashes from titles in found relevant notes 2024-08-18 22:31:15 -05:00
Debanjum Singh Solanky
acdc3f9470 Unwrap any json in md code block, when parsing chat actor responses
This is a more robust way to extract json output requested from
gemma-2 (2B, 9B) models which tend to return json in md codeblocks.

Other models should remain unaffected by this change.

Also removed request to not wrap json in codeblocks from prompts. As
code is doing the unwrapping automatically now, when present
2024-08-16 14:16:29 -05:00
Debanjum Singh Solanky
ca45fce8ac Break long links in train of thought to stay within chat page width 2024-08-16 14:16:29 -05:00
sabaimran
c0316a6b5d Enable free tier users to have unlimited chats with the default chat model (#886)
- Allow free tier users to have unlimited chats with default chat model. It'll only be rate-limited and at the same rate as subscribed users
- In the server chat settings, replace the concept of default/summarizer models with default/advanced chat models. Use the advanced models as a default for subscribed users.
- For each `ChatModelOption' configuration, allow the admin to specify a separate value of `max_tokens' for subscribed users. This allows server admins to configure different max token limits for unsubscribed and subscribed users
- Show error message in web app when hit rate limit or other server errors
2024-08-16 12:14:44 -07:00
Debanjum
8dad9362e7 Improve search model config display for admin (#887) from aam-at/feature/improve_search_model_config_admin
Currently, the search model config display for admins only shows the id of the search model config, which is not very informative. 

The changes enhances the admin console by displaying the name of the search model config (name), as well as the bi-encoder model (bi_encoder) and cross-encoder model (cross_encoder) along the id.
2024-08-16 07:33:55 -07:00
Debanjum
2b1482d2b4 Fix indexing content from Emacs #883 from aam-at/bugfix/fix_emacs_if
Previously `force' was passed as a query param to the single indexing API. After the recent API updates, it is meant to select the API method to use (PATCH vs PATCH). Converting `force' argument to a bool fixes implementing this new behavior
2024-08-16 07:32:46 -07:00
Debanjum
0b568e204e Add model_config for cross-encoder model (#885) from aam-at/feature/crossencoder_model_config
Add `model_config' for the cross-encoder model, so the server admin can use models which require the `trust_remote_code' argument to run locally
2024-08-16 07:32:19 -07:00
Debanjum
39e566ba91 Improve Document, Online Search to Answer Vague or Meta Questions (#870)
- Major
  - Improve doc search actor performance on vague, random or meta questions
  - Pass user's name to document and online search actors prompts

- Minor
  - Fix and improve openai chat actor tests
  - Remove unused max tokns arg to extract qs func of doc search actor
2024-08-16 06:46:13 -07:00
Debanjum Singh Solanky
27ad9b1302 Remove unused max tokns arg to extract qs func of doc search actor 2024-08-13 12:53:39 +05:30
Debanjum Singh Solanky
f75606d7f5 Improve doc search actor performance on vague, random or meta questions
- Issue
  Previously the doc search actor wouldn't extract good search queries
  to run on user's documents for broad, vague questions.

- Fix
  The updated extract questions prompt shows and tells the doc search
  actor on how to deal with such questions

  The doc search actor's temperature was also increased to support more
  creative/random questions. The previous temp of 0 was meant to
  encourage structured json output. But now with json mode, a low temp is
  not necessary to get json output
2024-08-13 12:53:39 +05:30
Debanjum Singh Solanky
3675938df6 Support passing temperature to offline chat model chat actors
- Use temperature of 0 by default for extract questions offline chat
  actor
- Use temperature of 0.2 for send_message_to_model_offline (this is
  the default temperature set by llama.cpp)
2024-08-13 12:53:00 +05:30
Shantanu Sakpal
b5bcce7f85 Cycle through chat history in chat input on Obsidian (#861)
* Add ability to cycle through the chat history in the chat input on Obsidian (similar to terminal history navigation)
* Add mod key shortcut to cycle through chat history in chat input
* Add shortcut help text in chat input placeholder

---------

Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
2024-08-12 23:55:25 -07:00
srikary12
05c0aa3882 Support exclusion file filters (#826)
### Overview
Support exclude file filter in user search queries

### Details
- All of the exclude file filter terms need to be satisfied
- Any one of the include file filter terms should be satisfied

### Example
- **Search Query**: *what happened yesterday? -file:"tasks.org" -file:"work.md" file:"diary.org" file:"journal.org*
- **Behavior**: Query will try find relevant notes in any of `journal.org` or `diary.org` and not in `tasks.org` and not in `work.md`

### Details
* Add support for exclusion file filters
* Translate file filter to valid Django DB entry filter regex
* Exclude all files when multiple exclude file filter in query

Previously we were applying an "Or" filter, which would exclude any
file mentioned in a query with multiple exclude file filter.

This is not what we naturally mean when we ask excluding a file in a query

* Rename, rearrange, deduplicate and add file filter tests

Closes #728
---------

Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
2024-08-12 05:41:54 -07:00
Alexander Matyasko
2d9bf14ecb Improve search model config display for admin 2024-08-11 19:13:25 +08:00
Debanjum Singh Solanky
7815e02dd4 Release Khoj version 1.20.4 2024-08-11 16:00:13 +05:30
Debanjum Singh Solanky
d951e36945 Update khoj.el package description, it had gone stale 2024-08-11 15:52:46 +05:30
Debanjum Singh Solanky
16b31c3e35 Refresh automation data shown by edit automation card after update
Previously required the automation page to be refreshed to see updates
to the automation in the edit automation card. This would be seen when
user tries to edit an automation multiple times (without a page refresh)
2024-08-11 15:52:46 +05:30
Debanjum Singh Solanky
f2f37ae444 Fix creating, editing automations that start weekly on Sunday 2024-08-11 15:52:46 +05:30
Debanjum Singh Solanky
ec9add9a51 Fix automation edit cards height. Scroll when card longer than screen 2024-08-11 15:52:46 +05:30
sabaimran
d99f03e4f3 If the list of choices in a chunk is empty, continue in openai response 2024-08-11 15:30:09 +05:30
Alexander Matyasko
f16b0f628b Fix true/false evaluation in Emacs to prevent unintended index re-indexing
Previously, the code incorrectly treated all non-nil values as true, leading to
the index being re-indexed with the force flag whenever the user selected to
update the index.
2024-08-11 17:24:11 +08:00
Alexander Matyasko
0e9e9648e6 Fix emacs if syntax 2024-08-11 17:24:11 +08:00
sabaimran
6f94a076f7 Add conversation_id parameter to the create_automation method 2024-08-11 10:45:13 +05:30
sabaimran
acb825f4f5 Bug fixes for automations
- Pass the new conversation id as kwarg for the scheduled_chat function
- For edit automations, re-use the original conversation id
- Parse images correctly for image automations
2024-08-11 10:41:43 +05:30
Debanjum Singh Solanky
5075d13902 Give visual feedback when interact with chat message feedback buttons
- Use color to provide visual feedback when hover, click on feedback
  buttons
- Use color to provide visual feedback when hover on speech, copy
  buttons click
- Add cooldown period before being able to send feedback on that message again.
  Avoids inadvertent multiple consecutive clicks on feedback buttons
2024-08-10 20:09:52 +05:30
Debanjum Singh Solanky
b3c6c8c84b Add OpenGraph metadata to web app pages for improve social share links 2024-08-10 18:14:05 +05:30
Debanjum Singh Solanky
fc411091c8 Add apple favicon, load favicons for each web app page from assets folder 2024-08-10 18:14:05 +05:30
Debanjum Singh Solanky
a7623e64fa Move Khoj webmanifest, assets to new web app public directory 2024-08-10 18:14:04 +05:30
sabaimran
af1d4b9ba4 Remove the premium requirement for speech for now 2024-08-10 14:10:12 +05:30
sabaimran
1d581464e6 Filter out any undefined agents when rendering the home page 2024-08-10 13:33:55 +05:30
sabaimran
acf1c14122 Release Khoj version 1.20.3 2024-08-09 18:11:11 +05:30
sabaimran
7d3a25f8c0 Handle processing case for the schedule leader process lock when it's empty 2024-08-09 16:37:06 +05:30
sabaimran
faf3584acd Fix automations edit button 2024-08-09 14:21:11 +05:30
sabaimran
5ef198a5b2 Improve default background color styling for inputs 2024-08-08 18:08:05 +05:30
sabaimran
c08b9e89f0 Update test_db_lock with new function name 2024-08-08 13:03:01 +05:30
sabaimran
64b2073e63 In the time-based job for managing the schedule leader, and logic to create a new lock when the current one is expired. 2024-08-08 12:42:59 +05:30
sabaimran
7ee0d9067d Fix apostrophe issue in copy text when commandempty in settings page 2024-08-08 11:41:10 +05:30
sabaimran
f28693c8c7 create a useismobilewidth method for standardized mobile view detection. 2024-08-07 21:04:44 +05:30
sabaimran
2943bed5d4 Update category colors 2024-08-07 18:51:31 +05:30
sabaimran
37afa3411f Improve the file upload experience in the settings page 2024-08-07 18:51:20 +05:30
sabaimran
1ee21f5150 Add support for showing files outside of conversation view and linking people to manage files in settings 2024-08-07 18:50:53 +05:30
sabaimran
93f4ceabc1 Add drag/drop file upload support to the chat input area 2024-08-07 18:50:19 +05:30
sabaimran
370ebdee24 Standardized the mobile width calculation 2024-08-07 18:49:06 +05:30
sabaimran
52fed6023f Overlay the side panel on top of other content 2024-08-07 18:46:06 +05:30
Alexander Matyasko
823f8d58bb Add model_config for crossencoder model
Add model_config for crossencoder model, so the user can use models
which require trust_remote_code.
2024-08-07 18:00:12 +08:00
sabaimran
09b71846be Remove favicon.ico as it's interfering with favicon rendering in the home page 2024-08-07 11:53:25 +05:30
Debanjum Singh Solanky
167ef000f4 Fix chat API for non-streaming mode json response 2024-08-06 19:27:54 +05:30
sabaimran
00ee4c2697 Release Khoj version 1.20.2 2024-08-06 16:16:33 +05:30
sabaimran
d4a8ff0683 Support workflow dispatch events for running the pypi.yml job 2024-08-06 15:55:39 +05:30
sabaimran
ccccb8e7e6 Just ignore the static directory outputting by django's static collection 2024-08-06 15:51:54 +05:30
sabaimran
c4be3b43e5 Add the compiled folder to the list of directories to look through for static templates 2024-08-06 14:50:44 +05:30
sabaimran
265d2a79be Remove duplicate assets from being included in the pypi output 2024-08-06 13:51:37 +05:30
sabaimran
24d0fdb262 Fix directory referenceds in pypi.yml configuration for compiled folder 2024-08-06 13:38:34 +05:30
sabaimran
23b1b36f8c Fix directory referenceds in pypi.yml configuration for compiled folder 2024-08-06 13:31:42 +05:30
sabaimran
81c75e1024 Fix static file folder path for the pypi build
- Since the .gitignore will ignore any of the assets in the src/ folder when building the package wheel, we need to output the static assets to another folder just for the python pypi package. Use /compiled for this.
2024-08-06 13:24:26 +05:30
sabaimran
694f551625 Fix mkdir step when copying generated files 2024-08-06 10:17:56 +05:30
sabaimran
7607abc726 Release Khoj version 1.20.1 2024-08-06 10:05:41 +05:30
sabaimran
e9f9d92989 Try to manually copy the built files into where the src directory should be for the pypi build 2024-08-06 10:05:06 +05:30
Debanjum
c23688e2de Fixes and Improvements Post Spring UX Release (#880)
- Auto focus on email input on login screen for smoother login experience
- Use file icon associated with search page results. Improve search bar
- Show logged in user's email in nav menu for context
- Use previous icons with eyes for search, agents and automations items in nav menu
2024-08-05 14:32:31 -07:00
Debanjum Singh Solanky
a4388c5e65 Use custom Khoj Icons for Search, Agents & Automation in Nav Menu
- Update agents, automations, search svg icons
2024-08-06 02:55:29 +05:30
sabaimran
e9d6899fc2 Change the way the export is created for the pypi package in order to transfer static files out of the tmp shell 2024-08-05 22:46:54 +05:30
sabaimran
b17577c138 Fix configuration for default voice model settings 2024-08-05 19:57:21 +05:30
Debanjum Singh Solanky
ec106d743d Use file icon associated with search page results. Improve search bar 2024-08-05 19:24:39 +05:30
Debanjum Singh Solanky
4258392fc7 Auto focus on email input on login screen for smoother login experience 2024-08-05 19:24:16 +05:30
Debanjum Singh Solanky
020a956c89 Show user email address on settings menu for logged in account context 2024-08-05 19:19:47 +05:30
sabaimran
998d08f155 Fix logic for deletion to automatically re-render the side pane 2024-08-05 18:07:20 +05:30
sabaimran
20d95dc45e Add the favicon.ico file to the public directory of app.khoj.dev 2024-08-05 18:04:03 +05:30
sabaimran
1eab6c8590 Add additional icons for agents, pencil line and chalkboard 2024-08-05 17:23:29 +05:30
sabaimran
bafda233e2 Add standlone khoj_domain for allowed_hosts 2024-08-05 17:11:37 +05:30
Debanjum Singh Solanky
e412ed3bcb Release Khoj version 1.20.0 2024-08-05 16:25:21 +05:30
Debanjum Singh Solanky
9f785dbafe Format web app package.json using prettier 2024-08-05 16:23:31 +05:30
Debanjum Singh Solanky
7d3a208f8b Update bump version script to bump new next.js web app version too 2024-08-05 16:20:47 +05:30
sabaimran
2a63439b16 Merge pull request #879 from khoj-ai/features/migrate-to-spring-ui
Migrate all existing pages except login to the new spring ui
2024-08-05 03:45:02 -07:00
sabaimran
b7ed32f455 Merge branch 'master' of github.com:khoj-ai/khoj into features/migrate-to-spring-ui 2024-08-05 16:12:46 +05:30
sabaimran
7e6b611a19 Fix typo for Obsidian 2024-08-05 15:55:06 +05:30
sabaimran
34d54c75f7 Lint new changes again 2024-08-05 15:54:50 +05:30
Debanjum Singh Solanky
7cb14ff07a Add dev setup script. Run prettier on web app pre-commit 2024-08-05 15:49:31 +05:30
sabaimran
91047d1619 Use a png for the windows desktop icon 2024-08-05 15:29:30 +05:30
sabaimran
1151d14466 Add a separate windows object in the todesktop configuration 2024-08-05 14:27:56 +05:30
sabaimran
c56072aa7b Update todesktop runtime and use the icns file for the todesktop configuration 2024-08-05 14:19:38 +05:30
sabaimran
484b0aa96b Use the newer, simpler favicon across desktop and documentation. Update the macos icon set 2024-08-05 14:06:04 +05:30
sabaimran
1b35a3b16e Fix link to login in the nav menu 2024-08-05 12:32:19 +05:30
sabaimran
5a5bbe3852 Remove deprecate views, assets 2024-08-05 12:31:47 +05:30
sabaimran
c61b289bd1 Migrate all existing pages except login to the new spring ui 2024-08-05 12:17:56 +05:30
sabaimran
f835e330b8 Fix selection of icons, colors, add examples for personal finance 2024-08-05 12:08:18 +05:30
sabaimran
af6a70c9fb Fix fuschia spelling in the colorutils file as well 2024-08-05 11:51:45 +05:30
sabaimran
e0775446c9 fix spelling of fuschia :( 2024-08-05 11:50:11 +05:30
sabaimran
de1cd8c264 Clean up some of the suggestions code, improve randomness of cards' 2024-08-05 11:19:50 +05:30
sabaimran
37e261ff93 Show connected icon when files or notion is indexed 2024-08-05 10:33:18 +05:30
sabaimran
8bc28fb11d Merge pull request #878 from khoj-ai/features/big-upgrade-chat-ux
Spring UI: Modernize UX for normie development
2024-08-04 21:32:18 -07:00
sabaimran
22cfedcaff In the chat history side panel, order conversations by updated time 2024-08-05 09:48:00 +05:30
sabaimran
8220dc6115 Include the updated_at datetime when returning a conversation session 2024-08-05 09:47:13 +05:30
Debanjum Singh Solanky
e296d387e1 Clean duplicate title shown in reference snippets of hierarchical docs
Hierarchical documents like org-mode, markdown have their ancestry
shown in first line. Remove it to show cleaner, deduplicated reference
text from org-mode, markdown files
2024-08-05 04:59:06 +05:30
Debanjum Singh Solanky
95c2a52775 Show file icons in references for first party supported document types
Add org, markdown, pdf, word, icon and default file icons to simplify
identifying file type used as reference for generating chat response
2024-08-05 04:59:06 +05:30
Debanjum Singh Solanky
18a973b666 Fix name of Khoj logo component file in web app 2024-08-05 04:59:06 +05:30
Debanjum Singh Solanky
842036688d Format next.js web app with prettier 2024-08-05 04:59:06 +05:30
Debanjum Singh Solanky
41bdd6d6d9 Throw warning on prettier formatting issues in web app 2024-08-05 03:58:20 +05:30
Debanjum Singh Solanky
1cdfa8087c Update Khoj tagline to "Your Second Brain" 2024-08-05 02:27:05 +05:30
Debanjum Singh Solanky
46f928165c Fix deep linking to settings page cards from docs 2024-08-05 02:27:05 +05:30
sabaimran
f7840782a4 Fix broken rendering of math equations via katex 2024-08-05 00:20:43 +05:30
Debanjum Singh Solanky
b803ed19d3 Add simplified, cleaner khoj logo images to web app static dir 2024-08-04 23:40:21 +05:30
sabaimran
69c3635ce7 Merge pull request #877 from khoj-ai/features/fit-and-finish-new-ux
Fit and finish updates for the new UX
2024-08-04 10:26:33 -07:00
Debanjum Singh Solanky
51e56e17ee Align padding of agent pills to home screen chat input on small screens 2024-08-04 21:57:54 +05:30
Debanjum Singh Solanky
b744dffefd Align voice message button with send chat message button style 2024-08-04 21:04:38 +05:30
Debanjum Singh Solanky
70f670dcf7 Show send button when text in chat input else voice message button
Utilize chat footer space more efficiently. This is especially useful
on small screens

- Send button is anyway only enabled when there is text in chat input
- Otherwise voice message button is better to show by default
2024-08-04 19:25:49 +05:30
Debanjum Singh Solanky
c627527a6f Reorder automation card actions buttons. Put Delete action last 2024-08-04 19:01:11 +05:30
Debanjum Singh Solanky
c7b67a978e Align agents and automation page structure, widths and spacings
- Remove invalid call to styles.main
- Remove unnecessary top padding above side pane to keep side pane at
  consistent position across web app
- Use same pageLayout styles and styling structure on agent like
  automation
- Vertically center automation section and page title on it's row
- Fix applying flex vs grid with tailwind
2024-08-04 19:01:11 +05:30
Debanjum Singh Solanky
60af173c4a Improve responsive spacing of chat page footer buttons
- Remove x axis footer padding on small screens to preserve space,
  keep equal spacing between footer items
- Add 1rem margin to buttons to not have overlap in boundary
- Add 1rem y-axis padding to chat footer to not have focus boundary
  leave the footer boundary on smaller screens
2024-08-04 19:01:10 +05:30
sabaimran
4f2fcc82f0 Make the input area only rounded on the top corners when in mobile view
- Create better styling for the input area buttons, resizing in mobile and creating more even height with a more minimal send button
2024-08-04 18:28:33 +05:30
sabaimran
322fb34d4b Add top padding to the automations header to align it with the agents page 2024-08-04 12:27:37 +05:30
sabaimran
3e1e4a1857 Move the clients section back to the bottom 2024-08-04 11:32:22 +05:30
Debanjum Singh Solanky
caf5c3d74c Link to Khoj manifest in home page metadata to support PWA install
Installing Khoj as PWA was supported in previous web UX as well. This
just adds link to the existing webmanifest to continue support for
installing Khoj as PWA with new web UX
2024-08-04 05:06:38 +05:30
Debanjum Singh Solanky
692058bbdd Fix time of day calculation logic
Previously between 00:00 - 04:00 it'd trigger afternoon insteead of
evening
2024-08-04 04:53:50 +05:30
Debanjum Singh Solanky
015c155582 Simplify structure of chat page to match other pages 2024-08-04 04:43:55 +05:30
Debanjum Singh Solanky
bf71e472c4 Load static assets from Khoj server in dev environment 2024-08-04 04:25:48 +05:30
Debanjum Singh Solanky
f38c072f07 Update chat session title in side pane to new title after rename
Previously the rename wasn't updating the chat session title. We'd
have to refresh the page or side pane to get latest chat session names
after rename action.
2024-08-04 04:25:48 +05:30
Debanjum Singh Solanky
2f7a8698a0 Fix width and equalize spacing between buttons in chat footer
Previously the footer's right border wasn't visible on small screens
due to usage of w-full

Use mr-1 on send button instead of px-1 on chat input parent to
eualize chat footer buttons spacing
2024-08-04 04:25:48 +05:30
Debanjum Singh Solanky
5541bc09c8 Prefix Khoj page breadcrumbs to chat page title for orientation
Allows tab search by looking at standard prefix. Still allows
page title based identification of different Khoj chat sessions
2024-08-04 04:25:48 +05:30
Debanjum Singh Solanky
6a9865ace7 Only show API keys card in non anon mode
- Show informative toast messages on copy, delete of API keys
- Onle show API keys card in non anonymous mode. API keys aren't
  required (and is disabled on server side) in anon mode. Not showing
  card at all in anon mode reduces chance of unnecessary confusion
2024-08-04 04:25:45 +05:30
Debanjum Singh Solanky
f28208d35b Only show chat sessions uptil last month in side pane
- Reduce chat title size
2024-08-04 01:52:08 +05:30
sabaimran
75559a55aa only show search if logged in. update agents icon 2024-08-03 23:23:03 +05:30
sabaimran
185dcb61f7 Update the settings page to better match the design 2024-08-03 20:49:19 +05:30
sabaimran
3e74d383fe Strip quotes from the response mode llm response 2024-08-03 17:33:20 +05:30
sabaimran
87e97e40f4 Resolve various warnings during export 2024-08-03 17:33:04 +05:30
sabaimran
5a75f2c00f Use filled icons when side panel is open 2024-08-03 15:42:49 +05:30
sabaimran
e6260a7bb6 Improve oadding for h9me page chat iput area and inc margin on api keys 2024-08-03 15:33:33 +05:30
Debanjum Singh Solanky
7a8a9fc807 Auto focus cursor on search input when open search page 2024-08-03 13:52:36 +05:30
Debanjum Singh Solanky
30304ccc56 Fix session drawer to fit title, action triple-dot in width on mobile 2024-08-03 13:52:36 +05:30
Debanjum Singh Solanky
5b17fa5dda Set home, chat page height so footer, header visible w/o scroll on phone
Set dynamic view height of page to 100%
2024-08-03 13:52:36 +05:30
sabaimran
687a881ad2 Remove the agents header in the loading view 2024-08-03 13:44:56 +05:30
sabaimran
0db630a123 image cards should be /image, not /paint 2024-08-03 13:44:31 +05:30
sabaimran
261f62e353 Fix automations mobile view by using a wrapper component that chooses a dialog or a drawer 2024-08-03 13:44:17 +05:30
sabaimran
4ce17acd00 Set greeting message to longer text in default view. Only show two agents in mobile 2024-08-03 12:14:58 +05:30
sabaimran
6c35ee4960 Revert height of the side panel on the home page 2024-08-03 11:59:07 +05:30
Debanjum Singh Solanky
e66adf60c5 Have the home and chat page take full height, reduce greeting top space 2024-08-03 11:54:12 +05:30
Debanjum Singh Solanky
cf8745ef78 Improve structure of chat footer on mobile to put agents above input 2024-08-03 11:31:57 +05:30
Debanjum Singh Solanky
529ffdb7e3 Make Title, Chat Footer Icons larger to ease click, tap on Mobile 2024-08-03 11:23:29 +05:30
Debanjum Singh Solanky
8d1c5226ec Remove unnecessary debug logs 2024-08-03 09:55:31 +05:30
sabaimran
f136214290 Improve the nav menu in the not logged in experience 2024-08-03 09:44:04 +05:30
sabaimran
f9606ce9b7 Merge branch 'features/fit-and-finish-new-ux' of github.com:khoj-ai/khoj into features/fit-and-finish-new-ux 2024-08-03 09:34:04 +05:30
Debanjum Singh Solanky
d8fe677933 Prevent overflow on Search page by search results 2024-08-03 07:07:35 +05:30
Debanjum Singh Solanky
f3765a20b9 Improve content alignment on automation page for small screens
- Left align email, location, timezone pills on small screens
- Indent user enabled automations to improve delineation between
  sections
2024-08-03 07:05:15 +05:30
Debanjum Singh Solanky
a6e1b2c7cb Style nav menu button and expand nav menu item click area to full-width
Style profile pircture button on nav menu
 - Use primary colored ring around subscribed user profile on nav menu
 - Use gray colored ring around non-subscribed user profile on nav menu
 - Use upper case initial as profile pic for user with no profile pic

- Click anywhere on nav menu item to trigger action
  Previously the actual clickable area was smaller than the width of
  the nav menu item
2024-08-03 05:43:24 +05:30
Debanjum Singh Solanky
eed9e401a2 Improve alignment of title bar elements 2024-08-03 04:11:58 +05:30
sabaimran
f188396395 Prompt to login when authenticated, click on suggestion card
- Improve styling for the side panel when not logged in
2024-08-03 01:42:32 +05:30
sabaimran
9c5ff1699a Use new nav menu alignment in the settings page 2024-08-03 01:42:32 +05:30
sabaimran
b1d3979ed9 Fix navmenu in settings, share/chat pages 2024-08-03 01:42:21 +05:30
sabaimran
5f8b76c8f2 Fix layout/styling of the factchecker app 2024-08-03 01:07:59 +05:30
sabaimran
1bb746aaed Adjust spacing when side panel is opened 2024-08-03 01:07:59 +05:30
sabaimran
07b3bdf181 Update nav menu styling to include everything in one header
- Move the nav menu into the chat history side panel component, so that they both show up on one line
- Update all pages to use it with the new formatting
- in mobile, present the sidebar button, home button, and profile button evenly centered in the middle
2024-08-03 01:07:55 +05:30
Debanjum Singh Solanky
e62888659f Only show greeting once userConfig is fetched from server
- Pass userConfig from Home as prop to chatBodyData component with
  loading state
- Pass loading state of userConfig to allow components to handle
  rendering dependent elements once it is loaded
2024-08-02 20:25:09 +05:30
Debanjum Singh Solanky
0adee07d40 Update home page greetings to use user name, when available 2024-08-02 20:25:09 +05:30
sabaimran
bbe7491f2f Prompt to login when authenticated, click on suggestion card
- Improve styling for the side panel when not logged in
2024-08-02 20:12:18 +05:30
sabaimran
d48a789442 Use new nav menu alignment in the settings page 2024-08-02 19:44:30 +05:30
sabaimran
e6014e89bf Fix navmenu in settings page 2024-08-02 19:28:59 +05:30
sabaimran
1509c536f9 Fix layout/styling of the factchecker app 2024-08-02 19:06:01 +05:30
sabaimran
0d8cdee60a Adjust spacing when side panel is opened 2024-08-02 17:49:50 +05:30
sabaimran
d3c07a098d Update nav menu styling to include everything in one header
- Move the nav menu into the chat history side panel component, so that they both show up on one line
- Update all pages to use it with the new formatting
- in mobile, present the sidebar button, home button, and profile button evenly centered in the middle
2024-08-02 17:46:13 +05:30
sabaimran
5a8ea884a9 Use new HTTP stream format within the new UX
Use updated format for HTTP streamed responses from the Khoj server in the new chat UX
Remove references to the websocket connected field, as websocket use has been deprecated
2024-08-02 02:35:10 -07:00
Debanjum Singh Solanky
02b46a1784 Render references after chat response is streamed for smoother render
Otherwise the Khoj's chat response is filling up in between the
streamed message and already rendered references section at the bottom
of the message

Define OnlineContext type to simplify typing online context param
across other interfaces and functions
2024-08-02 14:11:34 +05:30
Debanjum Singh Solanky
a733e5c1d4 Remove unused handleCompiledReferences chat functions 2024-08-02 13:18:55 +05:30
Debanjum Singh Solanky
7858aff2e2 Trigger welcomeConsole only once on chat, shared chat page load 2024-08-02 13:18:01 +05:30
Debanjum Singh Solanky
cab0957fd3 Just show Khoj logo on title bar on small screens
Continue to show logo + text on larger screens
2024-08-02 13:18:01 +05:30
Debanjum Singh Solanky
3f607b3978 Add icons, improve description of home, chat & search page metadata 2024-08-02 13:18:01 +05:30
Debanjum Singh Solanky
4f783b911c Update DOMPurify imports correctly to resolve compilation warnings 2024-08-02 13:18:01 +05:30
sabaimran
4492017b96 Move processmessagechunk file into a common chat function 2024-08-02 12:31:43 +05:30
sabaimran
13dee7d89e Remove status update for understanding query 2024-08-01 19:22:21 +05:30
sabaimran
6babd5c0ce Merge pull request #876 from khoj-ai/features/use-intl-phone-input-settings
Use international phone number input and verify whatsapp flow
2024-08-01 04:52:02 -07:00
sabaimran
1b2cad2a2c Use af in the default state and configure the phone number input styling 2024-08-01 17:04:57 +05:30
sabaimran
723b37955a Disable input for phone number only if its pending verification 2024-08-01 16:45:38 +05:30
sabaimran
84dd1b57fe Use an intl phone input number field and fix the whole verification flow
- There were some state mismatches in configuring a whatsapp number. This commit fixes those issues and uses an external library for phone number validation
2024-08-01 16:44:17 +05:30
sabaimran
ed16914ac3 Remove deprecated fields and fix erroneous export in settings page 2024-08-01 14:45:54 +05:30
sabaimran
7941f4d54d Remove references to deprecated setupwebsocket function 2024-08-01 14:43:17 +05:30
sabaimran
db93ac5d4b Merge branch 'features/big-upgrade-chat-ux' of github.com:khoj-ai/khoj into features/use-new-sse-in-new-chat-ux 2024-08-01 14:41:50 +05:30
sabaimran
fd0e0405af Fix logic for setting and sending the initial chat message from the home page
- Load agents only once when the page loads, rather than triggering constant re-renders
2024-08-01 13:53:16 +05:30
sabaimran
9a43622cef Remove usages of the websocketconnected variable 2024-08-01 13:14:23 +05:30
sabaimran
bfeb64b48f Migrate the shared chat page to also use the new SSE streaming format 2024-08-01 13:14:09 +05:30
sabaimran
833553c3a3 Move conversation commands selection earlier to include in telemetry collected 2024-08-01 12:52:41 +05:30
sabaimran
dbbcf2564f Remove the usage of emojis in the incremental status updates 2024-08-01 12:52:05 +05:30
sabaimran
cd85a51980 Ingest new format for server sent events within the HTTP streamed response
- Note that the SSR for next doesn't support rendering on the client-side, so it'll only update it one big chunk
- Fix unique key error in the chatmessage history for incoming messages
- Remove websocket value usage in the chat history side panel
- Remove other websocket code from the chat page
2024-08-01 12:50:43 +05:30
Debanjum
60870a7a3e Create Settings Page in new Web App (#872)
- Details
  - Add Profile Client, Content Sections
  - Make Multi Step Cards for Whatsapp, Files, Notion Integrations
  - Align Settings page with new Baraabar UX
2024-07-30 06:59:42 -07:00
Debanjum Singh Solanky
32ce564b7c Remove unused Files Connect button and setup Github content card 2024-07-30 18:55:14 +05:30
Debanjum Singh Solanky
ecb873c488 Only allow search model to be updated without being subscribed
Do not make fetch request to server if user is not subscribed
2024-07-30 18:50:57 +05:30
Debanjum Singh Solanky
f58cff5bcc Increase rate limit in the api/content vs deprecated indexer API 2024-07-30 16:09:26 +05:30
Debanjum Singh Solanky
f0bb6883f8 Improve Delete experience on Files Card in Settings Page
Improve placeholder text for notion API key and Whatsapp
number (mention country code required)
2024-07-30 15:25:14 +05:30
sabaimran
b1eb564706 remove the optional pydantic typing from the files param 2024-07-30 15:25:14 +05:30
sabaimran
4a7efdc552 Use patch in place of put in the indexer API call, ensure that files are not being required in the indexer path 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
ffbf57292c Create synced files management modal on the settings page
Use a Command Dialog to allow easier filtering of files to view
without having to leave the settings page
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
ccc46a09b5 Add new API to batch delete a list of files by filename
- Rearrange DELETE content API definitions order to go from more
  specific to more general
- Create batched file deletion DB adapter
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
9d86cb57ac Build UX to Connect and Manage Notion Integration via Settings Page 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
7ee179ee1f Return user's Notion token in API call for detailed user settings 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
00a908ae12 Move subscription card to Profile settings section. Remove Billing section
- Why
  Profile section and billing section looked too empty (1 card each).
  Combining them makes the setting page look more complete. Shows
  subscription options early on
- Details
  - Made Futurist text orange
  - Made Unsubscribe a down button instead of cloud slash
  - Updated toast title to subscription
  - Improve Futurist trial title and description
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
058c902dc7 Delete unused npm package-lock.json as Web app uses yarn.lock instead 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
b8c9b3ffa3 Reduce padding height of input area on new home page 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
8a447107dd Set user name on clicking Save button on settings page 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
44e0b20202 Align Content, Client & Billing settings sections with new designs 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
51e83bcc26 Improve responsive behavior of settings cards 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
efcad4996d Add phone number verification for Whatsapp to new settings page 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
48548684c0 Add card to connect Whatsapp to Khoj on settings page 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
8ec90f194f Add title icons for each content section card on settings page 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
60cdf61737 Create billing section for managing subscription on settings page
- Replicate behavior on current settings.html page
- Improve text for each subscription state to make it more informative, fun
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
2e165a0e0a Create client API keys section on settings page
- Add table shadcn component to use in API keys settings section
- In dev mode, route requests to auth to khoj server at localhost:42110
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
00fa4fa0fa Save model on selecting model in dropdown. No extra save action reqd
- Remove now unnecessary button to Save in Card with dropdown
- Use toast to show success, failure (not working)
- Rename language to search, Move it to features section. Add icon to
  the card
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
13292fc4ca Add icons to card headings 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
a5a06da3fc Use Dropdown component for model options. Make cards more responsive
- Ensure model name doesn't stretch or shrink dropdown width from
  parent card width
- Ensure buttons flex wrap on smaller displays
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
ade2f6f5d1 Rename selected voice model in get config API response for consistency
- Update references in new and old web client settings
- Arrange new client settings props and add header comments similar to
- config response for code readability
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
b3253562a5 Dynamically set Content cards buttons based on already setup or not 2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
7e8e80f29e Create config page using tailwind, shadcn components, styling
- Include side pane but with only the account info in it
- Replicate styling of the old config page
2024-07-30 15:25:14 +05:30
Debanjum Singh Solanky
88007d7552 Get user config in the new web client from the new user config APIs 2024-07-30 15:25:14 +05:30
sabaimran
a6339bb973 Add mroe card suggestions and simplify color selection for suggestion cards 2024-07-29 19:11:39 +05:30
sabaimran
551630f0f1 Code clean-up and some fit and finish
- Add a lot more suggestions cards, improve mobile rendering of suggestion cards, improve alignment of chat input, shift message when starts recording voice, remove dead code
2024-07-28 15:19:36 +05:30
sabaimran
413255ddc7 Add closing tag to whatsapp qr code image 2024-07-28 13:50:38 +05:30
sabaimran
41eb85c933 Update the docs for whatsapp to include the QR code 2024-07-28 13:43:50 +05:30
sabaimran
1a1d9c7257 Merge branch 'master' of github.com:khoj-ai/khoj into features/big-upgrade-chat-ux 2024-07-27 14:18:05 +05:30
Raghav Tirumale
1685c60e3c Nav Menu Upgrades and Minor UX Improvements (#869)
* Converted navigation menu into a dropdown menu
* Moved collapsed side panel menu icons into top row
* Auto refresh when conversation is deleted to update side panel and route back to main page if deletion is on current conversation
* Highlight the current conversation in the side panel
* Dynamic homepage messages with current day and time of day.
* `colorutils` upgraded to have more expansive tailwind color options and dynamic class name generation.
* Converted create agent button alert into shadcn `ToolTip`
* Colored lines and icons for agents in chat window
* Cleaned up border styling in dark mode
* fixed three dot menu in side panel to be more easier to click
* Add the KhojLogo import in the nav menu and use a default user profile icon when not authenticated
* Get rid of custom --box-shadow CSS variable
* Pass the agent metadat through the chat body data in order to style the send button
* Add login to the unauthenticated login view, redirecto to home if conversation history not loaded
* Set a max height for the input text area
* Simplify tailwind class names

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
2024-07-27 14:12:00 +05:30
Debanjum
8503d7a07b Split Configure API into Content, Model API paths (#857)
## Major: Breaking Changes
- Move API endpoints under /configure/<type>/model to /api/model/<type>
- Move API endpoints under /api/configure/content/ to /api/content/
- Accept file deletion requests by clients during sync
- Split /api/v1/index/update into /api/content PUT, PATCH API endpoints

## Minor: Create New API Endpoint
- Create API endpoints to get user content configurations

Related: #852
2024-07-26 23:48:41 -07:00
Debanjum Singh Solanky
878cc023a0 Fix and improve openai chat actor tests
- Use new form of passing doc references to now passing chat actor
  test
- Fix message list generation from conversation logs provided
  Strangely the parent conversation_log gets passed down to
  message_to_log func when the kwarg is not explicitly specified
2024-07-26 23:53:47 +05:30
Debanjum Singh Solanky
a47a54f207 Pass user name to document and online search actors prompts
This should improve the quality of personal information extraction
from document and online sources. The user name is only used when it
is set
2024-07-26 23:53:17 +05:30
sabaimran
e86143dbb0 Merge pull request #867 from khoj-ai/features/search-page-v2
Update the search page
2024-07-26 08:08:04 -07:00
sabaimran
eb5af38f33 Release Khoj version 1.17.0 2024-07-26 20:14:45 +05:30
Raghav Tirumale
5dcac18ba5 New Agents Page User Interface (#866)
Changes for new agents page
- Modernized agent cards
- Responsive design to support mobile users
- Button for users to create their own agents (coming soon)
- Optimized to use tailwind and icon utils
- Side panel added for quick access to conversations
2024-07-26 20:12:31 +05:30
Debanjum Singh Solanky
3daef910c0 Remove stale comment from api content 2024-07-26 20:05:35 +05:30
sabaimran
44d34f9090 Update the unit test for the subscribed user 2024-07-26 19:59:01 +05:30
sabaimran
377f7668c5 Merge pull request #858 from khoj-ai/use-sse-instead-of-websocket
Use Single HTTP API for Robust, Generalizable Chat Streaming
2024-07-26 07:11:54 -07:00
sabaimran
6607e666dc Increase rate limit for data upload packet size in indexer.py 2024-07-26 19:35:32 +05:30
Debanjum Singh Solanky
778c571288 Use enum to track chat stream event types in chat api router 2024-07-26 00:19:43 +05:30
sabaimran
7482797605 Add some better default states for no files found, prompt to search. Add link to search in the file search compnoent in side panel 2024-07-25 13:00:28 +05:30
sabaimran
662dffea3b Press enter to search 2024-07-24 19:28:38 +05:30
sabaimran
19cd607c96 Style the see content button correctly 2024-07-24 18:28:23 +05:30
sabaimran
75a370cc06 Implement focus mode to click into full text of the note 2024-07-24 18:00:33 +05:30
sabaimran
5adbfe14ab Add a search page that just renders truncated results when you click search 2024-07-24 17:43:19 +05:30
sabaimran
52db15706d Remove unused styling 2024-07-24 17:42:36 +05:30
sabaimran
cfe7a1068e Update the navmenu title if prop is updated and undefined 2024-07-24 17:41:31 +05:30
Debanjum Singh Solanky
ebe92ef16d Do not send references twice in streamed image response
Remove unused image content to reduce response payload size.
References are collated, sent separately
2024-07-24 17:18:14 +05:30
Debanjum Singh Solanky
37b8fc5577 Extract events even when http chunk contains partial or mutiple events
Previous logic was more brittle to break with simple unbalanced
'{' or '}' string present in the event data. This method of trying to
identify valid json obj was fairly brittle. It only allowed json
objects or processed event as raw strings.

Now we buffer chunk until we see our unicode magic delimiter and only
then process it.

This is much less likely to break based on event data and the
delimiter is more tunable if we want to reduce rendering breakage
likelihood further
2024-07-24 17:17:39 +05:30
sabaimran
4d30e5b158 Fix indexing error for notion, expecting image and docx in dict 2024-07-24 16:58:31 +05:30
sabaimran
694bedc25b Add support for text to speech and speech to text (#863)
- Add support for text to speech, speech to text. Add loading and responsive indicators to reflect state.
- When streaming for speech to text, show incremental transcription in the message input field
- When streaming text to speech, and a pause button in the chat message to allow user to stop playback
2024-07-24 14:36:40 +05:30
Raghav Tirumale
3e4325edab Upgrade: New Home Screen for Khoj (#860)
* V1 of the new automations page
Implemented:
- Shareable
- Editable
- Suggested Cards
- Create new cards
- added side panel new conversation button
- Implement mobile-friendly view for homepage
- Fix issue of new conversations being created when selected agent is changed
- Improve center of the homepage experience
- Fix showing agent during first chat experience
- dark mode gradient updates

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
2024-07-24 13:16:19 +05:30
Debanjum Singh Solanky
70201e8db8 Log total, ttft chat response time on start, end llm_response events
- Deduplicate code to collect chat telemetry by relying on
  end_llm_response event
- Log time to first token and total chat response time for latency
  analysis of Khoj as an agent. Not just the latency of the LLM
- Remove duplicate timer in the image generation path
2024-07-23 23:21:12 +05:30
Debanjum Singh Solanky
b36a7833a6 Remove the old mechanism of streaming compiled references
Do not need response generator to stuff compiled references in chat
stream using "### compiled references:" separator.

References are now sent to clients as structured json while streaming
2024-07-23 19:53:51 +05:30
Debanjum Singh Solanky
eb4e12d3c5 s/online_context/onlineContext chat API response field for consistency
This will align the name of the online context field returned by
current chat message and chat history
2024-07-23 19:50:43 +05:30
Debanjum
498fe2458c Support Gemma 2 Model Family for Offline Chat (#855)
## Overview
- Gemma 2 is a new open model family by Google. They've released a 9B, 29B param model. A 2B model is also expected.
- It performs really well on the Chatbot arena and shows good performance when testing within Khoj as well.
- Llama.cpp support for Gemma 2 architecture seems to have stabilized
- If Gemma 2 performs well in further testing, it can be made the default offline chat model for Khoj
  - Once the 2B param model is released, the model size to download can be automatically chosen based on (V)RAM available

## Major
- Support Gemma 2 for Offline Chat
- Improve and fix chat model prompts for better, consistent context

## Minor
- Fix and improve offline chat actor, director tests
- Improve offline chat truncation to consider chat message delimiter tokens
2024-07-23 06:57:02 -07:00
Debanjum Singh Solanky
0277d16daf Share desktop chat streaming utility funcs across chat, shortcut views
Null check menu, menuContainer to avoid errors on Khoj mini
2024-07-23 19:16:33 +05:30
Debanjum Singh Solanky
e439a6ddac Use async/await in web client chat stream instead of promises
Align streaming logic across web, desktop and obsidian clients
2024-07-23 18:17:47 +05:30
Debanjum Singh Solanky
fafc467173 Put loading spinner at bottom of chat message in web client 2024-07-23 18:17:47 +05:30
Debanjum Singh Solanky
fc33162ec6 Use new chat streaming API to show Khoj train of thought in Desktop app
Show loading spinner at end of current message
2024-07-23 18:17:47 +05:30
Debanjum Singh Solanky
c5ad172616 Keep loading animation at message end & reduce lists padding in Obsidian
Previously loading animation would be at top of message. Moving it to
bottom is more intuitve and easier to track.

Remove white-space: pre from list elements. It was adding too much y
axis padding to chat messages (and train of thought)
2024-07-23 17:56:03 +05:30
Debanjum Singh Solanky
54b4203683 Update chat API client tests to mix testing of batch and streaming mode 2024-07-23 17:56:03 +05:30
Debanjum Singh Solanky
3f5f418d0e Use new chat streaming API to show Khoj train of thought in Obsidian client 2024-07-23 17:56:03 +05:30
Debanjum Singh Solanky
8303b09129 Convert snake case to camel case in chat view of obsidian plugin 2024-07-23 15:29:12 +05:30
Debanjum Singh Solanky
b224d7ffad Simplify get_conversation_by_user DB adapter code 2024-07-23 14:51:11 +05:30
Debanjum Singh Solanky
daec439d52 Replace old chat router with new chat router with advanced streaming
- Details
  Only return notes refs, online refs, inferred queries and generated
  response in non-streaming mode. Do not return train of throught and
  other status messages

  Incorporate missing logic from old chat API router into new one.

- Motivation
  So we can halve chat API code by getting rid of the duplicate logic
  for the websocket router

  The deduplicated code:
  - Avoids inadvertant logic drift between the 2 routers
  - Improves dev velocity
2024-07-23 14:51:11 +05:30
Debanjum Singh Solanky
2d4b284218 Simplify streaming chat function in web client 2024-07-23 14:38:55 +05:30
Debanjum Singh Solanky
6b9550238f Simplify advanced streaming chat API, align params with normal chat API 2024-07-22 22:51:24 +05:30
Debanjum Singh Solanky
b8d3e3669a Stream Status Messages via Streaming Response from server to web client
- Overview
Use simpler HTTP Streaming Response to send status messages, alongside
response and references from server to clients via API.

Update web client to use the streamed response to show train of thought,
stream response and render references.

- Motivation
This should allow other Khoj clients to pass auth headers and recieve
Khoj's train of thought messages from server over simple HTTP
streaming API.

It'll also eventually deduplicate chat logic across /websocket and
/chat API endpoints and help maintainability and dev velocity

- Details
  - Pass references as a separate streaming message type for simpler
    parsing. Remove passing "### compiled references" altogether once
    the original /api/chat API is deprecated/merged with the new one
    and clients have been updated to consume the references using this
    new mechanism
  - Save message to conversation even if client disconnects. This is
    done by not breaking out of the async iterator that is sending the
    llm response. As the save conversation is called at the end of the
    iteration
  - Handle parsing chunked json responses as a valid json on client.
    This requires additional logic on client side but makes the client
    more robust to server chunking json response such that each chunk
    isn't itself necessarily a valid json.
2024-07-22 15:41:21 +05:30
Debanjum Singh Solanky
91fe41106e Convert Websocket into Server Side Event (SSE) API endpoint
- Convert functions in SSE API path into async generators using yields
- Validate image generation, online, notes lookup and general paths of
  chat request are handled fine by the web client and server API
2024-07-21 14:20:22 +05:30
sabaimran
9cf52bb7e4 Update automations UX for more consistency (#856)
* Update the automations UI to be a more suitable color distribution based on new designs

* Use accented colors for the metadata, update dark mode colors

* Update form to use icons as well and render more pretty inline form labels
2024-07-21 12:22:23 +05:30
sabaimran
e694c82343 Fix Docker build issues with yarn / next /node (#859)
* Rollback node version being installed from nodesource to node 20
2024-07-19 19:11:29 +05:30
sabaimran
1af9dbb083 Switch node/yarn install steps to use more native installation patterns 2024-07-19 17:10:08 +05:30
sabaimran
6d5ca5a3e1 yarn clean cache before build 2024-07-19 16:06:38 +05:30
sabaimran
7f0d1bd414 Add verbose logs when outputing yarn install steps 2024-07-19 15:48:43 +05:30
sabaimran
7426a4f819 Prefetch related agent when retrieving the conversation for performance improvements 2024-07-19 14:43:30 +05:30
Debanjum Singh Solanky
07f36fa95a Update new web interface with update calls to /content, /model APIs 2024-07-19 12:23:22 +05:30
Debanjum Singh Solanky
f03525f431 Add back /api/configure as /api/settings API endpoint
It had been removed during the /api/configure/content to /api/content
API migration before
2024-07-19 05:40:34 +05:30
Debanjum Singh Solanky
3832ef0236 Move API endpoints under /api/configure/phone/ to /api/phone/
Pull out /api/configure/phone API endpoints into /api/phone for
more concise and sufficiently explanatory API path

Refactor Flow
1. Rename /api/configure/phone -> /api/phone
2024-07-19 05:40:34 +05:30
Debanjum Singh Solanky
1197266912 Move API endpoints under /configure/<type>/model to /api/model/<type>
Now the API to configure all the AI models is under /api/models.
This provides better organization and API hierarchy. The /configure
url segment was redundant.

- Rename POST /api/phone to PATCH /api/phone
- Rename GET /api/configure to GET /api/settings

Refactor Flow
1. Move out POST /user/name to main api.py
2. Rename /api/configure/<type>/model -> /api/model/<type>
3. Rename @api_configure to @api_mode
4. Rename file api_config.py to api_model.py
2024-07-19 05:40:34 +05:30
Debanjum Singh Solanky
469a1cb6a2 Move API endpoints under /api/configure/content/ to /api/content/
Pull out /api/configure/content API endpoints into /api/content to
allow for more logical organization of API path hierarchy

This should make the url more succinct and API request intent more
understandable by using existing HTTP method semantics along with the
path.

The /configure URL path segment was either
- redundant (e.g POST /configure/notion) or
- incorrect (e.g GET /configure/files)

Some example of naming improvements:
- GET /configure/types -> GET /content/types
- GET /configure/files -> GET /content/files
- DELETE /configure/files -> DELETE /content/files

This should also align, merge better the the content indexing API
triggered via PUT, PATCH /content

Refactor Flow
1. Rename /api/configure/types -> /api/content/types
2. Rename /api/configure -> /api
3. Move /api/content to api_content from under api_config
2024-07-19 05:40:34 +05:30
Debanjum Singh Solanky
bba4e0b529 Accept file deletion requests by clients during sync
- Remove unused full_corpus boolean. The full_corpus=False code path
  wasn't being used (accept for in a test)
- The full_corpus=True code path used was ignoring file deletion
  requests sent by clients during sync. Unclear why this was done
- Added unit test to prevent regression and show file deletion by
  clients during sync not ignored now
2024-07-19 04:53:01 +05:30
Debanjum Singh Solanky
5923b6d89e Split /api/v1/index/update into /api/content PUT, PATCH API endpoints
- This utilizes PUT, PATCH HTTP method semantics to remove need for
  the "regenerate" query param and "/update" url suffix
- This should make the url more succinct and API request intent more
  understandable by using existing HTTP method semantics
2024-07-19 01:45:53 +05:30
Debanjum Singh Solanky
e9f86e320b Fix and improve offline chat actor, director tests
- Use updated references schema with compiled key
- Enable director tests that are now expected to pass and that do pass
  (with Gemma 2 at least)
2024-07-18 03:43:09 +05:30
Debanjum Singh Solanky
b0ee78586c Improve offline chat truncation to consider message separator tokens 2024-07-18 03:43:09 +05:30
Debanjum Singh Solanky
6f46e6afc6 Improve and fix chat model prompts for better, consistent context
- Add day of week to system prompt of openai, anthropic, offline chat models
- Pass more context to offline chat system prompt to
  - ask follow-up questions
  - know where to find information about khoj (itself)
- Fix output mode selection prompt. Log error if model does not select
  valid option from list of valid output modes provided
- Use consistent names for question, answers passed to
  extract_questions_offline prompt

- Log which model extracts question, what the offline chat model sees
  as context. Similar to debug log shown for openai models
2024-07-18 03:43:09 +05:30
Debanjum Singh Solanky
53eabe0c06 Support Gemma 2 for Offline Chat
- Pass system message as the first user chat message as Gemma 2
  doesn't support system messages
- Use gemma-2 chat format
- Pass chat model name to generic, extract questions chat actors
  Used to figure out chat template to use for model
  For generic chat actor argument was anyway available but not being
  passed, which is confusing
2024-07-18 03:09:38 +05:30
Debanjum Singh Solanky
65dade4838 Create API endpoints to get user content configurations
This is to be used by the new Next.js web client
2024-07-17 13:41:14 +05:30
Debanjum
2ab8fb78b1 Migrate the PyPI package to use project name: khoj (#853)
### Changes
- Deprecate [khoj-assistant](https://pypi.org/project/khoj-assistant) pypi package. Use more accurate and succinct pypi project name, [khoj](https://pypi.org/project/khoj)
- Update references to use `khoj` pypi package in docs and code
- Update pypi workflow to publish to both khoj, khoj-assistant for now
- Update stale python 3.9 support mentioned in our pyproject
   Can't support python 3.9 as depend on [Django 5.0.7](https://pypi.org/project/Django/5.0.7/) which needs python >=3.10

### Verify
- Updated `pypi.yml` github workflow publishes to both (new) [khoj](https://pypi.org/project/khoj/1.16.1.dev16/), (old) [khoj-assistant](https://pypi.org/project/khoj-assistant/1.16.1.dev16/) pypi projects
- Can install Khoj python package with `pip install khoj`
2024-07-17 01:05:51 -07:00
Debanjum
bf815e4463 Refactor Config API and Settings pages for Reuse and Consistency (#852)
### Major
- Reuse get config data logic across config pages on web client
- Make config api endpoint urls and response fields consistent
- Rename API path /api/config to /api/configure
- Move Web, Desktop client settings page to be under `/settings` from the previous `/config` url path

### Minor
- Pass isMobileWidth prop to SidePanel via chat share interface
- Turn prettier off instead of throwing error for now
- Do no explicitly add line-clamp plugin as it's in Tailwind by default
2024-07-17 01:03:06 -07:00
Debanjum Singh Solanky
a1c362a4f7 Expose web, desktop settings page under /settings, not /configure
- Update references to the settings page to use new url across docs
  and code
- Rename desktop and web settings page to settigns.html instead of
  config[ure].html
2024-07-17 13:17:29 +05:30
Debanjum Singh Solanky
b015b0e83d Arrange config API detailed response fields to improve readability
There are a lot of fields being returned. Group returned fields and
add comment header to each Group for readability
2024-07-17 13:17:28 +05:30
Debanjum Singh Solanky
71ebf31a54 Make config API detailed response fields more intuitive, consistent
- Use name, id for every [search|chat|voice|pain]_model_option
- Rename current_model_state field to more intuitive enabled_content_source
- Update references to the update fields in config.html
2024-07-17 12:41:01 +05:30
Debanjum Singh Solanky
30d60aaae9 Add, fix Khoj Docker container labels 2024-07-17 10:41:17 +05:30
Debanjum Singh Solanky
583fa3c188 Migrate the pypi package to khoj project name. Update references
- Deprecate khoj-assistant pypi package. Use more accurate and
  succinct pypi project name, khoj
- Update references to sye khoj pypi package in docs and code instead
  of the legacy khoj-assistant pypi package
- Update pypi workflow to publish to both khoj, khoj-assistant for now
- Update stale python 3.9 support mentioned in our pyproject. Can't
  support python 3.9 as depend on latest django which support >=3.10
2024-07-17 10:41:16 +05:30
Debanjum Singh Solanky
7316e6b9d3 Pass isMobileWidth prop to SidePanel via chat share interface 2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
4759c4ac96 Turn prettier off instead of throwing error for now
Until web interface code is reformatted with prettier
2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
466ef3f8f1 Do no explicitly add line-clamp plugin as it's in Tailwind by default 2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
59000a47cb Move Desktop config page to /configure from /config url path
Update references to point to page at /configure instead of /config
2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
a5c16ad600 Move Web client config page to /configure from /config url path
Update docs, clients and error messages to point to /configure
instead of /config
2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
de15a7a3fc Rename API path /api/config to /api/configure
- Update clients calling /api/config to call /api/configure instead
2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
dd31936746 Make config api endpoint urls consistent
- Consistently use /content/ for data. Remove content-source from path
- Remove unnecessary /data/ prefix for API endpoints under /config
2024-07-16 16:13:27 +05:30
Debanjum Singh Solanky
e8176b41ef Reuse get config data logic across config pages on web client
- Put logic to get config data, detailed or basic into router helpers module
- Use the get config func across the config pages on web clients

- Put configure content and get_notion_auth_url funcs in router helper
  module to avoid circular import
2024-07-16 16:13:27 +05:30
sabaimran
1a5405e24c Fix interpretation of day of week in automation form 2024-07-16 10:12:30 +05:30
sabaimran
c837f3779e Update the agents page with new UX (#850)
- Use icons/colors for setting the styling of agents
- Update automations page to use the shadcn cards: https://github.com/shadcn-ui/ui
2024-07-16 10:10:55 +05:30
sabaimran
1c6ed9bc6d Migrate the existing automations page to use React (#849)
Migrates the Automations page to React, mostly keeping the overall design consistent with organization. Use component library, with some changes in color. Add easier management with straightforward form and editing experience.
Use system preference for determining dark mode if not explicitly set.
2024-07-15 21:42:33 +05:30
Debanjum
c7764c7470 Fix, Improve Behavior, Styling of Chat View on Web (#851)
### Behavior
- Close chat sessions side panel on click open a chat session
- Show agent profile card with description when hover on agent in chat view
- Show action bar on last chat message without hover
- Show chat message action buttons without hover on mobile interfaces
- Show chat message timestamp on hover in chat view
- Show text descriptions of chat message action buttons on hover
- Render inline png, webp images generated by Khoj in chat view

### Fixes
- Do not render references with broken links in chat view
- Fix closing side panel on mobile when click open a chat session
- Only open side panel as drawer in mobile view
- Constrain chat messages to stay within view port across screen sizes

### Styling: Spacing, Sizing, Mobile Friendly
- Make Khoj icon appropriately sized and side panel arrow bold
- Conversations list should resize to take max space on side panel
- Make loading message, styling configurable. Do not show agent when no data
- Improve Train of Thought icons spacing and loading circle
- Improve mobile friendly styling of chat session side panel
- Improve styling of chat input, references UI across screen sizes
- Center cursor in chat input. See upto 2 lines for multi-line context

### Miscellaneous
- Add code formatter for web interface with prettier
2024-07-15 08:39:14 -07:00
Debanjum Singh Solanky
6c630bc6c3 Constrain chat messages to stay in view port across screen sizes
- Constrain chat messages max width to view port across screen sizes
- Wrap references on smaller screens, use tailwind, not js to apply styling
2024-07-15 21:00:50 +05:30
sabaimran
9a5bf4c701 Fix rendering of teaser reference panel in mobile width 2024-07-15 19:40:55 +05:30
sabaimran
2e9275c0f3 Remove side panel padding in desktop view. Fix width in mobile view 2024-07-15 19:33:12 +05:30
Debanjum Singh Solanky
ba0ba6b59f Merge branch 'features/big-upgrade-chat-ux' of github.com:khoj-ai/khoj into document-styling-on-chat-ux 2024-07-15 10:42:56 +05:30
Debanjum
23f61d49e0 Support syncing, searching images from Obsidian plugin (#847)
- Sync images from Obsidian vault with Khoj server now that Khoj can OCR images
- Support rendering images returned by Khoj search modal
2024-07-14 20:41:39 -07:00
Debanjum Singh Solanky
6f8f846086 Standardize code format for web interface with prettier
Use husky, lint-staged to run prettier pre-commit
2024-07-15 00:34:54 +05:30
sabaimran
06dce4729b Make most major changes for an updated chat UI (#843)
- Updated references panel
- Use subtle coloring for chat cards
- Chat streaming with train of thought
- Side panel with limited sessions, expandable
- Manage conversation file filters easily from the side panel
- Updated nav menu, easily go to agents/automations/profile
- Upload data from the chat UI (on click attachment icon)
- Slash command pop-up menu, scrollable and selectable
- Dark mode-enabled
- Mostly mobile friendly
2024-07-14 23:18:06 +05:30
Debanjum Singh Solanky
6dd90931e8 Fix closing side panel on mobile when click open a chat session 2024-07-14 22:54:49 +05:30
Debanjum Singh Solanky
47b754c07b Only open side panel as drawer in mobile view 2024-07-14 14:08:41 +05:30
Debanjum Singh Solanky
b47f30ad77 Make Khoj icon appropriately sized and side panel arrow bold 2024-07-14 14:06:36 +05:30
Debanjum Singh Solanky
e6b21144e2 Conversations list should resize to take max space on side panel 2024-07-14 13:49:36 +05:30
Debanjum Singh Solanky
c2bf405489 Make loading message, styling configurable. Do not show agent when no data
- Pass Loading message, class name via props to both inline and normal
  loading spinners
- Pass loading conversation message to loading spinner when chat
  history is being fetched
2024-07-14 13:00:36 +05:30
Debanjum Singh Solanky
63719747cb Show agent profile card with description when hover on agent in chat view
- Create profile card componennt. Use it for agent profile card
- Pass agent persona from khoj server via API
- Put link to agent profile page in the hover card to make it 2 clicks
  away. Othewise inadvertent clicks on agent in chat view lead away to
  agent page
- Use tailwind line-clamp extension to clamp card to first two lines
2024-07-14 12:20:11 +05:30
Debanjum Singh Solanky
dbbd4b9777 Show action bar on last chat message without hover 2024-07-14 10:32:31 +05:30
Debanjum Singh Solanky
a0f38e079f Improve Train of Thought icons spacing and loading circle 2024-07-14 09:35:15 +05:30
Debanjum Singh Solanky
e9567741eb Improve mobile friendly styling of chat session side panel 2024-07-14 00:57:08 +05:30
Debanjum Singh Solanky
b26a6e25d1 Show chat message action buttons without hover on mobile interfaces
This is because hover maybe hard to do on mobile devices
2024-07-14 00:54:23 +05:30
Debanjum Singh Solanky
f69f9e3523 Close chat sessions side panel on click open a chat session 2024-07-14 00:53:16 +05:30
Debanjum Singh Solanky
d51011314f Improve styling of chat input, references UI across screen sizes
Use tailwind screen breakpoints shorthand instead of js to apply
different styling for different screen sizes
2024-07-13 20:45:34 +05:30
Debanjum Singh Solanky
2668e42e7f Center cursor in chat input. See upto 2 lines for multi-line context
- Reuse class name when get slash command icons
- Previous chat input styling didn't have the cursor centered in the
  chat input text area. But it did allow seeing multi line chat inputs
  for context
2024-07-13 02:51:29 +05:30
Debanjum Singh Solanky
aeaebfb515 Show chat message timestamp on hover in chat view 2024-07-13 02:51:19 +05:30
Debanjum Singh Solanky
e00c6b486e Add hover text descriptions of action buttons on chat message in web view 2024-07-12 15:40:51 +05:30
Debanjum Singh Solanky
5fccccfdff Do not render references with broken links in chat view 2024-07-12 15:14:11 +05:30
Debanjum Singh Solanky
b98a0cfe1b Render inline png, webp images generated by Khoj in chat view
Add spacing between chat message paragraphs
2024-07-12 15:13:19 +05:30
sabaimran
3e7e73ddd6 Switch from using dynamic routes to static routes and extracting slug from URL manually. See https://github.com/vercel/next.js/discussions/64660 for limitations with static export / dynamic routes 2024-07-11 23:06:27 +05:30
sabaimran
bea0aa5445 Improve the logged out share experience 2024-07-11 20:11:21 +05:30
Debanjum Singh Solanky
02658ad4fd Upgrade Django version 2024-07-11 16:35:10 +05:30
Debanjum Singh Solanky
cbae8b68fb Add DB migration from making bi_encode configs optional in #834 2024-07-11 16:33:31 +05:30
Debanjum Singh Solanky
3a75838196 Add Keyboard shortcuts to navigate in Khoj Desktop 2024-07-11 16:29:53 +05:30
Debanjum Singh Solanky
6c1861b319 Improve the prompt to generate images with DALLE3 and SD3
- Major
  - Ask for prompt in prose
  - Remove seed from SD3 image generation to improve diversity of output
    for a given prompt
    Otherwise for conversations with similar sounding
    prompts, the images would be almost exactly the same. This maybe
    another indicator of SD3's inability to capture detailed
    instructions
  - Consistently use "prompt" wording instead of "query" in improved
    image generation prompts.
    Previously a mix of those terms were being used, which could confuse
    the chat model

- Minor
  - Add day of week to prompt
  - Remove 2-5 sentence limit on instructions to SD3. It seems to be
    able to follow longer instructions just with less fidelity than
    DALLE. And the 2-5 sentence instruction limit wasn't being adhered to
  - Improve ability to edit, improve the image based on follow-up
    instructions by the user
  - Align prompts for DALLE and SD3. Only difference is to wrap text to
    be rendered in quotes for SD3. This improves it's ability to render
    requested text. DALLE cannot render text as well or consistently
2024-07-11 16:29:53 +05:30
Debanjum Singh Solanky
21fe1a917b Support syncing, searching images from Obsidian plugin 2024-07-11 16:22:31 +05:30
sabaimran
6f1d799759 Modularize code and implemenet share experience 2024-07-10 23:08:16 +05:30
sabaimran
1b4a51f4a2 Remove print statement for debugging timestamps 2024-07-10 14:54:22 +05:30
sabaimran
0369eb6e0e Fix timestamp bug for pending message and expand CSP for thumbnails 2024-07-10 14:53:31 +05:30
sabaimran
375685530f Add content security policy to the chat page 2024-07-10 11:18:41 +05:30
sabaimran
c5cfd0f2cf Remove unused slash command-related useeffect hook 2024-07-10 10:03:58 +05:30
sabaimran
e1a5c17775 Add DOMPurify for rendering md text. Add a easter egg in the console 2024-07-10 10:03:08 +05:30
sabaimran
e358723baa Fix image rendering and unique key for pending message? 2024-07-09 21:55:54 +05:30
sabaimran
c8c5d50b1a Improve command bar slash experience 2024-07-09 21:39:13 +05:30
sabaimran
c25bf97831 Update hover styling for see all button 2024-07-09 20:55:54 +05:30
sabaimran
23b71b0dff Remove shadow from the slash command bar 2024-07-09 20:52:38 +05:30
sabaimran
998e2aec30 Update dark mode, fix chat message time stamp, fix rendering for new message 2024-07-09 20:50:20 +05:30
sabaimran
0c6b6de09e Revert web client route chat page rendering logic 2024-07-09 19:47:04 +05:30
sabaimran
cc22e1b013 Add pop-up module for the slash commands 2024-07-09 19:46:17 +05:30
sabaimran
5b69252337 Add hover effects for chat messages 2024-07-09 14:56:57 +05:30
sabaimran
a0e9530fa4 Merge branch 'master' of github.com:khoj-ai/khoj into features/chat-ui-updates-big 2024-07-09 12:57:50 +05:30
sabaimran
260aa61818 Remove tests for python3.9 2024-07-09 12:28:11 +05:30
sabaimran
4471c1e37f Apply mitigations for piling up open connections
- Because we're using a FastAPI api framework with a Django ORM, we're running into some interesting conditions around connection pooling and clean-up. We're ending up with a large pile-up of open, stale connections to the DB recurringly when the server has been running for a while. To mitigate this problem, given starlette and django run in different python threads, add a middleware that will go and call the connection clean up method in each of the threads.
2024-07-09 12:22:58 +05:30
sabaimran
609e7ee19c Fix width of side panel 2024-07-09 12:02:01 +05:30
Debanjum
0b1b262512 Add system dependencies required by RapidOCR to fix Khoj Docker image (#842)
- Issue
The Khoj docker build would fail with `ImportError: libGL.so.1: cannot open shared object file: No such file or directory`. This was required by the Khoj RapidOCR python package dependency. 

- Fix
A minimal set of system packages have been added to resolve this issue.
2024-07-08 22:16:16 +05:30
kxnarak
43413cd21f add dependencies required by the RapidOCR python package 2024-07-08 18:26:19 +05:30
sabaimran
bf4c2f219e Merge branch 'master' of github.com:khoj-ai/khoj into features/chat-ui-updates-big 2024-07-08 17:00:42 +05:30
sabaimran
037e157648 Fix a variety of links 2024-07-08 16:49:13 +05:30
sabaimran
6b80bb3f37 Add a demo for the khoj mini application, minor updates to other pages, remove out of date demos page 2024-07-08 16:33:47 +05:30
Debanjum Singh Solanky
9e31ebff93 Release Khoj version 1.16.0 2024-07-07 18:26:10 +05:30
Debanjum Singh Solanky
54132efd67 Fix Khoj Obsidian plugin build 2024-07-07 18:26:10 +05:30
Debanjum Singh Solanky
510d9b3a29 Add short keys to open chat menu, new chat, search from Obsidian pane 2024-07-07 17:57:17 +05:30
Debanjum Singh Solanky
3e0c882e27 Transcribe only when keyboard shortcut or button pressed in Obsidian
- Transcribe on holding Ctrl+s keyboard shortcut
- Transcribe on holding the transcribe button pressed via mouse too
- Make the transcribe button robust to inadvertent touches by using timeout
- Do not transcribe, trigger auto-send on silences. Silence detection
  is super rudimentary, just blocks standard emanations by whisper
  when no speech
2024-07-07 17:57:17 +05:30
sabaimran
0eb000c3ea Add health checks for the django ORM 2024-07-07 16:11:28 +05:30
sabaimran
6f8a65c529 References, mobile friendly chat sessions and file filter 2024-07-07 15:42:29 +05:30
Debanjum Singh Solanky
a31cd0dec1 Fix async batch delete of indexed entries 2024-07-06 22:45:26 +05:30
Debanjum
08b379c2ab Fix, Improve Indexing, Deleting Files (#840)
### Fix
- Fix degrade in speed when indexing large files
- Resolve org-mode indexing bug by splitting current section only once by heading
- Improve summarization by fixing formatting of text in indexed files

### Improve
- Improve scaling user, admin flows to delete all entries for a user
2024-07-06 19:52:42 +05:30
Debanjum Singh Solanky
4a471979eb Upgrade sentence-transformer package to version 3.0.1
Add einops dependency for some sentence transformer models like the
nomic-embed
2024-07-06 19:35:59 +05:30
Debanjum Singh Solanky
d693baccbc Make it optional to set the encoder, cross-encoder configs via admin UI 2024-07-06 19:35:59 +05:30
Debanjum Singh Solanky
1baebb8d0e Identify markdown headings by any whitespace character after ^#+
Previously only markdown headings with space characters after # would
be considered a heading. So ^##\t wouldn't be considered a valid heading
2024-07-06 19:35:59 +05:30
Debanjum Singh Solanky
010486fb36 Split current section once by heading to resolve org-mode indexing bug
- Split once by heading (=first_non_empty) to extract current section body
  Otherwise child headings with same prefix as current heading will
  cause the section split to go into infinite loop
- Also add check to prevent getting into recursive loop while trying
  to split entry into sub sections
2024-07-06 19:35:59 +05:30
Debanjum Singh Solanky
6a135b1ed7 Fix degrade in speed of indexing large files. Improve summarization
Adding files to the DB for summarization was slow, buggy in two ways:
- We were updating same text of modified files in DB = no of chunks
  per file times

- The `" ".join(file_content)' code was breaking each character in the
  file content by a space. This formats the original file content
  incorrectly before storing in the DB

Because this code ran in the main file indexing path, it was slowing down
file indexing. Knowledge bases with larger files were impacted more strongly
2024-07-06 19:35:59 +05:30
Debanjum Singh Solanky
e6ffb6b52c Improve scaling user flow to delete all entries
- Delete entries by batch to improve efficiency of query at scale
- Share code to delete all user entries between it's async, sync methods
- Add indicator to show when files being deleted on web config page
2024-07-06 19:35:59 +05:30
Debanjum Singh Solanky
1ab59865b5 Improve scaling admin flow to delete all entries for user 2024-07-06 19:35:59 +05:30
Debanjum
05138cbd0a Use DOM Scripting, Add CSP to Web config pages. Disable CSP in Obsidian plugin (#834)
- Add CSP to web config pages. Load phone no. validation js, css from S3
- Construct config page elements on Web via DOM scripting
- Disable CSP in Khoj Obsidian as it interferes with Obsidian functionality

- Other miscellaneous voice message level improvements (rate limit, listening animation)
2024-07-06 19:30:09 +05:30
Debanjum Singh Solanky
9bdb48807b Ratelimit text to speech model. Validate share chat url domain
- Do not log auth error message on server when Resend setup as Magic
  links for sign-in are now supported
2024-07-06 12:53:19 +05:30
Debanjum Singh Solanky
b334db0fca Add CSP to web config pages. Load phone no validation js, css from S3 2024-07-06 12:48:28 +05:30
Debanjum Singh Solanky
2f034f807a Construct config page elements on Web via DOM scripting.
Minimize isage of innerHTML to prevent DOM clobbering and unintended
escape by user Input
2024-07-06 12:48:28 +05:30
Debanjum Singh Solanky
69c9e8cc08 Disable CSP in Khoj Obsidian as it interferes with Obsidian functionality
The Khoj CSP interferes with other Obsidian features and plugins as
CSP is applied page wide.

For now chat message sanitization via Dompurify should suffice.

Enable CSP when can scope it to only the Khoj Obsidian plugin.
2024-07-05 16:10:08 +05:30
Debanjum Singh Solanky
a353d883a0 Make it optional to set the encoder, cross-encoder configs via admin UI
Upgrade sentence-transformer, add einops dependency for some sentence
transformer models like nomic
2024-07-05 16:09:30 +05:30
Debanjum Singh Solanky
6d59ad7fc9 Add listening circle animation to speak button in Obsidian plugin
Use icon active focus as color of animation button
2024-07-05 14:00:53 +05:30
sabaimran
aec44a0b89 Add dark mode toggle! And improve experience for train of thought 2024-07-04 18:29:21 +05:30
Debanjum Singh Solanky
516af86575 Fix add, remove of the text to speech loader element in Obsidian 2024-07-04 17:38:45 +05:30
sabaimran
465ef0b772 Add a loading experience when waiting for khoj response 2024-07-04 13:49:51 +05:30
Debanjum Singh Solanky
814aca6d69 Skip summarize when not triggered via slash cmd and can't summarize
Maybe better to fallback to non-summarize behavior if summarize intent
is just inferred but we can't actually summarize because the single
file added to conversation isn't satisfied
2024-07-04 13:31:00 +05:30
Debanjum
4446de00d3 Enable Voice, Keyboard Shortcuts in Khoj Obsidian Plugin (#837)
- Simplify quick jump between Khoj side pane and main editor view using keyboard shortcuts
- Enable voice chat in Obsidian to make interactions with Khoj more seamless
2024-07-04 13:28:29 +05:30
sabaimran
5ea8b16f84 Fix missing method error 2024-07-04 12:08:22 +05:30
sabaimran
d61bddf56c Fix retrieving image model by prefetching the openai config in the async method 2024-07-04 11:58:33 +05:30
sabaimran
a129b017b9 Fix image generation on server -- use default config when not set by user 2024-07-04 09:13:23 +05:30
sabaimran
34118078bf kill the emojis 2024-07-04 00:30:21 +05:30
sabaimran
d5ba916978 Working example of streaming, intersection observer, other UI updates 2024-07-04 00:30:01 +05:30
sabaimran
78d1a29bc1 Finish up filte filter side panel menu 2024-07-02 23:32:36 +05:30
sabaimran
6fa2dbc042 Do not use the custom configured max prompt size to send message to anthropic 2024-07-02 21:59:06 +05:30
sabaimran
8a6722ba97 Add basic implementation for chat side panel components 2024-07-02 21:56:43 +05:30
Debanjum Singh Solanky
afcfc60637 Merge DB migrations post merge of SD3 via API support PR 2024-07-02 17:54:58 +05:30
Debanjum
c015eeb5dd Improve Online Search: Parallelize Search, Use Jina Reader API by default (#832)
- Overview
  Khoj wil be able to do online search out of the box, even for self-hosted users
  - Default to Jina search, reader API when no Serper.dev, Olostep API keys
  - Run online searches in parallel to process multiple queries faster

- Details
  - Jina provides a [reader API](https://github.com/jina-ai/reader) for online search and web page reading
     It requires no API key. This provides a good default to enable 
     online search for self-hosted readers requiring no additional setup. 

  - Jina search API also returns webpage contents with the results, so
     just use those directly when Jina Search API used instead of
     trying to read webpages separately. The extract relevant content from
     webpage step using a chat model is still used from the
    `read_webpage_and_extract_content' func in this case.

  - Parse search results from Jina search API into same format as
     Serper.dev for accurate rendering of online references by clients

  - Run online searches in parallel with AsyncIO to process multiple queries faster
2024-07-02 17:44:51 +05:30
Debanjum
826c3dc9cc Enable using Stable Diffusion 3 for Image Generation via API (#830)
- Support Stable Diffusion 3 via API
  Server Admin needs to setup model similar to DALLE-3 via Django Admin Panel
- Use shorter prompt generator to prompt SD3 to create better images
- Allow users to set paint model to use from web client config page
2024-07-02 17:28:50 +05:30
Debanjum Singh Solanky
d5ceff2691 Update tests and documentation with Jina reader API usage and info
Update offline, openai chat actor, director tests to not require
Serper to run the online command tests

Update documentation for self-hosted online search to mention no setup
is required by default. But improvements can be made by using
Serper.dev or Olostep
2024-07-02 17:19:09 +05:30
Debanjum Singh Solanky
553beae848 No need to set OpenAI API key from environment variable explicitly
It is unnecessary as the OpenAI client automatically tries to use API
key from OPENAI_API_KEY env var when the api_key field is unset
2024-07-02 17:19:09 +05:30
Debanjum Singh Solanky
a038e4911b Default to Jina search, reader API when no Serper.dev, Olostep API keys
Jina AI provides a search and webpage reader API that doesn't require
an API key. This provides a good default to enable online search for
self-hosted readers requiring no additional setup.

Jina search API also returns webpage contents with the results, so
just use those directly when Jina Search API used instead of
trying to read webpages separately. The extract relvant content from
webpage step using a chat model is still used from the
`read_webpage_and_extract_content' func in this case.

Parse search results from Jina search API into same format as
Serper.dev for accurate rendering of online references by clients
2024-07-02 17:19:08 +05:30
Debanjum Singh Solanky
ff44734774 Run online searches in parallel to process multiple queries faster 2024-07-02 17:19:08 +05:30
sabaimran
0ee7cc8c47 Change overall architecure of how information is flowing for better statefulness 2024-07-02 12:39:54 +05:30
sabaimran
541ce04ebc Checkpoint: Updated sidebar panel with new components
- Add non-functional UI elements for chat, references, feedback buttons, rename/share session, mic, attachment, websocket connection
2024-07-02 11:18:50 +05:30
Raghav Tirumale
8eccd8a5e4 Support Indexing Images via OCR (#823)
- Added support for uploading .jpeg, .jpg, and .png files to Khoj from Web, Desktop app
- Updating indexer to generate raw text and entries using RapidOCR
- Details
  * added support for indexing images via ocr
  * fixed pyproject.toml
  * Update src/khoj/processor/content/images/image_to_entries.py
     Co-authored-by: Debanjum <debanjum@gmail.com>
  * Update src/khoj/processor/content/images/image_to_entries.py
     Co-authored-by: Debanjum <debanjum@gmail.com>
  * removed redudant try except blocks
  * updated desktop js file to support image formats
  * added tests for jpg and png
  * Fix processing for image to entries files
  * Update unit tests with working image indexer
  * Change png test from version verificaition to open-cv verification

---------

Co-authored-by: Debanjum <debanjum@gmail.com>
Co-authored-by: sabaimran <narmiabas@gmail.com>
2024-07-01 06:00:00 -07:00
Debanjum Singh Solanky
cffc14a46a Trigger voice chat via keyboard shortcut in Khoj side pane
Quickly trigger voice chat from Khoj side pane using Keyboard shortcuts
2024-07-01 18:06:09 +05:30
Debanjum Singh Solanky
3723904512 Toggle jump between Khoj side pane & previous editor via cmd, kbd shortcut
Improve quick navigation to, from Khoj side pane using Keyboard
shortcut or Obsidian command
2024-07-01 18:05:59 +05:30
Debanjum Singh Solanky
fbb95ca342 Put cursor on chat input when focus on chat view in Obsidian
This should improve fluidity of keyboard interactions with Khoj on
Obsidian.

Open Khoj chat view via keybinding or command pallete and ask
question using only the keyboard, with no mouse clicks required
2024-07-01 18:05:55 +05:30
Debanjum Singh Solanky
093e276908 Enable Voice chat in Khoj Obsidian plugin
- Automatically carry out voice chats with Khoj from within Obsidian
  When send voice message, Khoj will auto respond with voice as well
- Listen to past Khoj messages as speech
- Add circular loading spinner to use while message is being converted
  to speech
2024-07-01 18:02:28 +05:30
sabaimran
c83b8f2768 Allow just one worker to be the background schedule leader (#836)
* Add a leader election mechanism to circumvent runtime issues for multiple schedulers

- Reduce the load on the DB and risk of issues on the service side by limiting the execution environment to one elected leader at a given time. This one is responsible for managing all of the execution of the jobs, though all workers are capable of adding and removing jobs

* Set a max duration for the schedule leader position (12 hrs), add some error if automation not added successfully
2024-06-28 13:13:25 +05:30
sabaimran
80fe5ce182 Fix user not authenticated interpretation error 2024-06-27 21:13:54 +05:30
Raghav Tirumale
24a0d8b073 Add OS Level Shortcut Window for Quick Access to Khoj Desktop (#815)
* rough sketch of desktop shortcuts. many bugs to fix still

* working MVP of desktop shortcut khoj

* UI fixes

* UI improvements for editable shortcut message

* major rendering fix to prevent clipboard text from getting lost

* UI improvements and bug fixes

* UI upgrades: custom top bar, edit sent message and color matching

* removed debug javascript file

* font reverted to Noto Sans

* cleaning up the code and removing diffs

* UX fixes

* cleaning up unused methods from html

* front end for button to send user back to main window to continue conversation

* UX fix for window and continue conversation support added

* migrated common js functions into chatutils.js

* Fix window closing issue in macos by

1. Use a helper function to determine if the window is open by seeing if there's a browser window with shortcut.html loaded
2. Use the  event listener on the window to handle teardown

* removed extra comment and renamed continue convo button

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
2024-06-27 07:20:13 -07:00
sabaimran
870d9ecdbf Add a fact checker feature with updated styling (#835)
- Add an experimental feature used for fact-checking falsifiable statements with customizable models. See attached screenshot for example. Once you input a statement that needs to be fact-checked, Khoj goes on a research spree to verify or refute it.
- Integrate frontend libraries for [Tailwind](https://tailwindcss.com/) and [ShadCN](https://ui.shadcn.com/) for easier UI development. Update corresponding styling for some existing UI components. 
- Add component for model selection 
- Add backend support for sharing arbitrary packets of data that will be consumed by specific front-end views in shareable scenarios
2024-06-27 18:45:38 +05:30
sabaimran
3b7a9358c3 Add our first view via Next.js for Agents (#817)
Initialize our migration to use Next.js for front-end views via Agents. This includes setup for getting authenticated users, reading in available agents, setting up a pop-up modal when you're clicking on an agent, and allowing users to start new conversations with agents.

Best attempt at an in-place migration, though there are some noticeable differences.

Also adds view for chat that are not being used, but in experimental phase.
2024-06-27 13:56:16 +05:30
Debanjum Singh Solanky
afbeee9e82 Rename copy-button to more general chat-action-button in Obsidian client
- Use 4 space indent of activateView function in pane_view component
2024-06-26 18:09:23 +05:30
sabaimran
8c12a69570 Fix issue in anthropic chat when khoj message becomes top message
This is because Anthropic requires the first message in the chat history to be from the user.
2024-06-26 12:59:34 +05:30
Debanjum Singh Solanky
4f89319b40 Release Khoj version 1.15.0 2024-06-26 10:38:16 +05:30
Debanjum Singh Solanky
bbfd320ed4 Use Yarn instead of NPM to bump Desktop, Obsidian client versions 2024-06-26 10:37:58 +05:30
Debanjum Singh Solanky
c793d8a69e Add Validation logic to save PaintModel. Use API key from Paint Model
Rename Paint Model, Adapters to TextToImage for consistency
2024-06-26 10:16:26 +05:30
Debanjum Singh Solanky
1acf969c6e Do not require OpenAI to generate image as local chat + sd3 works now
Previously the text_to_image helper would only trigger the image
generation flow if OpenAI client was setup. This is not required
anymore as offline chat model + sd3 API works. So remove that check
2024-06-26 10:16:26 +05:30
Debanjum Singh Solanky
2c4bf91a61 Allow user to set paint model to use from web client config page 2024-06-26 10:16:26 +05:30
Debanjum Singh Solanky
eb09aba747 Remove quotes wrapping the prompt from being passed to image gen model 2024-06-26 10:16:26 +05:30
Debanjum Singh Solanky
fdd4c02461 Use shorter prompt generator to prompt SD3 to create better images 2024-06-26 10:16:26 +05:30
Debanjum Singh Solanky
eda33e092f Enable using Stable Diffusion 3 for Image Generation via API 2024-06-26 10:16:26 +05:30
Debanjum
a25689fabf Use user theme in Obsidian for Khoj plugin styling (#825)
Makes the Khoj chat in the Obsidian plugin adapt better to the user theme, making it feel more seamless, and helps with dark mode compatibility
2024-06-26 10:14:17 +05:30
Debanjum Singh Solanky
cfe46fd9f5 Add Border Color instead of BG Color for Chat Message in Obsidian 2024-06-26 08:11:04 +05:30
sabaimran
fb818ead60 Use active bg instead of code background for khoj response 2024-06-26 08:05:13 +05:30
sabaimran
a4b2552540 Update conversation session selection menu to use Obsidian theme colors as well 2024-06-26 08:05:13 +05:30
sabaimran
da5b07e913 Remove custom styling on the reference buttons 2024-06-26 08:05:13 +05:30
sabaimran
c4a1ae9375 Make the Khoj Obsidian plugin more user theme friendly
Use the CSS variables from the theme for the Khoj UI components
2024-06-26 08:04:17 +05:30
Debanjum Singh Solanky
d6fe5d9a63 Pass current component as arg to markdown renderer in chat view
This doesn't work on search modal, but hopefully will get resolved
once we migrate search into a view from a modal
2024-06-24 16:12:20 +05:30
Debanjum Singh Solanky
0d04018622 Install pydantic with optional email validator package
Otherwise Khoj fails on startup. Not sure why, must be new changes to
pydantic?
2024-06-24 16:12:20 +05:30
Debanjum Singh Solanky
6f280b1ccc Split setup of specific OpenAI API proxies into separate doc pages 2024-06-24 16:12:20 +05:30
Debanjum Singh Solanky
68e7c297e0 Add Advanced Self Hosting Section, Improve Self Hosting, OpenAI Proxy Docs
- Add instructions for self-hosted users with info, warning boxes to
  avoid, fix common issues when setting up Khoj server
- Create new Advanced Self Hosting section
  - Extract Advanced Self-Hosting Sections from the Advanced Page and
    move them to separate Pages under Advanced Self Hosting section
- Improve OpenAI Proxy Docs
  - Put Ollama setup as a section under OpenAI API Proxy page instead
    of a separate page
  - Add Section to use Khoj with chat model from LM Studio
  - Update LiteLLM docs to use chat model from LM Studio
2024-06-24 16:12:20 +05:30
Debanjum Singh Solanky
732332a3c5 Spell fix s/e.g/e.g./ across code, tests and docs 2024-06-24 15:24:45 +05:30
Debanjum Singh Solanky
8fc7f980aa Revert KHOJ_DOMAIN to only support single domain.
Multiple domain support didn't generalize to other portions where it
is used
2024-06-24 15:24:45 +05:30
sabaimran
4110e71e84 Add info in the documentation about text to speech 2024-06-24 12:46:33 +05:30
sabaimran
939811e9b5 Fix conversation look up logic 2024-06-24 09:10:03 +05:30
Debanjum Singh Solanky
a4d88612c1 Just use yarn for package version locking. Remove npm package lock 2024-06-23 16:06:20 +05:30
Debanjum Singh Solanky
55be90cdd2 Sanitize user input fields on Automations page of web client
Use Dompurify to sanitize user input
2024-06-23 14:14:47 +05:30
Debanjum Singh Solanky
1c7a562880 Generate automation cards via DOM scripting 2024-06-23 13:22:38 +05:30
Debanjum Singh Solanky
57a36967bf Run Obsidian version script in bump_version.sh to write to versions.json
This handles updates from manifest.json minAppVersion field to the
versions.json file.

The minAppVersion field is for the minimum Obsidian app version
supported by a Khoj plugin version
2024-06-23 08:18:55 +05:30
Debanjum Singh Solanky
c7c32a7467 Improve online chat reference extraction in Khoj.el Emacs package
- Handle online references with no title
- Improve handling references which are arrays instead of lists
2024-06-23 08:13:36 +05:30
Debanjum Singh Solanky
9d33d8c0fa Upgrade typescript eslint dev dependency of Khoj Obsidian plugin 2024-06-23 07:36:49 +05:30
Debanjum
a94062469a Automatically Find Similar Notes on Emacs in Background (#827)
Khoj will find and display notes similar to the current entry in the side pane when
1. find similar is open in side pane and
2. cursor has moved to a new entry

### Major
- Find similar notes to current note at cursor automatically in background
- Only show headings of search result and increase default results count

### Minor
- Pass absolute path of file to index from khoj.el emacs client
- Update help message to only show the smaller set of new keybindings
- Fix edge cases in loading some chat sessions
2024-06-23 07:36:11 +05:30
sabaimran
38090b2553 In dockerize.yml file, revert the added configuration 2024-06-22 21:11:25 +05:30
sabaimran
a53178cab9 Add developer support for using next.js to serve generated static files (#814)
To improve the developer experience for front-end development, we're migrating to Next.js. In order to do this migration page-by-page, we're using static site generation via Next.js. This also helps us avoid making cross site requests from front-end to back-end for the time being, while giving a ramp to separating out server and client if needed for scale down the road.

Dev instructions for using the next.js setup are in the added README.

This adds scaffolding for including the built files in the python package as well as the docker images. Docker setup has been tested locally. In order to verify the build is working as expected, we can navigate to the {khoj_host}:42110/experimental and verify that the experiment page comes up.

This setup works with serving static files included in the src/interface/web folder from the Django app. The key bit for understanding the setup is in the yarn export command in package.json.
2024-06-22 20:12:41 +05:30
Debanjum Singh Solanky
59edb99f04 Simplify, improve bump version development script
- Just use in-built `npm version' command to update desktop, obsidian version
- Upgrade by major, minor or patch version using new -t flag in script
  E.g bump_version -t minor
2024-06-22 18:19:38 +05:30
Debanjum Singh Solanky
abd6f58aee Upgrade Desktop app package dependencies 2024-06-22 17:38:52 +05:30
Debanjum Singh Solanky
f413dc62cd Upgrade Obsidian plugin dependencies. Add package lock file for it
Add it to bump_version script as well.
2024-06-22 17:38:52 +05:30
Debanjum Singh Solanky
1d7d51a7ab Upgrade Documentation packages 2024-06-22 17:38:48 +05:30
Debanjum Singh Solanky
22f6db0a6b Upgrade RapidOCR and enable for Python 3.12. Fix PDF OCR test 2024-06-22 16:01:55 +05:30
Debanjum Singh Solanky
55a23eae25 Upgrade pillow to fix pytest workflow failure 2024-06-22 15:17:43 +05:30
Debanjum Singh Solanky
7e277e9381 Fix getting file-toggle-button element in chat of web app 2024-06-21 15:54:38 +05:30
Debanjum Singh Solanky
fa7b40ab86 Automatically respond with Voice if subscribed user sent Voice message 2024-06-21 15:53:01 +05:30
Debanjum Singh Solanky
5e5fe4b7af Improve font size, spacing of conversation session on desktop app 2024-06-21 12:25:35 +05:30
sabaimran
d3c0111121 Include base URL when using openai api config in extract questions. Close #831 2024-06-21 12:18:50 +05:30
sabaimran
b9966eb3d4 Add support for text to speech in chat responses (#821)
* Enable speech to text responses in khoj chat

- Current issue: reads out all the markdown formatting, plus waits for the whole result to be streamed before playing it

* Extract content from markdown-formatted text

* Add a loader for while you're waiting for Khoj's response

* Add user configuration option for chat model options, allow server side configuration for option list

* Join up APIs, views, admin pages to allow configuring custom voice models
2024-06-21 11:30:28 +05:30
Debanjum Singh Solanky
427575e958 Improve khoj chat new, delete session flows
When create new conversation session, automatically request query. As
that is expected next action after creating new session

Pass session-id to khoj-chat to allow reuse from
create-new-conversation func

When delete conversation session, do not call load chat session.
Unnecessary action.

Use thread-last to improve code flow in new, delete conversation funcs
2024-06-21 10:54:59 +05:30
Debanjum Singh Solanky
59032a06d5 Improve defaults when extracting fields from online reference in khoj.el 2024-06-21 10:54:59 +05:30
Debanjum Singh Solanky
9262aea7a5 Fix comments, func calls based on melpazoid, checkdoc, package-lint 2024-06-21 10:54:59 +05:30
sabaimran
ff26b19d2b Add a migration for allowing the docx field in the entries file type 2024-06-21 09:47:49 +05:30
sabaimran
3cfe5aabe5 Add support for magic link email sign-in (#820)
* Add magic link email sign-in option

* Adding backend routes and model changes to keep state of email verification code and status

* Test and fix end to end email verification flow

* Add documentation for how to use the magic link sign-in when self-hosting Khoj

* Add magic link sign in to public conversation page
2024-06-20 13:32:58 +05:30
Debanjum Singh Solanky
0afe66ac39 Restore cursor to original window after opening Khoj side pane
Previously the cursor would move to the Khoj side pane on opening it.
This would break user's flow, especially when find similar triggers
automatically

New behavior maintains smoother update of auto find similar without
disrupting user browsing
2024-06-20 12:50:13 +05:30
Debanjum Singh Solanky
afe91a2633 Only show headings of search result and increase total count returned
Previously it would show complete result body this would make the
result width variable and hard to track all the returned results

Showing just heading makes it easier to track
2024-06-20 12:50:13 +05:30
Debanjum Singh Solanky
2b12a5514e Find similar notes to current note at cursor automatically in background
- Call find similar on current element if point has moved to new
  element
- Delete the first result from find-similar search results as that'll
  be the current note (which is trivially most similar to itself)
- Determine find-similar based text formating at the rendering layer
  rather than at the top level find-similar func
2024-06-20 12:50:13 +05:30
Raghav Tirumale
093eb473cb Add Documentation for the /summarize Command (#822)
* added documentation for the /summarize command

* Add a hint for natural language usage

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
2024-06-20 12:08:01 +05:30
Raghav Tirumale
bd3b590153 Support Indexing Docx Files (#801)
* Add support for indexing docx files and associated unit tests

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
2024-06-20 11:18:01 +05:30
Debanjum Singh Solanky
d042e073cc Pass absolute path of file to index from khoj.el emacs client 2024-06-20 00:26:18 +05:30
Debanjum Singh Solanky
d23f2849d4 Update help message to only show the smaller set of new keybindings 2024-06-20 00:26:18 +05:30
Raghav Tirumale
d4e5c95711 Add Ability to Summarize Documents (#800)
* Uses entire file text and summarizer model to generate document summary.
* Uses the contents of the user's query to create a tailored summary.
* Integrates with File Filters #788 for a better UX.
2024-06-18 19:31:07 +05:30
346 changed files with 29959 additions and 30539 deletions

View File

@@ -8,6 +8,7 @@ on:
- master
paths:
- src/khoj/**
- src/interface/web/**
- pyproject.toml
- Dockerfile
- prod.Dockerfile
@@ -30,6 +31,7 @@ on:
env:
# Tag Image with tag name on release
# else with user specified tag (default 'dev') if triggered via workflow
# else with run_id if triggered via a pull request
# else with 'pre' (if push to master)
DOCKER_IMAGE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || github.event_name == 'workflow_dispatch' && github.event.inputs.tag || 'pre' }}

View File

@@ -8,6 +8,7 @@ on:
- 'master'
paths:
- src/khoj/**
- src/interface/web/**
- pyproject.toml
- .github/workflows/pypi.yml
pull_request:
@@ -15,8 +16,10 @@ on:
- 'master'
paths:
- src/khoj/**
- src/interface/web/**
- pyproject.toml
- .github/workflows/pypi.yml
workflow_dispatch:
jobs:
publish:
@@ -25,7 +28,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -34,9 +37,20 @@ jobs:
with:
python-version: '3.11'
- name: ⬇️ Install Application
- name: ⬇️ Install Server
run: python -m pip install --upgrade pip && pip install --upgrade .
- name: ⬇️ Install Web Client
run: |
yarn install
yarn pypiciexport
working-directory: src/interface/web
- name: 📂 Copy Generated Files
run: |
mkdir -p src/khoj/interface/compiled
cp -r /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/khoj/interface/compiled/* src/khoj/interface/compiled/
- name: ⚙️ Build Python Package
run: |
# Setup Environment for Reproducible Builds
@@ -44,7 +58,7 @@ jobs:
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
rm -rf dist
# Build PyPi Package
# Build PyPI Package
pipx run build
- name: 🌡️ Validate Python Package
@@ -54,11 +68,13 @@ jobs:
pipx run twine check dist/*
- name: ⏫ Upload Python Package Artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: khoj-assistant
path: dist/*.whl
name: khoj
path: dist/khoj-*.whl
- name: 📦 Publish Python Package to PyPI
if: startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/master'
uses: pypa/gh-action-pypi-publish@v1.8.14
with:
skip-existing: true

View File

@@ -29,7 +29,6 @@ jobs:
fail-fast: false
matrix:
python_version:
- '3.9'
- '3.10'
- '3.11'
- '3.12'

6
.gitignore vendored
View File

@@ -16,13 +16,15 @@ todesktop.json
# Build artifacts
/src/khoj/interface/web/images
/src/khoj/interface/built/
/src/khoj/interface/compiled/404.html
/build/
/dist/
khoj_assistant.egg-info
/config/khoj*.yml
.pytest_cache
*.log
static
/src/khoj/static
# Obsidian plugin artifacts
# ---
@@ -35,6 +37,8 @@ src/interface/obsidian/main.js
# Exclude sourcemaps
*.map
# IntelliJ
.idea
# obsidian
data.json

View File

@@ -1,13 +1,24 @@
# syntax=docker/dockerfile:1
FROM ubuntu:jammy
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
LABEL homepage="https://khoj.dev"
LABEL repository="https://github.com/khoj-ai/khoj"
LABEL org.opencontainers.image.source="https://github.com/khoj-ai/khoj"
# Install System Dependencies
RUN apt update -y && apt -y install python3-pip swig
RUN apt update -y && apt -y install python3-pip swig curl
WORKDIR /app
# Install Node.js and Yarn
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
RUN apt -y install nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt update && apt -y install yarn
# Install RapidOCR dependencies
RUN apt -y install libgl1 libgl1-mesa-glx libglib2.0-0
# Install Application
WORKDIR /app
COPY pyproject.toml .
COPY README.md .
ARG VERSION=0.0.0
@@ -20,6 +31,11 @@ COPY . .
# Set the PYTHONPATH environment variable in order for it to find the Django app.
ENV PYTHONPATH=/app/src:$PYTHONPATH
# Go to the directory src/interface/web and export the built Next.js assets
WORKDIR /app/src/interface/web
RUN bash -c "yarn cache clean && yarn install --verbose && yarn ciexport"
WORKDIR /app
# Run the Application
# There are more arguments required for the application to run,
# but these should be passed in through the docker-compose.yml file.

View File

@@ -4,7 +4,7 @@
[![test](https://github.com/khoj-ai/khoj/actions/workflows/test.yml/badge.svg)](https://github.com/khoj-ai/khoj/actions/workflows/test.yml)
[![dockerize](https://github.com/khoj-ai/khoj/actions/workflows/dockerize.yml/badge.svg)](https://github.com/khoj-ai/khoj/pkgs/container/khoj)
[![pypi](https://github.com/khoj-ai/khoj/actions/workflows/pypi.yml/badge.svg)](https://pypi.org/project/khoj-assistant/)
[![pypi](https://github.com/khoj-ai/khoj/actions/workflows/pypi.yml/badge.svg)](https://pypi.org/project/khoj/)
![Discord](https://img.shields.io/discord/1112065956647284756?style=plastic&label=discord)
</div>

View File

@@ -45,7 +45,8 @@ services:
- KHOJ_DEBUG=False
- KHOJ_ADMIN_EMAIL=username@example.com
- KHOJ_ADMIN_PASSWORD=password
# Uncomment the following lines to make your instance publicly accessible. Replace the domain with your domain. Proceed with caution, especially if you are using anonymous mode.
# Uncomment the following lines to make your instance publicly accessible.
# Replace the domain with your domain. Proceed with caution, especially if you are using anonymous mode.
# - KHOJ_NO_HTTPS=True
# - KHOJ_DOMAIN=192.168.0.104
command: --host="0.0.0.0" --port=42110 -vv --anonymous-mode

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 170 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,8 @@
{
"label": "Advanced Self Hosting",
"position": 6,
"link": {
"type": "generated-index",
"description": "Advanced setup for Self Hosting Khoj server"
}
}

View File

@@ -0,0 +1,52 @@
# Authenticate
:::info
This is only helpful for self-hosted users or teams. If you're using [Khoj Cloud](https://app.khoj.dev), both Magic Links and Google OAuth work.
:::
By default, most of the instructions for self-hosting Khoj assume a single user, and so the default configuration is to run in anonymous mode. However, if you want to enable authentication, you can do so either with with [Magic Links](#using-magic-links) or [Google OAuth](#using-google-oauth) as shown below. This can be helpful to make Khoj securely accessible to you and your team.
:::tip[Note]
Remove the `--anonymous-mode` flag in your start up command to enable authentication.
:::
## Using Magic Links
The most secure way to do this is to integrate with [Resend](https://resend.com) by setting up an account and adding an environment variable for `RESEND_API_KEY`. You can get your API key [here](https://resend.com/api-keys). This will allow you to automatically send sign-in links to users who want to log in.
It's still possible to use the magic links feature without Resend, but you'll need to manually send the magic links to users who want to log in.
## Manually sending magic links
1. The user will have to enter their email address in the login form.
They'll click `Send Magic Link`. Without the Resend API key, this will just create an unverified account for them in the backend
<img src="/img/magic_link.png" alt="Magic link login form" width="400"/>
2. You can get their magic link using the admin panel
Go to the [admin panel](http://localhost:42110/server/admin/database/khojuser/). You'll see a list of users. Search for the user you want to send a magic link to. Tick the checkbox next to their row, and use the action drop down at the top to 'Get email login URL'. This will generate a magic link that you can send to the user, which will appear at the top of the admin interface.
| Get email login URL | Retrieved login URL |
|---------------------|---------------------|
| <img src="/img/admin_get_emali_login.png" alt="Get user magic sign in link" width="400" />| <img src="/img/admin_successful_login_url.png" alt="Successfully retrieved a login URL" width="400" />|
3. Send the magic link to the user. They can click on it to log in.
Once they click on the link, they'll automatically be logged in. They'll have to repeat this process for every new device they want to log in from, but they shouldn't have to repeat it on the same device.
A given magic link can only be used once. If the user tries to use it again, they'll be redirected to the login page to get a new magic link.
## Using Google OAuth
To set up your self-hosted Khoj with Google Auth, you need to create a project in the Google Cloud Console and enable the Google Auth API.
To implement this, you'll need to:
1. You must use the `python` package or build from source, because you'll need to install additional packages for the google auth libraries (`prod`). The syntax to install the right packages is
```
pip install khoj[prod]
```
2. [Create authorization credentials](https://developers.google.com/identity/sign-in/web/sign-in) for your application.
3. Open your [Google cloud console](https://console.developers.google.com/apis/credentials) and create a configuration like below for the relevant `OAuth 2.0 Client IDs` project:
![Google auth login project settings](https://github.com/khoj-ai/khoj/assets/65192171/9bcbf6f4-197d-4d0c-973a-c10b1331c892)
4. Configure these environment variables: `GOOGLE_CLIENT_SECRET`, and `GOOGLE_CLIENT_ID`. You can find these values in the Google cloud console, in the same place where you configured the authorized origins and redirect URIs.
That's it! That should be all you have to do. Now, when you reload Khoj without `--anonymous-mode`, you should be able to use your Google account to sign in.

View File

@@ -0,0 +1,37 @@
# LiteLLM
:::info
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
:::
:::info
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
:::
[LiteLLM](https://docs.litellm.ai/docs/proxy/quick_start) exposes an OpenAI compatible API that proxies requests to other LLM API services. This provides a standardized API to interact with both open-source and commercial LLMs.
Using LiteLLM with Khoj makes it possible to turn any LLM behind an API into your personal AI agent.
## Setup
1. Install LiteLLM
```bash
pip install litellm[proxy]
```
2. Start LiteLLM and use Mistral tiny via Mistral API
```
export MISTRAL_API_KEY=<MISTRAL_API_KEY>
litellm --model mistral/mistral-tiny --drop_params
```
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
- Name: `proxy-name`
- Api Key: `any string`
- Api Base Url: **URL of your Openai Proxy API**
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
- Name: `llama3.1` (replace with the name of your local model)
- Model Type: `Openai`
- Openai Config: `<the proxy config you created in step 3>`
- Max prompt size: `20000` (replace with the max prompt size of your model)
- Tokenizer: *Do not set for OpenAI, Mistral, Llama3 based models*
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View File

@@ -0,0 +1,30 @@
# LM Studio
:::info
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
:::
:::info
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
:::
[LM Studio](https://lmstudio.ai/) is a desktop app to chat with open-source LLMs on your local machine. LM Studio provides a neat interface for folks comfortable with a GUI.
LM Studio can expose an [OpenAI API compatible server](https://lmstudio.ai/docs/local-server). This makes it possible to turn chat models from LM Studio into your personal AI agents with Khoj.
## Setup
1. Install [LM Studio](https://lmstudio.ai/) and download your preferred Chat Model
2. Go to the Server Tab on LM Studio, Select your preferred Chat Model and Click the green Start Server button
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
- Name: `proxy-name`
- Api Key: `any string`
- Api Base Url: `http://localhost:1234/v1/` (default for LMStudio)
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
- Name: `llama3.1` (replace with the name of your local model)
- Model Type: `Openai`
- Openai Config: `<the proxy config you created in step 3>`
- Max prompt size: `20000` (replace with the max prompt size of your model)
- Tokenizer: *Do not set for OpenAI, mistral, llama3 based models*
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View File

@@ -0,0 +1,36 @@
# Ollama
:::info
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
:::
:::info
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
:::
Ollama allows you to run [many popular open-source LLMs](https://ollama.com/library) locally from your terminal.
For folks comfortable with the terminal, Ollama's terminal based flows can ease setup and management of chat models.
Ollama exposes a local [OpenAI API compatible server](https://github.com/ollama/ollama/blob/main/docs/openai.md#models). This makes it possible to use chat models from Ollama to create your personal AI agents with Khoj.
## Setup
1. Setup Ollama: https://ollama.com/
2. Start your preferred model with Ollama. For example,
```bash
ollama run llama3.1
```
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
- Name: `ollama`
- Api Key: `any string`
- Api Base Url: `http://localhost:11434/v1/` (default for Ollama)
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
- Name: `llama3.1` (replace with the name of your local model)
- Model Type: `Openai`
- Openai Config: `<the ollama config you created in step 3>`
- Max prompt size: `20000` (replace with the max prompt size of your model)
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model.

View File

@@ -0,0 +1,17 @@
# Support Multilingual Docs
Khoj uses an embedding model to understand documents. Multilingual embedding models improve the search quality for documents not in English. This affects both search and chat with docs experiences across Khoj.
To improve search and chat quality for non-english documents you can use a [multilingual model](https://www.sbert.net/docs/pretrained_models.html#multi-lingual-models).<br />
For example, the [paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) supports [50+ languages](https://www.sbert.net/docs/pretrained_models.html#:~:text=we%20used%20the%20following%2050%2B%20languages), has decent search quality and speed for a consumer machine.
To use it:
1. Open [the search config](http://localhost:42110/server/admin/database/searchmodelconfig/) on your server's admin settings page. Either create a new search model, if none exists, or update the existing one. For example,
- Set the `bi_encoder` field to `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`
- Set the `cross_encoder` field to `mixedbread-ai/mxbai-rerank-xsmall-v1`
2. Regenerate your content index from all the relevant clients. This step is very important, as you'll need to re-encode all your content with the new model.
:::info[Note]
Modern search/embedding model like [mixedbread-ai/mxbai-embed-large-v1](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1) expect a prefix to the query (or docs) string to improve encoding. Update the `bi_encoder_query_encode_config` field of your [embedding model](http://localhost:42110/server/admin/database/searchmodelconfig/) with `{prompt: <prefix-prompt>}` to improve the search quality of these models.
E.g. `{prompt: "Represent this query for searching documents"}`. You can pass any valid JSON object that the SentenceTransformer `encode` function accepts
:::

View File

@@ -0,0 +1,37 @@
---
sidebar_position: 1
---
# Use OpenAI Proxy
:::info
This is only helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
:::
:::info
Khoj natively supports local LLMs [available on HuggingFace in GGUF format](https://huggingface.co/models?library=gguf). Using an OpenAI API proxy with Khoj maybe useful for ease of setup, trying new models or using commercial LLMs via API.
:::
Khoj can use any OpenAI API compatible server including [Ollama](/advanced/ollama), [LMStudio](/advanced/lmstudio) and [LiteLLM](/advanced/litellm).
Configuring this allows you to use non-standard, open or commercial, local or hosted LLM models for Khoj
Combine them with Khoj can turn your favorite LLM into an AI agent. Allowing you to chat with your docs, find answers from the internet, build custom agents and run automations.
For specific integrations, see our [Ollama](/advanced/ollama), [LMStudio](/advanced/lmstudio) and [LiteLLM](/advanced/litellm) setup docs. For general instructions to setup Khoj with an OpenAI API proxy see below.
## General Setup
1. Start your preferred OpenAI API compatible app
3. Create a new [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/add) on your Khoj admin panel
- Name: `proxy-name`
- Api Key: `any string`
- Api Base Url: **URL of your Openai Proxy API**
4. Create a new [Chat Model Option](http://localhost:42110/server/admin/database/chatmodeloptions/add) on your Khoj admin panel.
- Name: `llama3` (replace with the name of your local model)
- Model Type: `Openai`
- Openai Config: `<the proxy config you created in step 3>`
- Max prompt size: `2000` (replace with the max prompt size of your model)
- Tokenizer: *Do not set for OpenAI, mistral, llama3 based models*
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View File

@@ -7,14 +7,15 @@ sidebar_position: 1
> Query your Second Brain from your machine
Use the Desktop app to chat and search with Khoj.
You can also sync any relevant files with Khoj using the app.
Khoj will use these files to provide contextual responses when you search or chat.
You can also share your files, folders with Khoj using the app.
Khoj will keep these files in sync to provide contextual responses when you search or chat.
## Features
- **Chat**
- **Faster answers**: Find answers quickly, from your private notes or the public internet
- **Assisted creativity**: Smoothly weave across retrieving answers and generating content
- **Iterative discovery**: Iteratively explore and re-discover your notes
- **Quick access**: Use [Khoj Mini](/features/khoj_mini) on the desktop to quickly pull up a mini chat module for quicker answers
- **Search**
- **Natural**: Advanced natural language understanding using Transformer based ML Models
- **Incremental**: Incremental search for a fast, search-as-you-type experience
@@ -22,9 +23,10 @@ Khoj will use these files to provide contextual responses when you search or cha
## Setup
1. Install the [Khoj Desktop app](https://khoj.dev/downloads) for your OS
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
3. Set your Khoj API Key on the *Settings* page of the Khoj Desktop app
4. [Optional] Add any files, folders you'd like Khoj to be aware of on the *Settings* page and Click *Save*
These files and folders will be automatically kept in sync for you
## Interface
| Chat | Search |

View File

@@ -30,7 +30,7 @@ sidebar_position: 2
| ![khoj search on emacs](/img/khoj_search_on_emacs.png) | ![khoj chat on emacs](/img/khoj_chat_on_emacs.png) |
## Setup
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
2. Add below snippet to your Emacs config file, usually at `~/.emacs.d/init.el`
@@ -90,13 +90,13 @@ M-x package-install khoj
See [Khoj Search](/features/search) for details
1. Hit `C-c s s` (or `M-x khoj RET s`) to open khoj search
2. Enter your query in natural language<br/>
E.g *"What is the meaning of life?"*, *"My life goals for 2023"*
E.g. *"What is the meaning of life?"*, *"My life goals for 2023"*
### Chat
See [Khoj Chat](/features/chat) for details
1. Hit `C-c s c` (or `M-x khoj RET c`) to open khoj chat
2. Ask questions in a natural, conversational style<br/>
E.g *"When did I file my taxes last year?"*
E.g. *"When did I file my taxes last year?"*
### Find Similar Entries
This feature finds entries similar to the one you are currently on.
@@ -105,7 +105,7 @@ This feature finds entries similar to the one you are currently on.
### Advanced Usage
- Add [query filters](https://github.com/khoj-ai/khoj/#query-filters) during search to narrow down results further
e.g `What is the meaning of life? -"god" +"none" dt>"last week"`
e.g. `What is the meaning of life? -"god" +"none" dt>"last week"`
- Use `C-c C-o 2` to open the current result at cursor in its source org file
- This calls `M-x org-open-at-point` on the current entry and opens the second link in the entry.

View File

@@ -23,7 +23,7 @@ sidebar_position: 3
1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel
2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
4. Set your Khoj API Key in the Khoj plugin settings in Obsidian
See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for more details on installing Obsidian plugins.
@@ -31,7 +31,7 @@ See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsid
## Use
### Chat
Click the *Khoj chat* icon 💬 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Chat* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette) and ask questions in a natural, conversational style.<br />
E.g *"When did I file my taxes last year?"*
E.g. *"When did I file my taxes last year?"*
See [Khoj Chat](/features/chat) for more details

View File

@@ -6,14 +6,16 @@ sidebar_position: 5
> Query your Second Brain from WhatsApp
Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan [this QR code](https://khoj.dev/whatsapp) on your phone to chat with Khoj on WhatsApp.
Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan the QQ code below on your phone to chat with Khoj on WhatsApp.
Without any desktop clients, you can start chatting with Khoj on WhatsApp. Bear in mind you do need one of the desktop clients in order to share and sync your data with Khoj. The WhatsApp AI bot will work right away for answering generic queries and using Khoj in default mode.
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/config).
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/settings).
If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://khoj.dev/pricing) on Khoj Cloud.
<img src="https://khoj-web-bucket.s3.amazonaws.com/khojwhatsapp.png" alt="WhatsApp QR Code" width="300" height="300" />
## Features
- **Slash Commands**: Use slash commands to quickly access Khoj features
@@ -23,6 +25,6 @@ If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://k
We have more commands under development, including `/share` to uploading documents directly to your Khoj account from WhatsApp, and `/speak` in order to get a speech response from Khoj. Feel free to [raise an issue](https://github.com/khoj-ai/flint/issues) if you have any suggestions for new commands.
## Nerdy Details
## Source Code
You can find all of the code for the WhatsApp bot in the the [flint repository](https://github.com/khoj-ai/flint). As all of our code, it is open source and you can contribute to it.

View File

@@ -186,7 +186,7 @@ In whichever clients you're using for testing, you'll need to update the server
### Before Making Changes
1. Install Git Hooks for Validation
```shell
pre-commit install -t pre-push -t pre-commit
./scripts/dev_setup.sh
```
- This ensures standard code formatting fixes and other checks run automatically on every commit and push
- Note 1: If [pre-commit](https://pre-commit.com/#intro) didn't already get installed, [install it](https://pre-commit.com/#install) via `pip install pre-commit`
@@ -229,7 +229,7 @@ The core code for the Obsidian plugin is under `src/interface/obsidian`. The fil
4. Open the `khoj` folder in the file explorer that opens. You'll see a file called `main.js` in this folder. To test your changes, replace this file with the `main.js` file that was generated by the development server in the previous section.
## Create Khoj Release (Only for Maintainers)
Follow the steps below to [release](https://github.com/debanjum/khoj/releases/) Khoj. This will create a stable release of Khoj on [Pypi](https://pypi.org/project/khoj-assistant/), [Melpa](https://stable.melpa.org/#%252Fkhoj) and [Obsidian](https://obsidian.md/plugins?id%253Dkhoj). It will also create desktop apps of Khoj and attach them to the latest release.
Follow the steps below to [release](https://github.com/debanjum/khoj/releases/) Khoj. This will create a stable release of Khoj on [Pypi](https://pypi.org/project/khoj/), [Melpa](https://stable.melpa.org/#%252Fkhoj) and [Obsidian](https://obsidian.md/plugins?id%253Dkhoj). It will also create desktop apps of Khoj and attach them to the latest release.
1. Create and tag release commit by running the bump_version script. The release commit sets version number in required metadata files.
```shell

View File

@@ -4,11 +4,11 @@ The Github integration allows you to index as many repositories as you want. It'
# Configure your settings
1. Go to [https://app.khoj.dev/config](https://app.khoj.dev/config) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
1. Go to [https://app.khoj.dev/settings](https://app.khoj.dev/settings) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
## Use the Github plugin
1. Generate a [classic PAT (personal access token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) from [Github](https://github.com/settings/tokens) with `repo` and `admin:org` scopes at least.
2. Navigate to [https://app.khoj.dev/config/content-source/github](https://app.khoj.dev/config/content-source/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
2. Navigate to [https://app.khoj.dev/settings#github](https://app.khoj.dev/settings#github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
3. Click `Save`. Go back to the settings page and click `Configure`.
4. Go to [https://app.khoj.dev/](https://app.khoj.dev/) and start searching!

View File

@@ -2,7 +2,7 @@
The Notion integration allows you to search/chat with your Notion workspaces. [Notion](https://notion.so/) is a platform people use for taking notes, especially for collaboration.
Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
Go to https://app.khoj.dev/settings to connect your Notion workspace(s) to Khoj.
![notion_integration](https://assets.khoj.dev/notion_integration.gif)
@@ -13,7 +13,7 @@ Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
![setup_new_integration](https://github.com/khoj-ai/khoj/assets/65192171/b056e057-d4dc-47dc-aad3-57b59a22c68b)
3. Share all the workspaces that you want to integrate with the Khoj integration you just made in the previous step
![enable_workspace](https://github.com/khoj-ai/khoj/assets/65192171/98290303-b5b8-4cb0-b32c-f68c6923a3d0)
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at http://localhost:42110/config/content-source/notion. Click `Save`.
5. Click `Configure` in http://localhost:42110/config to index your Notion workspace(s).
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at [http://localhost:42110/settings#notion](http://localhost:42110/settings#notion). Click `Save`.
5. Click `Configure` in http://localhost:42110/settings to index your Notion workspace(s).
That's it! You should be ready to start searching and chatting. Make sure you've configured your [chat settings](/get-started/setup#2-configure).

View File

@@ -29,6 +29,7 @@ Khoj is available as a [Desktop app](/clients/desktop), [Emacs package](/clients
![](/img/khoj_clients.svg ':size=400px')
### Supported Data Sources
Khoj can understand your org-mode, markdown, PDF, plaintext files, [Github projects](/data-sources/github_integration) and [Notion pages](/data-sources/notion_integration).
Khoj can understand your word, PDF, org-mode, markdown, plaintext files, [Github projects](/data-sources/github_integration) and [Notion pages](/data-sources/notion_integration).
![](/img/khoj_datasources.svg ':size=200px')

View File

@@ -5,5 +5,5 @@
Khoj will use your local time zone to determine the scheduling localization. You can go back and configure the prompt any time you want from the automations page. You can also delete the automation if you no longer need it.
:::danger[Note]
Automations will not deliver emails to self-hosted users out of the box. You'll have to have Resend and [Google Auth](/miscellaneous/google_auth) setup to send emails.
Automations will not deliver emails to self-hosted users out of the box. You'll have to have Resend and [Authentication](/advanced/authentication) setup to send emails.
:::

View File

@@ -25,7 +25,7 @@ Offline chat stays completely private and can work without internet using open-s
> - An Nvidia, AMD GPU or a Mac M1+ machine would significantly speed up chat response times
1. Open your [Khoj offline settings](http://localhost:42110/server/admin/database/offlinechatprocessorconversationconfig/) and click *Enable* on the Offline Chat configuration.
2. Open your [Chat model options settings](http://localhost:42110/server/admin/database/chatmodeloptions/) and add any [GGUF chat model](https://huggingface.co/models?library=gguf) to use for offline chat. Make sure to use `Offline` as its type. For a balanced chat model that runs well on standard consumer hardware we recommend using [Hermes-2-Pro-Mistral-7B by NousResearch](https://huggingface.co/NousResearch/Hermes-2-Pro-Mistral-7B-GGUF) by default.
2. Open your [Chat model options settings](http://localhost:42110/server/admin/database/chatmodeloptions/) and add any [GGUF chat model](https://huggingface.co/models?library=gguf) to use for offline chat. Make sure to use `Offline` as its type. For a balanced chat model that runs well on standard consumer hardware we recommend using [Llama 3.1 by Meta](https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF) by default. For machines with no or small GPU we recommend using [Gemma 2 2B](https://huggingface.co/bartowski/gemma-2-2b-it-GGUF) or [Phi 3.5 mini](https://huggingface.co/bartowski/Phi-3.5-mini-instruct-GGUF)
:::tip[Note]
@@ -68,3 +68,4 @@ Slash commands allows you to change what Khoj uses to respond to your query
- **/online**: Use online information and incorporate it in the prompt to the LLM to send you a response.
- **/image**: Generate an image in response to your query.
- **/help**: Use /help to get all available commands and general information about Khoj
- **/summarize**: Can be used to summarize 1 selected file filter for that conversation. Refer to [File Summarization](summarization) for details.

View File

@@ -0,0 +1,9 @@
# Desktop Quick Chat (Khoj Mini)
Once you have the Khoj [desktop application](https://khoj.dev/downloads) installed, you can use the desktop shortcut to quickly pull up a mini chat module for quicker answers. See the desktop setup instructions [in the docs](/clients/desktop.md) for more information.
To use it, you just have to copy the text you want to inject into your query, and then run `Ctrl + Shift + K` (or `Cmd + Shift + K` on Mac) to open the mini chat module. The text you copied will be automatically pasted into the chat module, and you can then hit enter to get the answer. You can edit the text before hitting enter if you want to refine your query.
The desktop shortcut is a great way to quickly get answers to your questions without having to switch between windows or tabs. It's especially useful when you're working on a project and need to quickly look up something without losing your focus.
![Desktop Shortcut](https://assets.khoj.dev/courseload_decision_dekstop.gif)

View File

@@ -1,17 +1,21 @@
# Online Search
By default, Khoj will try to infer which information-sourcing tools are required to answer your question. Sometimes, you'll have a need for outside questions that the LLM's knowledge doesn't cover. In that case, it will use the `online` search feature.
Khoj will research on the internet to ground its responses, when it determines that it would need fresh information outside its existing knowledge to answer the query. It will always show any online references it used to respond to your requests.
For example, these queries would trigger an online search:
By default, Khoj will try to infer which information sources, it needs to read to answer your question. This can include reading your documents or researching information online. You can also explicitly trigger an online search by adding the `/online` prefix to your chat query.
Example queries that should trigger an online search:
- What's the latest news about the Israel-Palestine war?
- Where can I find the best pizza in New York City?
- Deadline for filing taxes 2024.
- /online Deadline for filing taxes 2024.
- Give me a summary of this article: https://en.wikipedia.org/wiki/Haitian_Revolution
Try it out yourself! https://app.khoj.dev
## Self-Hosting
The general online search function currently requires an API key from Serper.dev. You can grab one here: https://serper.dev/, and then add it as an environment variable with the name `SERPER_DEV_API_KEY`.
Online search works out of the box even when self-hosting. Khoj uses [JinaAI's reader API](https://jina.ai/reader/) to search online and read webpages by default. No API key setup is necessary.
Without any API keys, Khoj will use the `requests` library to directly read any webpages you give it a link to. This means that you can use Khoj to read any webpage that you have access in your local network.
To improve online search, set the `SERPER_DEV_API_KEY` environment variable to your [Serper.dev](https://serper.dev/) API key. These search results include additional context like answer box, knowledge graph etc.
For advanced webpage reading, set the `OLOSTEP_API_KEY` environment variable to your [Olostep](https://www.olostep.com/) API key. This has a higher success rate at reading webpages than the default webpage reader.

View File

@@ -0,0 +1,26 @@
---
sidebar_position: 5
---
# File Summarization
You can use the `/summarize` command to get Khoj to generate context driven summaries of your documents.
Simply select a single file filter on the left panel menu and then use `/summarize [any context]` and Khoj
will produce a tailored summary of the text.
You can also try a natural language query which include the intent for summary without explicitly using the `/summarize` command.
## Design Diagram
<img src="/img/summarize.jpg" alt="Chat on Web" style={{width: '800px'}}/>
## Example Usage
* `/summarize in a way that can be used as practice questions for a test`
* `/summarize in a way a toddler can understand`
* `/summarize in one paragraph`
Without using the `/summarize` command:
* `create a summary of the document in a way that can be used as practice questions for a test`
* `summarize the document in a way a toddler can understand`
* `generate a one paragraph summary of the document`

View File

@@ -0,0 +1,27 @@
# Voice
You can talk to Khoj using your voice. Khoj will respond to your queries using the same models as the chat feature. You can use voice chat on the web, Desktop, and Obsidian apps.
![Voice Chat](/img/mic_chat_icon.png)
Click on the little mic icon to send your voice message to Khoj. It will send back what it heard via text. You'll have some time to edit it before sending it, if required. Try it at https://app.khoj.dev/.
## Voice Response
If you send a voice message, Khoj will automatically respond back with a voice message.
You can also click on the speaker icon next to any message to hear it out loud. The voice response feature is available only on the web view right now.
![Speaker Icon](/img/speaker_icon.png)
## Setup (Self-Hosting)
Voice chat will automatically be configured when you initialize the application. The default configuration will run locally. If you want to use the OpenAI whisper API for voice chat, you can set it up by following these steps:
1. Setup your OpenAI API key. See instructions [here](/get-started/setup#2-configure).
2. Create a new configuration at http://localhost:42110/server/admin/database/speechtotextmodeloptions/. We recommend the value `whisper-1` and model type `Openai`.
If you want to use the Text to Speech feature, you can set it up by following these steps:
1. Setup your account on [ElevenLabs.io](https://elevenlabs.io/).
2. Configure your API key in your environment variables with the key `ELEVEN_LABS_API_KEY`.
2. (Optional) Create a new [Voice model option](http://localhost:42110/server/admin/database/voicemodeloption/) with a specific voice ID from whichever voice you want to use. You can explore the options [here](https://elevenlabs.io/app/voice-library).

View File

@@ -1,14 +0,0 @@
# Voice
You can talk to Khoj using your voice. Khoj will respond to your queries using the same models as the chat feature. You can use voice chat on the web, Desktop, and Obsidian apps. Click on the little mic icon to send your voice message to Khoj. It will send back what it heard via text. You'll have some time to edit it before sending it, if required. Try it at https://app.khoj.dev/.
:::info[Voice Response]
Khoj doesn't yet respond with voice, but it will send back a text response. Let us know if you're interested in voice responses at team at khoj.dev.
:::
## Setup (Self-Hosting)
Voice chat will automatically be configured when you initialize the application. The default configuration will run locally. If you want to use the OpenAI whisper API for voice chat, you can set it up by following these steps:
1. Setup your OpenAI API key. See instructions [here](/get-started/setup#2-configure).
2. Create a new configuration at http://localhost:42110/server/admin/database/speechtotextmodeloptions/. We recommend the value `whisper-1` and model type `Openai`.

View File

@@ -1,51 +0,0 @@
---
sidebar_position: 2
---
# Demos
Check out a couple of demos and screenshots of Khoj in action.
### Screenshots
| Web | Obsidian | Emacs |
|:---:|:--------:|:-----:|
| ![](/img/khoj_search_on_web.png ':size=300px') | ![](/img/khoj_search_on_obsidian.png ':size=300px') | ![](/img/khoj_search_on_emacs.png ':size=300px') |
| ![](/img/khoj_chat_on_web.png ':size=300px') | ![](/img/khoj_chat_on_obsidian.png ':size=300px') | ![](/img/khoj_chat_on_emacs.png ':size=400px') |
### Videos
#### Khoj in Obsidian
[Link to Video](https://github-production-user-asset-6210df.s3.amazonaws.com/6413477/240061700-3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b.mp4)
##### Installation
1. Install Khoj via `pip` and start Khoj backend in a terminal (Run `khoj`)
```bash
python -m pip install khoj-assistant
khoj
```
2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
- Check the new Khoj plugin settings
- Let Khoj backend index the markdown, pdf, Github markdown files in the current Vault
- Open Khoj plugin on Obsidian via Search button on Left Pane
- Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
- Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
#### Khoj in Emacs, Browser
[Link to Video](https://user-images.githubusercontent.com/6413477/184735169-92c78bf1-d827-4663-9087-a1ea194b8f4b.mp4)
##### Installation
- Install Khoj via pip
- Start Khoj app
- Add this readme and [khoj.el readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs) as org-mode for Khoj to index
- Search \"*Setup editor*\" on the Web and Emacs. Re-rank the results for better accuracy
- Top result is what we are looking for, the [section to Install Khoj.el on Emacs](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#2-Install-Khojel)
##### Analysis
- The results do not have any words used in the query
- *Based on the top result it seems the re-ranking model understands that Emacs is an editor?*
- The results incrementally update as the query is entered
- The results are re-ranked, for better accuracy, once user hits enter

View File

@@ -1,7 +1,7 @@
---
sidebar_position: 0
slug: /
keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain ai", "ai search engine"]
keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain", "personal ai", "ai search engine"]
---
# Overview
@@ -9,7 +9,7 @@ keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features"
<p align="center"><img src="/img/khoj-logo-sideways-500.png" width="200" alt="Khoj Logo"></img></p>
<div align="center">
<b>An AI copilot for your Second Brain</b>
<b>Your Second Brain</b>
</div>
<br />
@@ -27,10 +27,10 @@ keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features"
Welcome to the Khoj Docs! This is the best place to get setup and explore Khoj's features.
- Khoj is an open source, personal AI
- You can [chat](/features/chat) with it about anything. It'll use files you shared with it to respond, when relevant
- You can [chat](/features/chat) with it about anything. It'll use files you shared with it to respond, when relevant. It can also access information from the public internet.
- Quickly [find](/features/search) relevant notes and documents using natural language
- It understands pdf, plaintext, markdown, org-mode files, [notion pages](/data-sources/notion_integration) and [github repositories](/data-sources/github_integration)
- Access it from your [Emacs](/clients/emacs), [Obsidian](/clients/obsidian), [Web browser](/clients/web) or the [Khoj Desktop app](/clients/desktop)
- Access it from your [Emacs](/clients/emacs), [Obsidian](/clients/obsidian), the [Khoj desktop app](/clients/desktop), or [any web browser](/clients/web)
- Use [cloud](https://app.khoj.dev/login) to access your Khoj anytime from anywhere, [self-host](/get-started/setup) on consumer hardware for privacy
## Quickstart
@@ -39,13 +39,3 @@ Welcome to the Khoj Docs! This is the best place to get setup and explore Khoj's
## At a Glance
![demo_chat](https://assets.khoj.dev/using_khoj_for_studying.gif)
#### [Search](/features/search)
- **Natural**: Use natural language queries to quickly find relevant notes and documents.
- **Incremental**: Incremental search for a fast, search-as-you-type experience
#### [Chat](/features/chat)
- **Faster answers**: Find answers faster, smoother than search. No need to manually scan through your notes to find answers.
- **Iterative discovery**: Iteratively explore and (re-)discover your notes
- **Assisted creativity**: Smoothly weave across answers retrieval and content generation
- **Online or Offline**: Choose online or offline chat depending on your requirements

View File

@@ -14,7 +14,7 @@ Here's what to consider if you're using Khoj, whether self-hosted or on our clou
1. We collect completely anonymized usage telemetry and send it to [PostHog](https://posthog.com/). This includes data like unique chat requests, unique search requests, unique requests to index data. Usage data is collected to help us understand how people are using Khoj, and to help us prioritize features.
- We do not log your IP address, nor upload any of your personal data to PostHog.
- You can see our telemetry aggregation code [here](https://github.com/khoj-ai/khoj/blob/master/src/khoj/routers/helpers.py#L71) and see our telemetry server [here](https://github.com/khoj-ai/khoj/blob/master/src/telemetry/telemetry.py).
- If you're self-hosting, you can opt out of telemetry by following [these instructions](./miscellaneous/telemetry).
- If you're self-hosting, you can opt out of telemetry by following [these instructions](/miscellaneous/telemetry).
Self-hosting isn't for everyone, so we've still taken steps to make Khoj privacy-friendly, even if you choose to use our [cloud offering](https://app.khoj.dev/login). Here's what to consider when using Khoj Cloud:
@@ -22,6 +22,8 @@ Self-hosting isn't for everyone, so we've still taken steps to make Khoj privacy
1. Your embeddings and the associated raw text are stored in a secure Postgres DB in our private AWS cloud. Your data is sharded on a unique user ID. We store the raw text in your files to improve file syncing and provide context when you chat with Khoj.
1. When you use the single-sign-on option with Google, we only receive your name, a link to your profile photo, and your email address.
You can see our full privacy policy [here](https://khoj.dev/privacy-policy).
:::tip[Info]
Your data is yours. We do not sell your data or use it for training models. Khoj is a sustainable, open-source alternative to closed-source, commercial personal AI. We have no interest in selling your data to make a quick buck.

View File

@@ -16,20 +16,13 @@ import TabItem from '@theme/TabItem';
## Setup
These are the general setup instructions for self-hosted Khoj.
- Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine
- Check the [Khoj Emacs docs](/clients/emacs#setup) to setup Khoj with Emacs<br />
It's simpler as it can skip the server *install*, *run* and *configure* step below.
- Check the [Khoj Obsidian docs](/clients/obsidian#setup) to setup Khoj with Obsidian<br />
Its simpler as it can skip the *configure* step below.
For Installation, you can either use Docker or install the Khoj server locally.
You can install the Khoj server using either Docker or Pip.
:::info[Offline Model + GPU]
If you want to use the offline chat model and you have a GPU, you should use Installation Option 2 - local setup via the Python package directly. Our Docker image doesn't currently support running the offline chat model on GPU, making inference times really slow.
:::
### Installation Option 1 (Docker)
### 1A. Install Method 1: Docker
#### Prerequisites
1. Install Docker Engine. See [official instructions](https://docs.docker.com/engine/install/).
@@ -37,21 +30,23 @@ If you want to use the offline chat model and you have a GPU, you should use Ins
#### Setup
Use the sample docker-compose [in Github](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml) to run Khoj in Docker. Start by configuring all the environment variables to your choosing. Your admin account will automatically be created based on the admin credentials in that file, so pay attention to those. To start the container, run the following command in the same directory as the docker-compose.yml file. This will automatically setup the database and run the Khoj server.
1. Get the sample docker-compose file [from Github](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml).
2. Configure the environment variables in the docker-compose.yml to your choosing.<br />
Note: *Your admin account will automatically be created based on the admin credentials in that file, so pay attention to those.*
3. Now start the container by running the following command in the same directory as your docker-compose.yml file. This will automatically setup the database and run the Khoj server.
```shell
docker-compose up
```
```shell
docker-compose up
```
Khoj should now be running at http://localhost:42110! You can see the web UI in your browser.
Khoj should now be running at http://localhost:42110. You can see the web UI in your browser.
### Installation Option 2 (Local)
### 1B. Install Method 2: Pip
#### Prerequisites
##### Install Postgres (with PgVector)
Khoj uses the `pgvector` package to store embeddings of your index in a Postgres database. In order to use this, you need to have Postgres installed.
Khoj uses Postgres DB for all server configuration and to scale to multi-user setups. It uses the pgvector package in Postgres to manage your document embeddings. Both Postgres and pgvector need to be installed for Khoj to work.
```mdx-code-block
<Tabs groupId="operating-systems">
@@ -97,24 +92,23 @@ sudo -u postgres createdb khoj --password
</Tabs>
```
#### Install Khoj server
#### Install package
##### Local Server Setup
##### Install Khoj Server
- *Make sure [python](https://realpython.com/installing-python/) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine*
- Check [llama-cpp-python setup](https://python.langchain.com/docs/integrations/llms/llamacpp#installation) if you hit any llama-cpp issues with the installation
Run the following command in your terminal to install the Khoj backend.
Run the following command in your terminal to install the Khoj server.
```mdx-code-block
<Tabs groupId="operating-systems">
<TabItem value="macos" label="MacOS">
```shell
# ARM/M1+ Machines
MAKE_ARGS="-DLLAMA_METAL=on" python -m pip install khoj-assistant
MAKE_ARGS="-DLLAMA_METAL=on" python -m pip install khoj
# Intel Machines
python -m pip install khoj-assistant
python -m pip install khoj
```
</TabItem>
<TabItem value="win" label="Windows">
@@ -128,25 +122,25 @@ python -m pip install khoj-assistant
$env:CMAKE_ARGS = "-DLLAMA_VULKAN=on"
# 2. Install Khoj
py -m pip install khoj-assistant
py -m pip install khoj
```
</TabItem>
<TabItem value="unix" label="Linux">
```shell
# CPU
python -m pip install khoj-assistant
python -m pip install khoj
# NVIDIA (CUDA) GPU
CMAKE_ARGS="DLLAMA_CUDA=on" FORCE_CMAKE=1 python -m pip install khoj-assistant
CMAKE_ARGS="DLLAMA_CUDA=on" FORCE_CMAKE=1 python -m pip install khoj
# AMD (ROCm) GPU
CMAKE_ARGS="-DLLAMA_HIPBLAS=on" FORCE_CMAKE=1 python -m pip install khoj-assistant
CMAKE_ARGS="-DLLAMA_HIPBLAS=on" FORCE_CMAKE=1 python -m pip install khoj
# VULCAN GPU
CMAKE_ARGS="-DLLAMA_VULKAN=on" FORCE_CMAKE=1 python -m pip install khoj-assistant
CMAKE_ARGS="-DLLAMA_VULKAN=on" FORCE_CMAKE=1 python -m pip install khoj
```
</TabItem>
</Tabs>
```
##### Local Server Start
##### Start Khoj Server
Before getting started, configure the following environment variables in your terminal for the first run
@@ -186,26 +180,37 @@ On the first run, you will be prompted to input credentials for your admin accou
Khoj should now be running at http://localhost:42110. You can see the web UI in your browser.
Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g with `@reboot khoj`)
Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g. with `@reboot khoj`)
### Setup Notes
You can use Khoj with a custom domain as well. To do so, you need to set the `KHOJ_DOMAIN` environment variable to your domain (e.g., `export KHOJ_DOMAIN=my-khoj-domain.com` or add it to your `docker-compose.yml`). By default, the Khoj server you set up will not be accessible outside of `localhost` or `127.0.0.1`.
:::warning[Without HTTPS certificate]
To expose Khoj on a custom domain over the public internet, use of an SSL certificate is strongly recommended. You can use [Let's Encrypt](https://letsencrypt.org/) to get a free SSL certificate for your domain.
To disable HTTPS, set the `KHOJ_NO_HTTPS` environment variable to `True`. This can be useful if Khoj is only accessible behind a secure, private network.
:::
### 2. Configure
1. Go to http://localhost:42110/server/admin and login with your admin credentials.
#### Login to the Khoj Admin Panel
Go to http://localhost:42110/server/admin and login with the admin credentials you setup during installation.
:::info[CSRF Error]
Ensure you are using **localhost, not 127.0.0.1**, to access the admin panel to avoid the CSRF error.
:::
:::info[DISALLOWED HOST Error]
You may hit this if you try access Khoj exposed on a custom domain (e.g. 192.168.12.3 or example.com) or over HTTP.
Set the environment variables KHOJ_DOMAIN=your-domain and KHOJ_NO_HTTPS=false if required to avoid this error.
:::
:::tip[Note]
Using Safari on Mac? You might not be able to login to the admin panel. Try using Chrome or Firefox instead.
:::
#### Configure Chat Model
##### Configure OpenAI or a custom OpenAI-compatible proxy server
Setup which chat model you'd want to use. Khoj supports local and online chat models.
:::tip[Multiple Chat Models]
Add a `ServerChatSettings` with `Default` and `Summarizer` fields set to your preferred chat model via [the admin panel](http://localhost:42110/server/admin/database/serverchatsettings/add/). Otherwise Khoj defaults to use the first chat model in your [ChatModelOptions](http://localhost:42110/server/admin/database/chatmodeloptions/) for all non chat response generation tasks.
:::
##### Configure OpenAI Chat
:::info[Ollama Integration]
Using Ollama? See the [Ollama Integration](/miscellaneous/ollama) section for more custom setup instructions.
Using Ollama? See the [Ollama Integration](/advanced/ollama) section for more custom setup instructions.
:::
1. Go to the [OpenAI settings](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/) in the server admin settings to add an OpenAI processor conversation config. This is where you set your API key and server API base URL. The API base URL is optional - it's only relevant if you're using another OpenAI-compatible proxy server.
@@ -214,55 +219,45 @@ Using Ollama? See the [Ollama Integration](/miscellaneous/ollama) section for mo
- The `tokenizer` and `max-prompt-size` fields are optional. Set them only if you're sure of the tokenizer or token limit for the model you're using. Contact us if you're unsure what to do here.
##### Configure Offline Chat
Any chat model on Huggingface in GGUF format can be used for local chat. Here's how you can set it up:
1. No need to setup a conversation processor config!
2. Go over to configure your [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/). Set the `chat-model` field to a supported chat model[^1] of your choice. For example, we recommend `NousResearch/Hermes-2-Pro-Mistral-7B-GGUF`, but [any gguf model on huggingface](https://huggingface.co/models?library=gguf) should work.
2. Go over to configure your [chat model options](http://localhost:42110/server/admin/database/chatmodeloptions/). Set the `chat-model` field to a supported chat model[^1] of your choice. For example, we recommend `bartowski/Meta-Llama-3.1-8B-Instruct-GGUF`, but [any gguf model on huggingface](https://huggingface.co/models?library=gguf) should work.
- Make sure to set the `model-type` to `Offline`. Do not set `openai config`.
- The `tokenizer` and `max-prompt-size` fields are optional. Set them only when using a non-standard model (i.e not mistral, gpt or llama2 model) when you know the token limit.
- The `tokenizer` and `max-prompt-size` fields are optional. You can set these for non-standard models (i.e not Mistral or Llama based models) or when you know the token limit of the model to improve context stuffing.
#### Share your data
1. Select files and folders to index [using the desktop client](/get-started/setup#2-download-the-desktop-client). When you click 'Save', the files will be sent to your server for indexing.
- Select Notion workspaces and Github repositories to index using the web interface.
You can sync your files and folders with Khoj using the [Desktop](/clients/desktop#setup), [Obsidian](/clients/obsidian#setup), or [Emacs](/clients/emacs#setup) clients or just drag and drop specific files on the [website](/clients/web#upload-documents). You can also directly sync your [Notion workspace](/data-sources/notion_integration).
[^1]: Khoj, by default, can use [OpenAI GPT3.5+ chat models](https://platform.openai.com/docs/models/overview) or [GGUF chat models](https://huggingface.co/models?library=gguf). See [this section](/miscellaneous/advanced#use-openai-compatible-llm-api-server-self-hosting) on how to locally use OpenAI-format compatible proxy servers.
[^1]: Khoj, by default, can use [OpenAI GPT3.5+ chat models](https://platform.openai.com/docs/models/overview) or [GGUF chat models](https://huggingface.co/models?library=gguf). See [this section](/advanced/use-openai-proxy) on how to locally use OpenAI-format compatible proxy servers.
:::tip[Note]
Using Safari on Mac? You might not be able to login to the admin panel. Try using Chrome or Firefox instead.
:::
### 3. Use Khoj 🚀
### 3. Download the desktop client (Optional)
Now open http://localhost:42110 to start interacting with Khoj!
You can use our desktop executables to select file paths and folders to index. You can simply select the folders or files, and they'll be automatically uploaded to the server. Once you specify a file or file path, you don't need to update the configuration again; it will grab any data diffs dynamically over time.
**To download the latest desktop client, go to https://download.khoj.dev** and the correct executable for your OS will automatically start downloading. You can also go to https://khoj.dev/downloads to explicitly download your image of choice. Once downloaded, you can configure your folders for indexing using the settings tab. To set your chat configuration, you'll have to use the web interface for the Khoj server you setup in the previous step.
To use the desktop client, you need to go to your Khoj server's settings page (http://localhost:42110/config) and copy the API key. Then, paste it into the desktop client's settings page. Once you've done that, you can select files and folders to index. Set the desktop client settings to use `http://127.0.0.1:42110` as the host URL.
### 4. Install Client Plugins (Optional)
### 4. Install Khoj Clients (Optional)
Khoj exposes a web interface to search, chat and configure by default.<br />
The optional steps below allow using Khoj from within an existing application like Obsidian or Emacs.
You can install a Khoj client to sync your documents or to easily access Khoj from within Obsidian, Emacs or your OS.
- **Khoj Desktop**:<br />
[Install](/clients/desktop#setup) the Khoj Desktop app.
- **Khoj Obsidian**:<br />
[Install](/clients/obsidian#setup) the Khoj Obsidian plugin
[Install](/clients/obsidian#setup) the Khoj Obsidian plugin.
- **Khoj Emacs**:<br />
[Install](/clients/emacs#setup) khoj.el
#### Setup host URL
To configure your host URL on your clients when self-hosting, use `http://127.0.0.1:42110`. This is the default port for the Khoj server. Note that `localhost` will not work.
### 5. Use Khoj 🚀
You can head to http://localhost:42110 to use the web interface. You can also use the desktop client to search and chat.
Set the host URL on your clients settings page to your Khoj server URL. By default, use `http://127.0.0.1:42110` or `http://localhost:42110`. Note that `localhost` may not work in all cases.
## Upgrade
### Upgrade Khoj Server
```mdx-code-block
<Tabs groupId="environment">
<TabItem value="localsetup" label="Local Setup">
```shell
pip install --upgrade khoj-assistant
pip install --upgrade khoj
```
*Note: To upgrade to the latest pre-release version of the khoj server run below command*
</TabItem>
@@ -284,14 +279,13 @@ You can head to http://localhost:42110 to use the web interface. You can also us
```
## Uninstall
### Uninstall Khoj Server
```mdx-code-block
<Tabs groupId="environment">
<TabItem value="localsetup" label="Local Setup">
```shell
# uninstall khoj server
pip uninstall khoj-assistant
pip uninstall khoj
# delete khoj postgres db
dropdb khoj -U postgres
@@ -324,13 +318,13 @@ You can head to http://localhost:42110 to use the web interface. You can also us
1. Install [pipx](https://pypa.github.io/pipx/#install-pipx)
2. Use `pipx` to install Khoj to avoid dependency conflicts with other python packages.
```shell
pipx install khoj-assistant
pipx install khoj
```
3. Now start `khoj` using the standard steps described earlier
#### Install fails while building Tokenizer dependency
- **Details**: `pip install khoj-assistant` fails while building the `tokenizers` dependency. Complains about Rust.
- **Details**: `pip install khoj` fails while building the `tokenizers` dependency. Complains about Rust.
- **Fix**: Install Rust to build the tokenizers package. For example on Mac run:
```shell
brew install rustup
@@ -343,3 +337,14 @@ You can head to http://localhost:42110 to use the web interface. You can also us
#### Khoj in Docker errors out with \"Killed\" in error message
- **Fix**: Increase RAM available to Docker Containers in Docker Settings
- **Refer**: [StackOverflow Solution](https://stackoverflow.com/a/50770267), [Configure Resources on Docker for Mac](https://docs.docker.com/desktop/mac/#resources)
## Advanced
### Self Host on Custom Domain
You can self-host Khoj behind a custom domain as well. To do so, you need to set the `KHOJ_DOMAIN` environment variable to your domain (e.g., `export KHOJ_DOMAIN=my-khoj-domain.com` or add it to your `docker-compose.yml`). By default, the Khoj server you set up will not be accessible outside of `localhost` or `127.0.0.1`.
:::warning[Without HTTPS certificate]
To expose Khoj on a custom domain over the public internet, use of an SSL certificate is strongly recommended. You can use [Let's Encrypt](https://letsencrypt.org/) to get a free SSL certificate for your domain.
To disable HTTPS, set the `KHOJ_NO_HTTPS` environment variable to `True`. This can be useful if Khoj is only accessible behind a secure, private network.
:::

View File

@@ -1,6 +1,6 @@
{
"label": "Miscellaneous",
"position": 6,
"position": 7,
"link": {
"type": "generated-index",
"description": "Additional resources for learning about Khoj"

View File

@@ -4,14 +4,6 @@ sidebar_position: 3
# Advanced Usage
## Search across Different Languages (Self-Hosting)
To search for notes in multiple, different languages, you can use a [multi-lingual model](https://www.sbert.net/docs/pretrained_models.html#multi-lingual-models).<br />
For example, the [paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) supports [50+ languages](https://www.sbert.net/docs/pretrained_models.html#:~:text=we%20used%20the%20following%2050%2B%20languages), has good search quality and speed. To use it:
1. Manually update the search config in server's admin settings page. Go to [the search config](http://localhost:42110/server/admin/database/searchmodelconfig/). Either create a new one, if none exists, or update the existing one. Set the bi_encoder to `sentence-transformers/multi-qa-MiniLM-L6-cos-v1` and the cross_encoder to `mixedbread-ai/mxbai-rerank-xsmall-v1`.
2. Regenerate your content index from all the relevant clients. This step is very important, as you'll need to re-encode all your content with the new model.
Note: If you use a search model that expects a prefix (e.g [mixedbread-ai/mxbai-embed-large-v1](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1)) to the query (or docs) string before encoding. Update the `bi_encoder_query_encode_config` field with `{prompt: <prefix-prompt>}`. Eg. `{prompt: "Represent this query for searching documents"}`. You can pass a valid JSON object that the SentenceTransformer `encode` function accepts
## Query Filters
Use structured query syntax to filter entries from your knowledge based used by search results or chat responses.
@@ -32,25 +24,3 @@ Use structured query syntax to filter entries from your knowledge based used by
- containing dates from the year *1984*
- excluding words *"big"* and *"brother"*
- that best match the natural language query *"what is the meaning of life?"*
## Use OpenAI compatible LLM API Server (Self Hosting)
Use this if you want to use non-standard, open or commercial, local or hosted LLM models for Khoj chat
1. Setup your desired chat LLM by installing an OpenAI compatible LLM API Server like [LiteLLM](https://docs.litellm.ai/docs/proxy/quick_start), [llama-cpp-python](https://github.com/abetlen/llama-cpp-python?tab=readme-ov-file#openai-compatible-web-server)
2. Set environment variable `OPENAI_API_BASE="<url-of-your-llm-server>"` before starting Khoj
3. Add ChatModelOptions with `model-type` `OpenAI`, and `chat-model` to anything (e.g `gpt-3.5-turbo`) during [Config](/get-started/setup#3-configure)
- *(Optional)* Set the `tokenizer` and `max-prompt-size` relevant to the actual chat model you're using
#### Sample Setup using LiteLLM and Mistral API
```shell
# Install LiteLLM
pip install litellm[proxy]
# Start LiteLLM and use Mistral tiny via Mistral API
export MISTRAL_API_KEY=<MISTRAL_API_KEY>
litellm --model mistral/mistral-tiny --drop_params
# Set OpenAI API Base to LiteLLM server URL and start Khoj
export OPENAI_API_BASE='http://localhost:8000'
khoj --anonymous-mode
```

View File

@@ -5,9 +5,10 @@ sidebar_position: 4
# Credits
Many Open Source projects are used to power Khoj. Here's a few of them:
- [Multi-QA MiniLM Model](https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1), [All MiniLM Model](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) for Text Search. See [SBert Documentation](https://www.sbert.net/examples/applications/retrieve_rerank/README.html)
- [OpenAI CLIP Model](https://github.com/openai/CLIP) for Image Search. See [SBert Documentation](https://www.sbert.net/examples/applications/image-search/README.html)
- [Llama.cpp](https://github.com/ggerganov/llama.cpp) to chat with local LLM
- [SentenceTransformer](https://www.sbert.net/examples/applications/retrieve_rerank/README.html) for Text Search
- [HuggingFace](https://huggingface.co/) for hosting open-source chat and search models
- Charles Cave for [OrgNode Parser](http://members.optusnet.com.au/~charles57/GTD/orgnode.html)
- [Org.js](https://mooz.github.io/org-js/) to render Org-mode results on the Web interface
- [Markdown-it](https://github.com/markdown-it/markdown-it) to render Markdown results on the Web interface
- [Llama.cpp](https://github.com/ggerganov/llama.cpp) to chat with local LLM
- [Katex](https://katex.org/) to render math

View File

@@ -1,17 +0,0 @@
# Setting up Google Auth
To set up your self-hosted Khoj with Google Auth, you need to create a project in the Google Cloud Console and enable the Google Auth API.
To implement this, you'll need to:
1. You must use the `python` package or build from source, because you'll need to install additional packages for the google auth libraries (`prod`). The syntax to install the right packages is
```
pip install khoj-assistant[prod]
```
2. [Create authorization credentials](https://developers.google.com/identity/sign-in/web/sign-in) for your application.
3. Go to your [Google cloud console](https://console.developers.google.com/apis/credentials) and create a configuration like below for the relevant `OAuth 2.0 Client IDs` project:
![Google auth login project settings](https://github.com/khoj-ai/khoj/assets/65192171/9bcbf6f4-197d-4d0c-973a-c10b1331c892)
4. Configure these environment variables: `GOOGLE_CLIENT_SECRET`, and `GOOGLE_CLIENT_ID`. You can find these values in the Google cloud console, in the same place where you configured the authorized origins and redirect URIs.
That's it! That should be all you have to do. Now, when you reload Khoj without `--anonymous-mode`, you should be able to use your Google account to sign in.

View File

@@ -1,33 +0,0 @@
# Ollama / Khoj
You can run your own open source models locally with Ollama and use them with Khoj.
:::info[Ollama Integration]
This is only going to be helpful for self-hosted users. If you're using [Khoj Cloud](https://app.khoj.dev), you're limited to our first-party models.
:::
Khoj supports any OpenAI-API compatible server, which includes [Ollama](http://ollama.ai/). Ollama allows you to start a local server with [several popular open-source LLMs](https://ollama.com/library) directly on your own computer. Combined with Khoj, you can chat with these LLMs and use them to search your notes and documents.
While Khoj also supports local-hosted LLMs downloaded from Hugging Face, the Ollama integration is particularly useful for its ease of setup and multi-model support, especially if you're already using Ollama.
## Setup
1. Setup Ollama: https://ollama.com/
2. Start your preferred model with Ollama. For example,
```bash
ollama run llama3
```
3. Go to Khoj settings at [OpenAI Processor Conversation Config](http://localhost:42110/server/admin/database/openaiprocessorconversationconfig/)
4. Create a new config.
- Name: `ollama`
- Api Key: `any string`
- Api Base Url: `http://localhost:11434/v1/` (default for Ollama)
5. Go to [Chat Model Options](http://localhost:42110/server/admin/database/chatmodeloptions/)
6. Create a new config.
- Name: `llama3` (replace with the name of your local model)
- Model Type: `Openai`
- Openai Config: `<the ollama config you created in step 4>`
- Max prompt size: `1000` (replace with the max prompt size of your model)
7. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model.

View File

@@ -10,7 +10,7 @@ Here are some top-level performance metrics for Khoj. These are rough estimates
- Semantic search using the bi-encoder is fairly fast at \<100 ms across all content types
- Reranking using the cross-encoder is slower at \<2s on 15 results. Tweak `top_k` to tradeoff speed for accuracy of results
- Filters in query (e.g by file, word or date) usually add \<20ms to query latency
- Filters in query (e.g. by file, word or date) usually add \<20ms to query latency
### Indexing performance

View File

@@ -9,7 +9,7 @@ import {themes as prismThemes} from 'prism-react-renderer';
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Khoj AI',
tagline: 'An AI copilot for your Second Brain',
tagline: 'Your Second Brain',
staticDirectories: ['assets'],
@@ -75,7 +75,6 @@ const config = {
({
image: 'img/khoj-logo-sideways-500.png',
metadata: [
{name: 'keywords', content: 'khoj, khoj ai, chatgpt, open ai, open source, productivity'},
{name: 'og:title', content: 'Khoj Documentation'},
{name: 'og:type', content: 'website'},
{name: 'og:site_name', content: 'Khoj Documentation'},
@@ -129,18 +128,18 @@ const config = {
},
{
label: 'Features',
to: '/features/all_features',
to: '/features/all-features',
},
{
label: 'Client Apps',
to: '/category/clients',
},
{
label: 'Self-Hosting',
label: 'Self-Host',
to: '/get-started/setup',
},
{
label: 'Contributing',
label: 'Contribute',
to: '/contributing/development',
},
],

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"id": "khoj",
"name": "Khoj",
"version": "1.14.0",
"version": "1.21.3",
"minAppVersion": "0.15.0",
"description": "An AI copilot for your Second Brain",
"description": "Your Second Brain",
"author": "Khoj Inc.",
"authorUrl": "https://github.com/khoj-ai",
"isDesktopOnly": false

View File

@@ -1,9 +1,16 @@
FROM ubuntu:jammy
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
LABEL org.opencontainers.image.source="https://github.com/khoj-ai/khoj"
# Install System Dependencies
RUN apt update -y && apt -y install python3-pip libsqlite3-0 ffmpeg libsm6 libxext6
RUN apt update -y && apt -y install python3-pip libsqlite3-0 ffmpeg libsm6 libxext6 swig curl
# Install Node.js and Yarn
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -
RUN apt -y install nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt update && apt -y install yarn
WORKDIR /app
@@ -20,6 +27,11 @@ COPY . .
# Set the PYTHONPATH environment variable in order for it to find the Django app.
ENV PYTHONPATH=/app/src:$PYTHONPATH
# Go to the directory src/interface/web and export the built Next.js assets
WORKDIR /app/src/interface/web
RUN bash -c "yarn cache clean && yarn install --verbose && yarn ciexport"
WORKDIR /app
# Run the Application
# There are more arguments required for the application to run,
# but these should be passed in through the docker-compose.yml file.

View File

@@ -3,11 +3,11 @@ requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "khoj-assistant"
description = "An AI copilot for your Second Brain"
name = "khoj"
description = "Your Second Brain"
readme = "README.md"
license = "AGPL-3.0-or-later"
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [
{ name = "Debanjum Singh Solanky, Saba Imran" },
]
@@ -27,7 +27,6 @@ classifiers = [
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -47,12 +46,13 @@ dependencies = [
"tiktoken >= 0.3.2",
"tenacity >= 8.2.2",
"magika ~= 0.5.1",
"pillow ~= 9.5.0",
"pydantic >= 2.0.0",
"pillow ~= 10.0.0",
"pydantic[email] >= 2.0.0",
"pyyaml ~= 6.0",
"rich >= 13.3.1",
"schedule == 1.1.0",
"sentence-transformers == 2.5.1",
"sentence-transformers == 3.0.1",
"einops == 0.8.0",
"transformers >= 4.28.0",
"torch == 2.2.2",
"uvicorn == 0.17.6",
@@ -64,16 +64,16 @@ dependencies = [
"tenacity == 8.3.0",
"anyio == 3.7.1",
"pymupdf >= 1.23.5",
"django == 4.2.11",
"django == 5.0.7",
"authlib == 1.2.1",
"llama-cpp-python == 0.2.76",
"llama-cpp-python == 0.2.88",
"itsdangerous == 2.1.2",
"httpx == 0.25.0",
"pgvector == 0.2.4",
"psycopg2-binary == 2.9.9",
"lxml == 4.9.3",
"tzdata == 2023.3",
"rapidocr-onnxruntime == 1.3.11; python_version<'3.12'",
"rapidocr-onnxruntime == 1.3.22",
"openai-whisper >= 20231117",
"django-phonenumber-field == 7.3.0",
"phonenumbers == 8.13.27",
@@ -87,6 +87,7 @@ dependencies = [
"cron-descriptor == 1.4.3",
"django_apscheduler == 0.6.2",
"anthropic == 0.26.1",
"docx2txt == 0.8"
]
dynamic = ["version"]
@@ -108,7 +109,7 @@ prod = [
"resend == 1.0.1",
]
dev = [
"khoj-assistant[prod]",
"khoj[prod]",
"pytest >= 7.1.2",
"pytest-xdist[psutil]",
"pytest-django == 4.5.2",

View File

@@ -2,25 +2,31 @@
project_root=$PWD
while getopts 'nc:' opt;
while getopts 'nc:t:' opt;
do
case "${opt}" in
c)
# Get current project version
current_version=$OPTARG
t)
# Get version type to bump. Options: major, minor, patch
version_type=$OPTARG
# Bump Web app to current version
cd $project_root/src/interface/web
yarn version --$version_type --no-git-tag-version
# Bump Desktop app to current version
cd $project_root/src/interface/desktop
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$current_version\",/" package.json
rm *.bak
yarn version --$version_type --no-git-tag-version
# Get bumped project version
current_version=$(grep '"version":' package.json | awk -F '"' '{print $4}')
# Bump Obsidian plugin to current version
cd $project_root/src/interface/obsidian
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$current_version\",/" package.json
sed -E -i.bak "s/version\": \"(.*)\"/version\": \"$current_version\"/" manifest.json
yarn build # verify build before bumping version
yarn version --$version_type --no-git-tag-version
# append current version, min Obsidian app version from manifest to versions json
cp $project_root/versions.json .
npm run version # append current version
rm *.bak
yarn run version # run Obsidian version script
# Bump Emacs package to current version
cd ../emacs
@@ -38,8 +44,57 @@ do
# Commit changes and tag commit for release
git add \
$project_root/src/interface/web/package.json \
$project_root/src/interface/desktop/package.json \
$project_root/src/interface/obsidian/package.json \
$project_root/src/interface/obsidian/yarn.lock \
$project_root/src/interface/obsidian/manifest.json \
$project_root/src/interface/obsidian/versions.json \
$project_root/src/interface/emacs/khoj.el \
$project_root/manifest.json \
$project_root/versions.json
git commit -m "Release Khoj version $current_version"
git tag $current_version master
;;
c)
# Get current project version
current_version=$OPTARG
# Bump Web app to current version
cd $project_root/src/interface/web
yarn version --new-version $current_version --no-git-tag-version
# Bump Desktop app to current version
cd $project_root/src/interface/desktop
yarn version --new-version $current_version --no-git-tag-version
# Bump Obsidian plugin to current version
cd $project_root/src/interface/obsidian
yarn version --new-version $current_version --no-git-tag-version
# append current version, min Obsidian app version from manifest.json to versions.json
cp $project_root/versions.json .
yarn run version # run Obsidian version script
# Bump Emacs package to current version
cd ../emacs
sed -E -i.bak "s/^;; Version: (.*)/;; Version: $current_version/" khoj.el
git add khoj.el
rm *.bak
# Copy current obsidian versioned files to project root
cd $project_root
cp src/interface/obsidian/versions.json .
cp src/interface/obsidian/manifest.json .
# Run pre-commit validation to fix jsons
pre-commit run --hook-stage manual --all
# Commit changes and tag commit for release
git add \
$project_root/src/interface/web/package.json \
$project_root/src/interface/desktop/package.json \
$project_root/src/interface/obsidian/package.json \
$project_root/src/interface/obsidian/yarn.lock \
$project_root/src/interface/obsidian/manifest.json \
$project_root/src/interface/obsidian/versions.json \
$project_root/src/interface/emacs/khoj.el \
@@ -54,17 +109,20 @@ do
next_version=$(touch bump.txt && git add bump.txt && hatch version | sed 's/\.dev.*//g')
git rm --cached -- bump.txt && rm bump.txt
# Bump Web app to next version
cd $project_root/src/interface/web
yarn version --new-version $next_version --no-git-tag-version
# Bump Desktop app to next version
cd $project_root/src/interface/desktop
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$current_version\",/" package.json
rm *.bak
yarn version --new-version $next_version --no-git-tag-version
# Bump Obsidian plugins to next version
cd $project_root/src/interface/obsidian
sed -E -i.bak "s/version\": \"(.*)\",/version\": \"$next_version\",/" package.json
sed -E -i.bak "s/version\": \"(.*)\"/version\": \"$next_version\"/" manifest.json
npm run version # updates versions.json
rm *.bak
yarn version --new-version $next_version --no-git-tag-version
# append next version, min Obsidian app version from manifest to versions json
git rm --cached -- versions.json
yarn run version # run Obsidian version script
# Bump Emacs package to next version
cd $project_root/src/interface/emacs
@@ -76,15 +134,17 @@ do
# Commit changes
git add \
$project_root/src/interface/web/package.json \
$project_root/src/interface/desktop/package.json \
$project_root/src/interface/obsidian/package.json \
$project_root/src/interface/obsidian/yarn.lock \
$project_root/src/interface/obsidian/manifest.json \
$project_root/src/interface/obsidian/versions.json \
$project_root/src/interface/emacs/khoj.el
git commit -m "Bump Khoj to pre-release version $next_version"
;;
?)
echo -e "Invalid command option.\nUsage: $(basename $0) [-c] [-n]"
echo -e "Invalid command option.\nUsage: $(basename $0) [-t] [-c] [-n]"
exit 1
;;
esac

119
scripts/dev_setup.sh Executable file
View File

@@ -0,0 +1,119 @@
# Initialize the development environment for the project
# ---
PROJECT_ROOT=$(git rev-parse --show-toplevel)
# Install Web App
# ---
echo "Installing Web App..."
cd $PROJECT_ROOT/src/interface/web
yarn install
# Install Obsidian App
# ---
echo "Installing Obsidian App..."
cd $PROJECT_ROOT/src/interface/obsidian
yarn install
# Install Desktop App
# ---
echo "Installing Desktop App..."
cd $PROJECT_ROOT/src/interface/desktop
yarn install
# Install Server App
# ---
echo "Installing Server App..."
cd $PROJECT_ROOT
# pip install --user pipenv && pipenv install -e '.[dev]' --skip-lock && pipenv shell
python3 -m venv .venv && pip install -e '.[dev]' && . .venv/bin/activate
# Install pre-commit hooks
# ----
echo "Installing pre-commit hooks..."
# Setup pre-commit hooks using the pre-commit package
pre-commit install -t pre-push -t pre-commit
# Run Prettier on web app
cat << 'EOF' > temp_pre_commit
# Run Prettier for Web App
# -------------------------
# Function to check if color output is possible
can_use_color() {
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && tput colors >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# Function to print colored text if possible
print_color() {
if can_use_color; then
tput setab "$1"
printf "%s" "$2"
tput sgr0
else
printf "%s" "$2"
fi
}
print_status() {
local status="$1"
local color="$2"
printf "prettier%-64s" "..."
print_color "$color" "$status"
printf "\n"
}
PROJECT_ROOT=$(git rev-parse --show-toplevel)
# Get the list of staged files
FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^src/interface/web/' | sed 's| |\\ |g')
if [ -z "$FILES" ]; then
if [ -t 1 ]; then
print_status "Skipped" 6
else
echo "prettier.....................................................Skipped"
fi
else
# Run prettier on staged files
echo "$FILES" | xargs $PROJECT_ROOT/src/interface/web/node_modules/.bin/prettier --ignore-unknown --write
# Check if any files were modified by prettier
MODIFIED=$(git diff --name-only -- $FILES)
if [ -n "$MODIFIED" ]; then
if [ -t 1 ]; then
print_status "Modified" 1
else
echo "prettier.....................................................Modified"
fi
exit 1
fi
# Add back the modified/prettified files to staging
# echo "$FILES" | xargs git add
# Show the user if changes were made
if [ -t 1 ]; then
print_status "Passed" 2
else
echo "prettier.....................................................Passed"
fi
fi
EOF
# Prepend the new content to the existing pre-commit file
cat temp_pre_commit "$(git rev-parse --git-dir)/hooks/pre-commit" > temp_combined_pre_commit
# Replace the old pre-commit file with the new combined one
mv temp_combined_pre_commit "$(git rev-parse --git-dir)/hooks/pre-commit"
# Clean up
# ---
# Remove the temporary pre-commit file
rm temp_pre_commit
# Make sure the pre-commit hook is executable
chmod +x "$(git rev-parse --git-dir)/hooks/pre-commit"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -13,8 +13,25 @@
<link rel="stylesheet" href="https://assets.khoj.dev/higlightjs/solarized-dark.min.css">
<script src="https://assets.khoj.dev/higlightjs/highlight.min.js"></script>
<script src="./utils.js"></script>
<script src="chatutils.js"></script>
<script>
// Add keyboard shortcuts to the chat view
window.addEventListener("keydown", function(event) {
// If enter key is pressed, send the message
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
chat();
// If ^O then Open the chat sessions panel
} else if (event.key === "o" && event.ctrlKey) {
handleCollapseSidePanel();
// If ^N then Create a new conversation
} else if (event.key === "n" && event.ctrlKey) {
createNewConversation();
// If ^D then Delete the conversation history
} else if (event.key === "d" && event.ctrlKey) {
clearConversationHistory();
}
});
let chatOptions = [];
function createCopyParentText(message) {
return function(event) {
@@ -44,6 +61,14 @@
let city = null;
let countryName = null;
let timezone = null;
let chatMessageState = {
newResponseTextEl: null,
newResponseEl: null,
loadingEllipsis: null,
references: {},
rawResponse: "",
isVoice: false,
}
fetch("https://ipapi.co/json")
.then(response => response.json())
@@ -58,356 +83,9 @@
return;
});
function formatDate(date) {
// Format date in HH:MM, DD MMM YYYY format
let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false });
let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit'}).replaceAll('-', ' ');
return `${time_string}, ${date_string}`;
}
function generateReference(referenceJson, index) {
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
// Escape reference for HTML rendering
let escaped_ref = reference.replaceAll('"', '&quot;');
// Generate HTML for Chat Reference
let short_ref = escaped_ref.slice(0, 100);
short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref;
let referenceButton = document.createElement('button');
referenceButton.textContent = short_ref;
referenceButton.id = `ref-${index}`;
referenceButton.classList.add("reference-button");
referenceButton.classList.add("collapsed");
referenceButton.tabIndex = 0;
// Add event listener to toggle full reference on click
referenceButton.addEventListener('click', function() {
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
this.textContent = escaped_ref;
} else {
this.classList.add("collapsed");
this.classList.remove("expanded");
this.textContent = short_ref;
}
});
return referenceButton;
}
function generateOnlineReference(reference, index) {
// Generate HTML for Chat Reference
let title = reference.title || reference.link;
let link = reference.link;
let snippet = reference.snippet;
let question = reference.question;
if (question) {
question = `<b>Question:</b> ${question}<br><br>`;
} else {
question = "";
}
let linkElement = document.createElement('a');
linkElement.setAttribute('href', link);
linkElement.setAttribute('target', '_blank');
linkElement.setAttribute('rel', 'noopener noreferrer');
linkElement.classList.add("inline-chat-link");
linkElement.classList.add("reference-link");
linkElement.setAttribute('title', title);
linkElement.textContent = title;
let referenceButton = document.createElement('button');
referenceButton.innerHTML = linkElement.outerHTML;
referenceButton.id = `ref-${index}`;
referenceButton.classList.add("reference-button");
referenceButton.classList.add("collapsed");
referenceButton.tabIndex = 0;
// Add event listener to toggle full reference on click
referenceButton.addEventListener('click', function() {
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
this.innerHTML = linkElement.outerHTML + `<br><br>${question + snippet}`;
} else {
this.classList.add("collapsed");
this.classList.remove("expanded");
this.innerHTML = linkElement.outerHTML;
}
});
return referenceButton;
}
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") {
let message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
let formattedMessage = formatHTMLMessage(message, raw);
// Create a new div for the chat message
let chatMessage = document.createElement('div');
chatMessage.className = `chat-message ${by}`;
chatMessage.dataset.meta = `${by_name} at ${message_time}`;
// Create a new div for the chat message text and append it to the chat message
let chatMessageText = document.createElement('div');
chatMessageText.className = `chat-message-text ${by}`;
chatMessageText.appendChild(formattedMessage);
chatMessage.appendChild(chatMessageText);
// Append annotations div to the chat message
if (annotations) {
chatMessageText.appendChild(annotations);
}
// Append chat message div to chat body
let chatBody = document.getElementById("chat-body");
if (renderType === "append") {
chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element
chatBody.scrollTop = chatBody.scrollHeight;
} else if (renderType === "prepend") {
chatBody.insertBefore(chatMessage, chatBody.firstChild);
} else if (renderType === "return") {
return chatMessage;
}
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
}
function processOnlineReferences(referenceSection, onlineContext) {
let numOnlineReferences = 0;
for (let subquery in onlineContext) {
let onlineReference = onlineContext[subquery];
if (onlineReference.organic && onlineReference.organic.length > 0) {
numOnlineReferences += onlineReference.organic.length;
for (let index in onlineReference.organic) {
let reference = onlineReference.organic[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
if (onlineReference.knowledgeGraph && onlineReference.knowledgeGraph.length > 0) {
numOnlineReferences += onlineReference.knowledgeGraph.length;
for (let index in onlineReference.knowledgeGraph) {
let reference = onlineReference.knowledgeGraph[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
if (onlineReference.peopleAlsoAsk && onlineReference.peopleAlsoAsk.length > 0) {
numOnlineReferences += onlineReference.peopleAlsoAsk.length;
for (let index in onlineReference.peopleAlsoAsk) {
let reference = onlineReference.peopleAlsoAsk[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
numOnlineReferences += onlineReference.webpages.length;
for (let index in onlineReference.webpages) {
let reference = onlineReference.webpages[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
}
return numOnlineReferences;
}
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) {
let chatEl;
if (intentType?.includes("text-to-image")) {
let imageMarkdown = generateImageMarkdown(message, intentType, inferredQueries);
chatEl = renderMessage(imageMarkdown, by, dt, null, false, "return");
} else {
chatEl = renderMessage(message, by, dt, null, false, "return");
}
// If no document or online context is provided, render the message as is
if ((context == null || context?.length == 0)
&& (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
return chatEl;
}
// If document or online context is provided, render the message with its references
let references = {};
if (!!context) references["notes"] = context;
if (!!onlineContext) references["online"] = onlineContext;
let chatMessageEl = chatEl.getElementsByClassName("chat-message-text")[0];
chatMessageEl.appendChild(createReferenceSection(references));
return chatEl;
}
function generateImageMarkdown(message, intentType, inferredQueries=null) {
let imageMarkdown;
if (intentType === "text-to-image") {
imageMarkdown = `![](data:image/png;base64,${message})`;
} else if (intentType === "text-to-image2") {
imageMarkdown = `![](${message})`;
} else if (intentType === "text-to-image-v3") {
imageMarkdown = `![](data:image/webp;base64,${message})`;
}
const inferredQuery = inferredQueries?.[0];
if (inferredQuery) {
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
return imageMarkdown;
}
function formatHTMLMessage(message, raw=false, willReplace=true) {
var md = window.markdownit();
let newHTML = message;
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for the AI chat model.
newHTML = newHTML.replace(/<s>\[INST\].+(<\/s>)?/g, '');
// Customize the rendering of images
md.renderer.rules.image = function(tokens, idx, options, env, self) {
let token = tokens[idx];
// Add class="text-to-image" to images
token.attrPush(['class', 'text-to-image']);
// Use the default renderer to render image markdown format
return self.renderToken(tokens, idx, options);
};
// Render markdown
newHTML = raw ? newHTML : md.render(newHTML);
// Sanitize the rendered markdown
newHTML = DOMPurify.sanitize(newHTML);
// Set rendered markdown to HTML DOM element
let element = document.createElement('div');
element.innerHTML = newHTML;
element.className = "chat-message-text-response";
// Add a copy button to each chat message
if (willReplace === true) {
let copyButton = document.createElement('button');
copyButton.classList.add("copy-button");
copyButton.title = "Copy Message";
let copyIcon = document.createElement("img");
copyIcon.src = "./assets/icons/copy-button.svg";
copyIcon.classList.add("copy-icon");
copyButton.appendChild(copyIcon);
copyButton.addEventListener('click', createCopyParentText(message));
element.append(copyButton);
}
// Get any elements with a class that starts with "language"
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
// For each element, add a parent div with the class "programmatic-output"
codeBlockElements.forEach((codeElement, key) => {
// Create the parent div
let parentDiv = document.createElement('div');
parentDiv.classList.add("programmatic-output");
// Add the parent div before the code element
codeElement.parentNode.insertBefore(parentDiv, codeElement);
// Move the code element into the parent div
parentDiv.appendChild(codeElement);
// Check if hijs has been loaded
if (typeof hljs !== 'undefined') {
// Highlight the code block
hljs.highlightBlock(codeElement);
}
// Add a copy button to each element
if (willReplace === true) {
let copyButton = document.createElement('button');
copyButton.classList.add("copy-button");
copyButton.title = "Copy Code";
let copyIcon = document.createElement("img");
copyIcon.src = "./assets/icons/copy-button.svg";
copyIcon.classList.add("copy-icon");
copyButton.appendChild(copyIcon);
copyButton.addEventListener('click', copyParentText);
codeElement.prepend(copyButton);
}
});
// Get all code elements that have no class.
let codeElements = element.querySelectorAll('code:not([class])');
codeElements.forEach((codeElement) => {
// Add the class "chat-response" to each element
codeElement.classList.add("chat-response");
});
let anchorElements = element.querySelectorAll('a');
anchorElements.forEach((anchorElement) => {
// Tag external links to open in separate window
if (
!anchorElement.href.startsWith("./") &&
!anchorElement.href.startsWith("#") &&
!anchorElement.href.startsWith("/")
) {
anchorElement.setAttribute('target', '_blank');
anchorElement.setAttribute('rel', 'noopener noreferrer');
}
// Add the class "inline-chat-link" to each element
anchorElement.classList.add("inline-chat-link");
});
return element
}
function createReferenceSection(references) {
let referenceSection = document.createElement('div');
referenceSection.classList.add("reference-section");
referenceSection.classList.add("collapsed");
let numReferences = 0;
if (references.hasOwnProperty("notes")) {
numReferences += references["notes"].length;
references["notes"].forEach((reference, index) => {
let polishedReference = generateReference(reference, index);
referenceSection.appendChild(polishedReference);
});
}
if (references.hasOwnProperty("online")){
numReferences += processOnlineReferences(referenceSection, references["online"]);
}
let referenceExpandButton = document.createElement('button');
referenceExpandButton.classList.add("reference-expand-button");
referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
referenceExpandButton.addEventListener('click', function() {
if (referenceSection.classList.contains("collapsed")) {
referenceSection.classList.remove("collapsed");
referenceSection.classList.add("expanded");
} else {
referenceSection.classList.add("collapsed");
referenceSection.classList.remove("expanded");
}
});
let referencesDiv = document.createElement('div');
referencesDiv.classList.add("references");
referencesDiv.appendChild(referenceExpandButton);
referencesDiv.appendChild(referenceSection);
return referencesDiv;
}
async function chat() {
// Extract required fields for search from form
async function chat(isVoice=false) {
// Extract chat message from chat input form
let query = document.getElementById("chat-input").value.trim();
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
console.log(`Query: ${query}`);
// Short circuit on empty query
@@ -435,9 +113,6 @@
await refreshChatSessionsPanel();
}
// Generate backend API URL to execute query
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
let newResponseEl = document.createElement("div");
newResponseEl.classList.add("chat-message", "khoj");
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
@@ -448,25 +123,7 @@
newResponseEl.appendChild(newResponseTextEl);
// Temporary status message to indicate that Khoj is thinking
let loadingEllipsis = document.createElement("div");
loadingEllipsis.classList.add("lds-ellipsis");
let firstEllipsis = document.createElement("div");
firstEllipsis.classList.add("lds-ellipsis-item");
let secondEllipsis = document.createElement("div");
secondEllipsis.classList.add("lds-ellipsis-item");
let thirdEllipsis = document.createElement("div");
thirdEllipsis.classList.add("lds-ellipsis-item");
let fourthEllipsis = document.createElement("div");
fourthEllipsis.classList.add("lds-ellipsis-item");
loadingEllipsis.appendChild(firstEllipsis);
loadingEllipsis.appendChild(secondEllipsis);
loadingEllipsis.appendChild(thirdEllipsis);
loadingEllipsis.appendChild(fourthEllipsis);
let loadingEllipsis = createLoadingEllipsis();
newResponseTextEl.appendChild(loadingEllipsis);
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
@@ -477,107 +134,36 @@
let chatInput = document.getElementById("chat-input");
chatInput.classList.remove("option-enabled");
// Setup chat message state
chatMessageState = {
newResponseTextEl,
newResponseEl,
loadingEllipsis,
references: {},
rawResponse: "",
rawQuery: query,
isVoice: isVoice,
}
// Call Khoj chat API
let response = await fetch(chatApi, { headers });
let rawResponse = "";
let references = null;
const contentType = response.headers.get("content-type");
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
chatApi += (!!region && !!city && !!countryName && !!timezone)
? `&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
: '';
if (contentType === "application/json") {
// Handle JSON response
try {
const responseAsJson = await response.json();
if (responseAsJson.image) {
// If response has image field, response is a generated image.
if (responseAsJson.intentType === "text-to-image") {
rawResponse += `![${query}](data:image/png;base64,${responseAsJson.image})`;
} else if (responseAsJson.intentType === "text-to-image2") {
rawResponse += `![${query}](${responseAsJson.image})`;
} else if (responseAsJson.intentType === "text-to-image-v3") {
rawResponse += `![${query}](data:image/webp;base64,${responseAsJson.image})`;
}
const inferredQueries = responseAsJson.inferredQueries?.[0];
if (inferredQueries) {
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQueries}`;
}
}
if (responseAsJson.context) {
const rawReferenceAsJson = responseAsJson.context;
references = createReferenceSection(rawReferenceAsJson);
}
if (responseAsJson.detail) {
// If response has detail field, response is an error message.
rawResponse += responseAsJson.detail;
}
} catch (error) {
// If the chunk is not a JSON object, just display it as is
rawResponse += chunk;
} finally {
newResponseTextEl.innerHTML = "";
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
const response = await fetch(chatApi, { headers });
if (references != null) {
newResponseTextEl.appendChild(references);
}
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
document.getElementById("chat-input").removeAttribute("disabled");
}
} else {
// Handle streamed response of type text/event-stream or text/plain
const reader = response.body.getReader();
const decoder = new TextDecoder();
let references = {};
readStream();
function readStream() {
reader.read().then(({ done, value }) => {
if (done) {
// Append any references after all the data has been streamed
if (references != {}) {
newResponseTextEl.appendChild(createReferenceSection(references));
}
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
document.getElementById("chat-input").removeAttribute("disabled");
return;
}
// Decode message chunk from stream
const chunk = decoder.decode(value, { stream: true });
if (chunk.includes("### compiled references:")) {
const additionalResponse = chunk.split("### compiled references:")[0];
rawResponse += additionalResponse;
newResponseTextEl.innerHTML = "";
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
const rawReference = chunk.split("### compiled references:")[1];
const rawReferenceAsJson = JSON.parse(rawReference);
if (rawReferenceAsJson instanceof Array) {
references["notes"] = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
references["online"] = rawReferenceAsJson;
}
readStream();
} else {
// Display response from Khoj
if (newResponseTextEl.getElementsByClassName("lds-ellipsis").length > 0) {
newResponseTextEl.removeChild(loadingEllipsis);
}
// If the chunk is not a JSON object, just display it as is
rawResponse += chunk;
newResponseTextEl.innerHTML = "";
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
readStream();
}
// Scroll to bottom of chat window as chat response is streamed
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
});
}
try {
if (!response.ok) throw new Error(response.statusText);
if (!response.body) throw new Error("Response body is empty");
// Stream and render chat response
await readChatStream(response);
} catch (err) {
console.error(`Khoj chat response failed with\n${err}`);
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️‍🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
newResponseTextEl.textContent = errorMsg;
}
}
@@ -1892,7 +1478,7 @@
div#new-conversation {
display: grid;
grid-auto-flow: column;
font-size: large;
font-size: medium;
text-align: left;
border-bottom: 1px solid var(--main-text-color);
margin: 8px 0;
@@ -1910,7 +1496,7 @@
border: 1px solid var(--main-text-color);
border-radius: 5px;
padding: 5px;
font-size: 14px;
font-size: small;
font-weight: 300;
line-height: 1.5em;
cursor: pointer;
@@ -1918,7 +1504,6 @@
text-align: left;
display: flex;
position: relative;
margin: 0 8px;
}
.three-dot-menu {

View File

@@ -0,0 +1,557 @@
function copyParentText(event, message=null) { //same
const button = event.currentTarget;
const textContent = message ?? button.parentNode.textContent.trim();
navigator.clipboard.writeText(textContent).then(() => {
button.firstChild.src = "./assets/icons/copy-button-success.svg";
setTimeout(() => {
button.firstChild.src = "./assets/icons/copy-button.svg";
}, 1000);
}).catch((error) => {
console.error("Error copying text to clipboard:", error);
const originalButtonText = button.innerHTML;
button.innerHTML = "⛔️";
setTimeout(() => {
button.innerHTML = originalButtonText;
button.firstChild.src = "./assets/icons/copy-button.svg";
}, 2000);
});
}
function createCopyParentText(message) { //same
return function(event) {
copyParentText(event, message);
}
}
function formatDate(date) { //same
// Format date in HH:MM, DD MMM YYYY format
let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false });
let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit'}).replaceAll('-', ' ');
return `${time_string}, ${date_string}`;
}
function generateReference(referenceJson, index) { //same
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
// Escape reference for HTML rendering
let escaped_ref = reference.replaceAll('"', '&quot;');
// Generate HTML for Chat Reference
let short_ref = escaped_ref.slice(0, 100);
short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref;
let referenceButton = document.createElement('button');
referenceButton.textContent = short_ref;
referenceButton.id = `ref-${index}`;
referenceButton.classList.add("reference-button");
referenceButton.classList.add("collapsed");
referenceButton.tabIndex = 0;
// Add event listener to toggle full reference on click
referenceButton.addEventListener('click', function() {
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
this.textContent = escaped_ref;
} else {
this.classList.add("collapsed");
this.classList.remove("expanded");
this.textContent = short_ref;
}
});
return referenceButton;
}
function generateOnlineReference(reference, index) { //same
// Generate HTML for Chat Reference
let title = reference.title || reference.link;
let link = reference.link;
let snippet = reference.snippet;
let question = reference.question;
if (question) {
question = `<b>Question:</b> ${question}<br><br>`;
} else {
question = "";
}
let linkElement = document.createElement('a');
linkElement.setAttribute('href', link);
linkElement.setAttribute('target', '_blank');
linkElement.setAttribute('rel', 'noopener noreferrer');
linkElement.classList.add("inline-chat-link");
linkElement.classList.add("reference-link");
linkElement.setAttribute('title', title);
linkElement.textContent = title;
let referenceButton = document.createElement('button');
referenceButton.innerHTML = linkElement.outerHTML;
referenceButton.id = `ref-${index}`;
referenceButton.classList.add("reference-button");
referenceButton.classList.add("collapsed");
referenceButton.tabIndex = 0;
// Add event listener to toggle full reference on click
referenceButton.addEventListener('click', function() {
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
this.innerHTML = linkElement.outerHTML + `<br><br>${question + snippet}`;
} else {
this.classList.add("collapsed");
this.classList.remove("expanded");
this.innerHTML = linkElement.outerHTML;
}
});
return referenceButton;
}
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") { //same
let message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
let formattedMessage = formatHTMLMessage(message, raw);
// Create a new div for the chat message
let chatMessage = document.createElement('div');
chatMessage.className = `chat-message ${by}`;
chatMessage.dataset.meta = `${by_name} at ${message_time}`;
// Create a new div for the chat message text and append it to the chat message
let chatMessageText = document.createElement('div');
chatMessageText.className = `chat-message-text ${by}`;
chatMessageText.appendChild(formattedMessage);
chatMessage.appendChild(chatMessageText);
// Append annotations div to the chat message
if (annotations) {
chatMessageText.appendChild(annotations);
}
// Append chat message div to chat body
let chatBody = document.getElementById("chat-body");
let body = document.body;
if (renderType === "append") {
chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element
body.scrollTop = chatBody.scrollHeight;
} else if (renderType === "prepend") {
chatBody.insertBefore(chatMessage, chatBody.firstChild);
} else if (renderType === "return") {
return chatMessage;
}
let chatBodyWrapper = document.getElementById("chat-body");
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
}
function processOnlineReferences(referenceSection, onlineContext) { //same
let numOnlineReferences = 0;
for (let subquery in onlineContext) {
let onlineReference = onlineContext[subquery];
if (onlineReference.organic && onlineReference.organic.length > 0) {
numOnlineReferences += onlineReference.organic.length;
for (let index in onlineReference.organic) {
let reference = onlineReference.organic[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
if (onlineReference.knowledgeGraph && onlineReference.knowledgeGraph.length > 0) {
numOnlineReferences += onlineReference.knowledgeGraph.length;
for (let index in onlineReference.knowledgeGraph) {
let reference = onlineReference.knowledgeGraph[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
if (onlineReference.peopleAlsoAsk && onlineReference.peopleAlsoAsk.length > 0) {
numOnlineReferences += onlineReference.peopleAlsoAsk.length;
for (let index in onlineReference.peopleAlsoAsk) {
let reference = onlineReference.peopleAlsoAsk[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
numOnlineReferences += onlineReference.webpages.length;
for (let index in onlineReference.webpages) {
let reference = onlineReference.webpages[index];
let polishedReference = generateOnlineReference(reference, index);
referenceSection.appendChild(polishedReference);
}
}
}
return numOnlineReferences;
}
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) { //same
let chatEl;
if (intentType?.includes("text-to-image")) {
let imageMarkdown = generateImageMarkdown(message, intentType, inferredQueries);
chatEl = renderMessage(imageMarkdown, by, dt, null, false, "return");
} else {
chatEl = renderMessage(message, by, dt, null, false, "return");
}
// If no document or online context is provided, render the message as is
if ((context == null || context?.length == 0)
&& (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
return chatEl;
}
// If document or online context is provided, render the message with its references
let references = {};
if (!!context) references["notes"] = context;
if (!!onlineContext) references["online"] = onlineContext;
let chatMessageEl = chatEl.getElementsByClassName("chat-message-text")[0];
chatMessageEl.appendChild(createReferenceSection(references));
return chatEl;
}
function generateImageMarkdown(message, intentType, inferredQueries=null) { //same
let imageMarkdown;
if (intentType === "text-to-image") {
imageMarkdown = `![](data:image/png;base64,${message})`;
} else if (intentType === "text-to-image2") {
imageMarkdown = `![](${message})`;
} else if (intentType === "text-to-image-v3") {
imageMarkdown = `![](data:image/webp;base64,${message})`;
}
const inferredQuery = inferredQueries?.[0];
if (inferredQuery) {
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
return imageMarkdown;
}
function formatHTMLMessage(message, raw=false, willReplace=true) { //same
var md = window.markdownit();
let newHTML = message;
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for the AI chat model.
newHTML = newHTML.replace(/<s>\[INST\].+(<\/s>)?/g, '');
// Customize the rendering of images
md.renderer.rules.image = function(tokens, idx, options, env, self) {
let token = tokens[idx];
// Add class="text-to-image" to images
token.attrPush(['class', 'text-to-image']);
// Use the default renderer to render image markdown format
return self.renderToken(tokens, idx, options);
};
// Render markdown
newHTML = raw ? newHTML : md.render(newHTML);
// Sanitize the rendered markdown
newHTML = DOMPurify.sanitize(newHTML);
// Set rendered markdown to HTML DOM element
let element = document.createElement('div');
element.innerHTML = newHTML;
element.className = "chat-message-text-response";
// Add a copy button to each chat message
if (willReplace === true) {
let copyButton = document.createElement('button');
copyButton.classList.add("copy-button");
copyButton.title = "Copy Message";
let copyIcon = document.createElement("img");
copyIcon.id = "copy-icon";
copyIcon.src = "./assets/icons/copy-button.svg";
copyIcon.classList.add("copy-icon");
copyButton.appendChild(copyIcon);
copyButton.addEventListener('click', createCopyParentText(message));
element.append(copyButton);
}
// Get any elements with a class that starts with "language"
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
// For each element, add a parent div with the class "programmatic-output"
codeBlockElements.forEach((codeElement, key) => {
// Create the parent div
let parentDiv = document.createElement('div');
parentDiv.classList.add("programmatic-output");
// Add the parent div before the code element
codeElement.parentNode.insertBefore(parentDiv, codeElement);
// Move the code element into the parent div
parentDiv.appendChild(codeElement);
// Add a copy button to each element
});
// Get all code elements that have no class.
let codeElements = element.querySelectorAll('code:not([class])');
codeElements.forEach((codeElement) => {
// Add the class "chat-response" to each element
codeElement.classList.add("chat-response");
});
let anchorElements = element.querySelectorAll('a');
anchorElements.forEach((anchorElement) => {
// Tag external links to open in separate window
if (
!anchorElement.href.startsWith("./") &&
!anchorElement.href.startsWith("#") &&
!anchorElement.href.startsWith("/")
) {
anchorElement.setAttribute('target', '_blank');
anchorElement.setAttribute('rel', 'noopener noreferrer');
}
// Add the class "inline-chat-link" to each element
anchorElement.classList.add("inline-chat-link");
});
return element
}
function createReferenceSection(references, createLinkerSection=false) {
console.log("linker data: ", createLinkerSection);
let referenceSection = document.createElement('div');
referenceSection.classList.add("reference-section");
referenceSection.classList.add("collapsed");
let numReferences = 0;
if (references.hasOwnProperty("notes")) {
numReferences += references["notes"].length;
references["notes"].forEach((reference, index) => {
let polishedReference = generateReference(reference, index);
referenceSection.appendChild(polishedReference);
});
}
if (references.hasOwnProperty("online")){
numReferences += processOnlineReferences(referenceSection, references["online"]);
}
let referenceExpandButton = document.createElement('button');
referenceExpandButton.id = "reference-expand-button";
referenceExpandButton.classList.add("reference-expand-button");
referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
referenceExpandButton.addEventListener('click', function() {
if (referenceSection.classList.contains("collapsed")) {
referenceSection.classList.remove("collapsed");
referenceSection.classList.add("expanded");
} else {
referenceSection.classList.add("collapsed");
referenceSection.classList.remove("expanded");
}
});
let referencesDiv = document.createElement('div');
referencesDiv.classList.add("references");
referencesDiv.appendChild(referenceExpandButton);
if (createLinkerSection) {
//add a linker button back to the desktop application
let linkerButton = document.createElement('button');
linkerButton.innerHTML = "Continue Conversation";
linkerButton.id = "linker-button";
linkerButton.addEventListener('click', function() {
window.routeBackToMainWindowAPI.sendSignal();
});
referencesDiv.appendChild(linkerButton);
console.log("shortcut window");
}
referencesDiv.appendChild(referenceSection);
return referencesDiv;
}
function createLoadingEllipsis() {
let loadingEllipsis = document.createElement("div");
loadingEllipsis.classList.add("lds-ellipsis");
let firstEllipsis = document.createElement("div");
firstEllipsis.classList.add("lds-ellipsis-item");
let secondEllipsis = document.createElement("div");
secondEllipsis.classList.add("lds-ellipsis-item");
let thirdEllipsis = document.createElement("div");
thirdEllipsis.classList.add("lds-ellipsis-item");
let fourthEllipsis = document.createElement("div");
fourthEllipsis.classList.add("lds-ellipsis-item");
loadingEllipsis.appendChild(firstEllipsis);
loadingEllipsis.appendChild(secondEllipsis);
loadingEllipsis.appendChild(thirdEllipsis);
loadingEllipsis.appendChild(fourthEllipsis);
return loadingEllipsis;
}
function handleStreamResponse(newResponseElement, rawResponse, rawQuery, loadingEllipsis, replace=true) {
if (!newResponseElement) return;
// Remove loading ellipsis if it exists
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
newResponseElement.removeChild(loadingEllipsis);
// Clear the response element if replace is true
if (replace) newResponseElement.innerHTML = "";
// Append response to the response element
newResponseElement.appendChild(formatHTMLMessage(rawResponse, false, replace, rawQuery));
// Append loading ellipsis if it exists
if (!replace && loadingEllipsis) newResponseElement.appendChild(loadingEllipsis);
// Scroll to bottom of chat view
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
}
function handleImageResponse(imageJson, rawResponse) {
if (imageJson.image) {
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
// If response has image field, response is a generated image.
if (imageJson.intentType === "text-to-image") {
rawResponse += `![generated_image](data:image/png;base64,${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
}
if (inferredQuery) {
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
}
// If response has detail field, response is an error message.
if (imageJson.detail) rawResponse += imageJson.detail;
return rawResponse;
}
function finalizeChatBodyResponse(references, newResponseElement) {
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
newResponseElement.appendChild(createReferenceSection(references));
}
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
document.getElementById("chat-input")?.removeAttribute("disabled");
}
function convertMessageChunkToJson(rawChunk) {
// Split the chunk into lines
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
try {
let jsonChunk = JSON.parse(rawChunk);
if (!jsonChunk.type)
jsonChunk = {type: 'message', data: jsonChunk};
return jsonChunk;
} catch (e) {
return {type: 'message', data: rawChunk};
}
} else if (rawChunk.length > 0) {
return {type: 'message', data: rawChunk};
}
}
function processMessageChunk(rawChunk) {
const chunk = convertMessageChunkToJson(rawChunk);
console.debug("Chunk:", chunk);
if (!chunk || !chunk.type) return;
if (chunk.type ==='status') {
console.log(`status: ${chunk.data}`);
const statusMessage = chunk.data;
handleStreamResponse(chatMessageState.newResponseTextEl, statusMessage, chatMessageState.rawQuery, chatMessageState.loadingEllipsis, false);
} else if (chunk.type === 'start_llm_response') {
console.log("Started streaming", new Date());
} else if (chunk.type === 'end_llm_response') {
console.log("Stopped streaming", new Date());
// Automatically respond with voice if the subscribed user has sent voice message
if (chatMessageState.isVoice && "{{ is_active }}" == "True")
textToSpeech(chatMessageState.rawResponse);
// Append any references after all the data has been streamed
finalizeChatBodyResponse(chatMessageState.references, chatMessageState.newResponseTextEl);
const liveQuery = chatMessageState.rawQuery;
// Reset variables
chatMessageState = {
newResponseTextEl: null,
newResponseEl: null,
loadingEllipsis: null,
references: {},
rawResponse: "",
rawQuery: liveQuery,
isVoice: false,
}
} else if (chunk.type === "references") {
chatMessageState.references = {"notes": chunk.data.context, "online": chunk.data.onlineContext};
} else if (chunk.type === 'message') {
const chunkData = chunk.data;
if (typeof chunkData === 'object' && chunkData !== null) {
// If chunkData is already a JSON object
handleJsonResponse(chunkData);
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
// Try process chunk data as if it is a JSON object
try {
const jsonData = JSON.parse(chunkData.trim());
handleJsonResponse(jsonData);
} catch (e) {
chatMessageState.rawResponse += chunkData;
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
}
} else {
chatMessageState.rawResponse += chunkData;
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
}
}
}
function handleJsonResponse(jsonData) {
if (jsonData.image || jsonData.detail) {
chatMessageState.rawResponse = handleImageResponse(jsonData, chatMessageState.rawResponse);
} else if (jsonData.response) {
chatMessageState.rawResponse = jsonData.response;
}
if (chatMessageState.newResponseTextEl) {
chatMessageState.newResponseTextEl.innerHTML = "";
chatMessageState.newResponseTextEl.appendChild(formatHTMLMessage(chatMessageState.rawResponse));
}
}
async function readChatStream(response) {
if (!response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
const eventDelimiter = '␃🔚␗';
let buffer = '';
while (true) {
const { value, done } = await reader.read();
// If the stream is done
if (done) {
// Process the last chunk
processMessageChunk(buffer);
buffer = '';
break;
}
// Read chunk from stream and append it to the buffer
const chunk = decoder.decode(value, { stream: true });
console.debug("Raw Chunk:", chunk)
// Start buffering chunks until complete event is received
buffer += chunk;
// Once the buffer contains a complete event
let newEventIndex;
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
// Extract the event from the buffer
const event = buffer.slice(0, newEventIndex);
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
// Process the event
if (event) processMessageChunk(event);
}
}
}

View File

@@ -19,7 +19,7 @@ const textFileTypes = [
'org', 'md', 'markdown', 'txt', 'html', 'xml',
// Other valid text file extensions from https://google.github.io/magika/model/config.json
'appleplist', 'asm', 'asp', 'batch', 'c', 'cs', 'css', 'csv', 'eml', 'go', 'html', 'ini', 'internetshortcut', 'java', 'javascript', 'json', 'latex', 'lisp', 'makefile', 'markdown', 'mht', 'mum', 'pem', 'perl', 'php', 'powershell', 'python', 'rdf', 'rst', 'rtf', 'ruby', 'rust', 'scala', 'shell', 'smali', 'sql', 'svg', 'symlinktext', 'txt', 'vba', 'winregistry', 'xml', 'yaml']
const binaryFileTypes = ['pdf']
const binaryFileTypes = ['pdf', 'jpg', 'jpeg', 'png']
const validFileTypes = textFileTypes.concat(binaryFileTypes);
const schema = {
@@ -233,11 +233,15 @@ function pushDataToKhoj (regenerate = false) {
// Request indexing files on server. With upto 1000 files in each request
for (let i = 0; i < filesDataToPush.length; i += 1000) {
const syncUrl = `${hostURL}/api/content?client=desktop`;
const filesDataGroup = filesDataToPush.slice(i, i + 1000);
const formData = new FormData();
filesDataGroup.forEach(fileData => { formData.append('files', fileData.blob, fileData.path) });
let request = axios.post(`${hostURL}/api/v1/index/update?force=${regenerate}&client=desktop`, formData, { headers });
requests.push(request);
requests.push(
regenerate
? axios.put(syncUrl, formData, { headers })
: axios.patch(syncUrl, formData, { headers })
);
}
// Wait for requests batch to finish
@@ -253,7 +257,7 @@ function pushDataToKhoj (regenerate = false) {
console.error(error);
state["completed"] = false;
if (error?.response?.status === 429 && (BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')))) {
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/config">Upgrade your plan</a> to unlock more space.`;
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/settings#subscription">Upgrade your plan</a> to unlock more space.`;
const win = BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config'));
if (win) win.webContents.send('needsSubscription', true);
} else if (error?.code === 'ECONNREFUSED') {
@@ -431,6 +435,9 @@ function addCSPHeaderToSession () {
let firstRun = true;
let win = null;
let titleBarStyle = process.platform === 'win32' ? 'default' : 'hidden';
const {globalShortcut, clipboard} = require('electron'); // global shortcut and clipboard dependencies for shortcut window
const openShortcutWindowKeyBind = 'CommandOrControl+Shift+K'
const createWindow = (tab = 'chat.html') => {
win = new BrowserWindow({
width: 800,
@@ -506,6 +513,48 @@ const createWindow = (tab = 'chat.html') => {
}
}
const createShortcutWindow = (tab = 'shortcut.html') => {
var shortcutWin = new BrowserWindow({
width: 400,
height: 600,
show: false,
titleBarStyle: titleBarStyle,
autoHideMenuBar: true,
frame: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
}
});
shortcutWin.setMenuBarVisibility(false);
shortcutWin.setResizable(false);
shortcutWin.setOpacity(0.95);
shortcutWin.setBackgroundColor('#f5f4f3');
shortcutWin.setHasShadow(true);
shortcutWin.setVibrancy('popover');
shortcutWin.loadFile(tab);
shortcutWin.once('ready-to-show', () => {
shortcutWin.show();
});
shortcutWin.on('closed', () => {
shortcutWin = null;
});
return shortcutWin;
};
function isShortcutWindowOpen() {
const windows = BrowserWindow.getAllWindows();
for (let i = 0; i < windows.length; i++) {
if (windows[i].webContents.getURL().endsWith('shortcut.html')) {
return true;
}
}
return false;
}
app.whenReady().then(() => {
addCSPHeaderToSession();
@@ -551,14 +600,13 @@ app.whenReady().then(() => {
});
ipcMain.handle('deleteAllFiles', deleteAllFiles);
createWindow();
const mainWindow = createWindow();
app.setAboutPanelOptions({
applicationName: "Khoj",
applicationVersion: khojPackage.version,
version: khojPackage.version,
authors: "Saba Imran, Debanjum Singh Solanky and contributors",
authors: "Khoj AI",
website: "https://khoj.dev",
copyright: "GPL v3",
iconPath: path.join(__dirname, 'assets', 'icons', 'favicon-128x128.png')
@@ -575,9 +623,43 @@ app.whenReady().then(() => {
console.warn("Desktop app update check failed:", e);
}
})
globalShortcut.register(openShortcutWindowKeyBind, () => {
console.log("Shortcut key pressed")
if(isShortcutWindowOpen()) return;
const shortcutWin = createShortcutWindow(); // Create a new shortcut window each time the shortcut is triggered
shortcutWin.setAlwaysOnTop(true, 'screen-saver', 1);
const clipboardText = clipboard.readText();
console.log('Sending clipboard text:', clipboardText); // Debug log
shortcutWin.webContents.once('dom-ready', () => {
shortcutWin.webContents.send('clip', clipboardText);
console.log('Message sent to window'); // Debug log
});
// Register a global shortcut for the Escape key for the shortcutWin
globalShortcut.register('Escape', () => {
if (shortcutWin) {
shortcutWin.close();
}
// Unregister the Escape key shortcut
globalShortcut.unregister('Escape');
});
shortcutWin.on('closed', () => {
// Unregister the Escape key shortcut
globalShortcut.unregister('Escape');
});
ipcMain.on('continue-conversation-button-clicked', () => {
openWindow('chat.html');
if (shortcutWin && !shortcutWin.isDestroyed()) {
shortcutWin.close();
}
// Unregister the Escape key shortcut
globalShortcut.unregister('Escape');
});
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

View File

@@ -1,8 +1,8 @@
{
"name": "Khoj",
"version": "1.14.0",
"description": "An AI copilot for your Second Brain",
"author": "Saba Imran, Debanjum Singh Solanky <team@khoj.dev>",
"version": "1.21.3",
"description": "Your Second Brain",
"author": "Khoj Inc. <team@khoj.dev>",
"license": "GPL-3.0-or-later",
"homepage": "https://khoj.dev",
"repository": "\"https://github.com/khoj-ai/khoj\"",
@@ -16,8 +16,8 @@
"start": "yarn electron ."
},
"dependencies": {
"@todesktop/runtime": "^1.3.0",
"axios": "^1.6.4",
"@todesktop/runtime": "^1.6.4",
"axios": "^1.7.4",
"cron": "^2.4.3",
"electron-store": "^8.1.0"
}

View File

@@ -15,6 +15,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
contextBridge.exposeInMainWorld('clipboardAPI', {
sendClipboardText: (callback) => {
ipcRenderer.on('clip', (event, message) => {
callback(message);
});
}
});
contextBridge.exposeInMainWorld('routeBackToMainWindowAPI', {
sendSignal: () => {
ipcRenderer.send('continue-conversation-button-clicked'); // Custom event name
}
});
contextBridge.exposeInMainWorld('storeValueAPI', {
handleFileOpen: (key) => ipcRenderer.invoke('handleFileOpen', key)
})

View File

@@ -182,7 +182,7 @@ window.updateStateAPI.onUpdateState((event, state) => {
window.needsSubscriptionAPI.onNeedsSubscription((event, needsSubscription) => {
console.log("needs subscription", needsSubscription);
if (needsSubscription) {
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/config");
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/settings#subscription");
needsSubscriptionElement.style.display = 'block';
}
});

View File

@@ -212,12 +212,12 @@
const headers = { 'Authorization': `Bearer ${khojToken}` };
// Populate type dropdown field with enabled content types only
fetch(`${hostURL}/api/config/types`, { headers })
fetch(`${hostURL}/api/content/types`, { headers })
.then(response => response.json())
.then(enabled_types => {
// Show warning if no content types are enabled
if (enabled_types.detail) {
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>.</div>";
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/settings'>settings page</a>.</div>";
document.getElementById("query").setAttribute("disabled", "disabled");
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
return [];

View File

@@ -0,0 +1,428 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Khoj Mini</title>
<style>
#title-bar {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 20px;
background-color: #f9f5de;
color: black;
-webkit-app-region: drag;
z-index: 9999;
margin: 0;
padding-left: 7px;
padding-right: 7px;
padding-top: 5px;
padding-bottom: 5px;
font-family: 'Noto Sans', sans-serif;
}
#loading-dots {
padding-top: 10px;
text-align: center;
font-size: 16px;
font-family: 'Noto Sans', sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
#styled-input {
width: 100%;
font-size: 14px;
height: 100px;
min-height: 50px;
background-color: #475569; /* Blue background */
color: #dcdfe4; /* White text */
font-family: 'Noto Sans', sans-serif;
border: none;
resize: vertical;
overflow: hidden;
}
.chat-input {
margin-top: 10px;
position: fixed;
bottom: 0;
left: 5;
width: 100%;
max-width: 600px;
display: flex;
padding-top: 10px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
#input-container {
background-color: #475569;
border-radius: 5px;
padding: 10px;
margin-top: 50px;
}
#send-button {
border: none;
border-radius: 5px 5px 5px 5px;
margin-top: 5px;
background-color: #5a6b84;
color: white;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
font-family: 'Noto Sans', sans-serif;
/* font-weight: bold; */
position: relative;
margin-right: 10px;
}
#send-button:hover {
background: #7489a9;
}
#edit-button {
border: none;
border-radius: 5px 5px 5px 5px;
margin-top: 5px;
background-color: #5a6b84;
color: white;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
font-family: 'Noto Sans', sans-serif;
/* font-weight: bold; */
position: relative;
}
#edit-button:hover {
background: #7489a9;
}
::-webkit-scrollbar {
width: 5px; /* Width of the scrollbar */
height: 5px;
}
/* * {
outline: 1px solid rgb(255, 255, 255);
} */
/* Track */
::-webkit-scrollbar-track {
background: #f1f1f1; /* Background of the scrollbar track */
border-radius: 10px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #2d2d2d; /* Color of the scrollbar thumb */
border-radius: 10px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #000000; /* Color of the scrollbar thumb on hover */
}
#copy-icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
#reference-expand-button{
background-color: #000000;
color: #dcdfe4; /* White text */
font-family: 'Noto Sans', sans-serif;
border-radius: 5px;
font-size: 16px;
border: none;
padding: 5px;
margin: 5px;
}
#linker-button{
background-color: #fee285;
color: black; /* White text */
font-family: 'Noto Sans', sans-serif;
border-radius: 5px;
font-size: 16px;
border: none;
padding: 5px;
margin: 5px;
}
#linker-button:hover {
background-color: #f9f5de;
}
div.collapsed {
display: none;
}
div.expanded {
display: block;
}
/* CSS for the container */
.logo-container {
/* display: flex; Use flexbox to align items */
align-items: center; /* Align items vertically */
justify-content: flex;
padding: 10px; /* Add padding to the container */
}
/* CSS for the image */
img {
width: 100px; /* Set the desired width */
height: auto; /* Allows the image to scale proportionally */
}
.clipboardText {
font-size: 14px;
font-family: 'Noto Sans', sans-serif;
background-color: #475569;
color: #dcdfe4;
padding: 10px;
border-radius: 5px;
width: 100%;
word-wrap: break-word;
}
.reference-button {
background-color: #dde5f0;
color: #dcdfe4;
font-family: 'Noto Sans', sans-serif;
border-radius: 5px;
font-size: 16px;
border: none;
padding: 5px;
margin: 5px;
}
#chat-body {
width: 100%;
font-family: 'Noto Sans', sans-serif;
word-wrap: break-word;
line-height: 20px;
}
b {
font-size: 14px;
font-family: 'Noto Sans', sans-serif;
}
h1 {
font-size: 20px;
font-family: 'Noto Sans', sans-serif;
text-align: center;
}
body {
background-color: #ffffff;
font-family:'Noto Sans', sans-serif;
font-weight: 400;
scrollbar-width: 5px; /* "auto" or "thin" */
scrollbar-color: white white;/* thumb color and track color */
height: 300px;
overflow: scroll;
}
#chat-body-wrapper {
padding-left: 10px;
padding-right: 10px;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
#loading-dots span {
display: inline-block;
animation: bounce 0.9s infinite alternate;
}
.dot1 {
animation-delay: 0.1s;
}
.dot2 {
animation-delay: 0.2s;
}
.dot3 {
animation-delay: 0.3s;
}
</style>
</head>
<body>
<div id="title-bar">Khoj (Esc to Quit)</div>
<div id="chat-body-wrapper">
<div id="input-container">
<textarea id="styled-input" name="styled-input">Hello World!</textarea>
<script>
try {
if (!window.clipboardAPI) {
throw new Error('clipboardAPI is not available');
}
window.clipboardAPI.sendClipboardText((clipboardText) => {
try {
const styledInput = document.getElementById('styled-input');
if (!styledInput) {
throw new Error('styled-input element not found');
}
styledInput.value = clipboardText;
console.log("success: ", clipboardText);
} catch (error) {
console.error('Error handling clipboard text:', error);
}
});
} catch (error) {
console.error('Error setting up clipboard listener:', error);
}
</script>
<div style="display: flex;">
<button id="send-button" onclick="chat()">
Send
<svg style="margin-left: 3px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send">
<path d="M5 12l10 0-5-5m5 5-5 5" />
</svg>
</button>
<button id="edit-button" onclick="edit()">
Edit
<svg style="margin-left: 6px; margin-right: 6px;" fill="#fff" height="11px" width="11px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512 512">
<g>
<g>
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
</g>
</g>
</svg>
</button>
</div>
</div>
<div id="loading-dots" style="display: none;"></div>
<div id="chat-body"></div>
</div>
<script src="main.js"></script>
<script type="text/javascript" src="./assets/purify.min.js?v={{ khoj_version }}"></script>
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
<script src="./utils.js"></script>
<script src="./chatutils.js"></script>
<script>
let region = null;
let city = null;
let countryName = null;
let timezone = null;
fetch("https://ipapi.co/json")
.then(response => response.json())
.then(data => {
region = data.region;
city = data.city;
countryName = data.country_name;
timezone = data.timezone;
})
.catch(err => {
console.log(err);
return;
});
function toggleLoading() {
var dots = document.getElementById('loading-dots');
if (dots.style.display === 'none') {
dots.innerHTML = 'Loading<span class="dot1">.</span><span class="dot2">.</span><span class="dot3">.</span>';
dots.style.display = 'inline-block';
} else {
dots.innerHTML = '';
dots.style.display = 'none';
}
}
function edit() {
//enable input for text area
let inp = document.getElementById("styled-input");
inp.removeAttribute('readonly');
//put focus on text area
inp.focus();
}
async function chat(isVoice=false) {
//set chat body to empty
let chatBody = document.getElementById("chat-body");
chatBody.innerHTML = "";
toggleLoading();
let inp = document.getElementById("styled-input");
query = inp.value;
inp.setAttribute('readonly', true);
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
console.log(`Query: ${query}`);
// Short circuit on empty query
if (query.length === 0)
return;
let chat_body = document.getElementById("chat-body");
let conversationID = chat_body.dataset.conversationId;
let hostURL = await window.hostURLAPI.getURL();
const khojToken = await window.tokenAPI.getToken();
const headers = { 'Authorization': `Bearer ${khojToken}` };
if (!conversationID) {
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers });
let data = await response.json();
conversationID = data.conversation_id;
chat_body.dataset.conversationId = conversationID;
}
let newResponseEl = document.createElement("div");
newResponseEl.classList.add("chat-message", "khoj");
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
chat_body.appendChild(newResponseEl);
let newResponseTextEl = document.createElement("div");
newResponseTextEl.classList.add("chat-message-text", "khoj");
newResponseEl.appendChild(newResponseTextEl);
// Temporary status message to indicate that Khoj is thinking
let loadingEllipsis = createLoadingEllipsis();
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
toggleLoading();
// Setup chat message state
chatMessageState = {
newResponseTextEl,
newResponseEl,
loadingEllipsis,
references: {},
rawResponse: "",
rawQuery: query,
isVoice: isVoice,
}
// Construct API URL to execute chat query
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
chatApi += (!!region && !!city && !!countryName && !!timezone)
? `&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
: '';
const response = await fetch(chatApi, { headers });
try {
if (!response.ok) throw new Error(response.statusText);
if (!response.body) throw new Error("Response body is empty");
// Stream and render chat response
await readChatStream(response);
} catch (err) {
console.error(`Khoj chat response failed with\n${err}`);
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️‍🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
newResponseTextEl.textContent = errorMsg;
}
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
}
</script>
</body>
</html>

View File

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

View File

@@ -34,8 +34,8 @@ function toggleNavMenu() {
document.addEventListener('click', function(event) {
let menu = document.getElementById("khoj-nav-menu");
let menuContainer = document.getElementById("khoj-nav-menu-container");
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
if (isClickOnMenu === false && menu.classList.contains("show")) {
let isClickOnMenu = menuContainer?.contains(event.target) || menuContainer === event.target;
if (menu && isClickOnMenu === false && menu.classList.contains("show")) {
menu.classList.remove("show");
}
});
@@ -85,7 +85,7 @@ async function populateHeaderPane() {
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
<div class="khoj-nav-username"> ${username} </div>
<a id="github-nav" class="khoj-nav" href="https://github.com/khoj-ai/khoj">GitHub</a>
<a id="settings-nav" class="khoj-nav" href="./config.html">⚙️ Settings</a>
<a id="settings-nav" class="khoj-nav" href="./settings.html">⚙️ Settings</a>
</div>
</div>
` : ''}

View File

@@ -3,9 +3,9 @@
"@electron/get@^2.0.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.2.tgz#ae2a967b22075e9c25aaf00d5941cd79c21efd7e"
integrity sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==
version "2.0.3"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960"
integrity sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==
dependencies:
debug "^4.1.1"
env-paths "^2.2.0"
@@ -50,10 +50,10 @@
dependencies:
defer-to-connect "^2.0.0"
"@todesktop/runtime@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@todesktop/runtime/-/runtime-1.3.0.tgz#7baa64fd5c2e4daa591bda96270a0e39947ec3c7"
integrity sha512-a5USs4VxnqvtNqFR6F3bCaQ56W6WFO4VOPPaXefCYiCxcsFMYb4IulXGkYjvcpkU/MFGWzmVnzba6UwK7eQMUQ==
"@todesktop/runtime@^1.6.4":
version "1.6.4"
resolved "https://registry.yarnpkg.com/@todesktop/runtime/-/runtime-1.6.4.tgz#a9d62a021cf2647c51371c892bfb1d4c5a29ed7e"
integrity sha512-n6dOxhrKKsXMM+i2u9iRvoJSR2KCWw0orYK+FT9RbWNPykhuFIYd0yy8dYgYy/OuClKGyGl4SJFi2757FLhWDA==
dependencies:
del "^6.0.0"
electron-updater "^4.6.1"
@@ -73,9 +73,9 @@
"@types/responselike" "^1.0.0"
"@types/http-cache-semantics@*":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
"@types/keyv@^3.1.4":
version "3.1.4"
@@ -85,36 +85,40 @@
"@types/node" "*"
"@types/luxon@~3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.1.tgz#08727da7d81ee6a6c702b9dc6c8f86be010eb4dc"
integrity sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==
version "3.3.8"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.8.tgz#84dbf2d020a9209a272058725e168f21d331a67e"
integrity sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==
"@types/node@*":
version "20.5.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377"
integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==
version "20.14.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.8.tgz#45c26a2a5de26c3534a9504530ddb3b27ce031ac"
integrity sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==
dependencies:
undici-types "~5.26.4"
"@types/node@^18.11.18":
version "18.17.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.12.tgz#c6bd7413a13e6ad9cfb7e97dd5c4e904c1821e50"
integrity sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==
version "18.19.39"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.39.tgz#c316340a5b4adca3aee9dcbf05de385978590593"
integrity sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==
dependencies:
undici-types "~5.26.4"
"@types/responselike@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
version "1.0.3"
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50"
integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==
dependencies:
"@types/node" "*"
"@types/semver@^7.3.6":
version "7.5.1"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367"
integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==
version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
"@types/yauzl@^2.9.1":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
version "2.10.3"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
dependencies:
"@types/node" "*"
@@ -134,14 +138,14 @@ ajv-formats@^2.1.1:
ajv "^8.0.0"
ajv@^8.0.0, ajv@^8.6.3:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
version "8.16.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4"
integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==
dependencies:
fast-deep-equal "^3.1.1"
fast-deep-equal "^3.1.3"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
uri-js "^4.4.1"
argparse@^2.0.1:
version "2.0.1"
@@ -163,12 +167,12 @@ atomically@^1.7.0:
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
axios@^1.6.4:
version "1.6.5"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8"
integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==
axios@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2"
integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==
dependencies:
follow-redirects "^1.15.4"
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
@@ -190,12 +194,12 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
buffer-crc32@~0.2.3:
version "0.2.13"
@@ -269,9 +273,9 @@ conf@^10.2.0:
semver "^7.3.5"
cron@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/cron/-/cron-2.4.3.tgz#4e43d8d9a6373b8f28d876c4e9a47c14422d8652"
integrity sha512-YBvExkQYF7w0PxyeFLRyr817YVDhGxaCi5/uRRMqa4aWD3IFKRd+uNbpW1VWMdqQy8PZ7CElc+accXJcauPKzQ==
version "2.4.4"
resolved "https://registry.yarnpkg.com/cron/-/cron-2.4.4.tgz#988c1757b3f288d1dfcc360ee6d80087448916dc"
integrity sha512-MHlPImXJj3K7x7lyUHjtKEOl69CSlTOWxS89jiFgNkzXfvhVjhMz/nc7/EIfN9vgooZp8XTtXJ1FREdmbyXOiQ==
dependencies:
"@types/luxon" "~3.3.0"
luxon "~3.3.0"
@@ -293,9 +297,9 @@ debounce-fn@^4.0.0:
mimic-fn "^3.0.0"
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
version "4.3.5"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e"
integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
dependencies:
ms "2.1.2"
@@ -311,11 +315,21 @@ defer-to-connect@^2.0.0:
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==
define-properties@^1.1.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5"
integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==
define-data-property@^1.0.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
define-properties@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
dependencies:
define-data-property "^1.0.1"
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
@@ -358,9 +372,9 @@ dot-prop@^6.0.1:
is-obj "^2.0.0"
electron-store@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.1.0.tgz#46a398f2bd9aa83c4a9daaae28380e2b3b9c7597"
integrity sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==
version "8.2.0"
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.2.0.tgz#114e6e453e8bb746ab4ccb542424d8c881ad2ca1"
integrity sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==
dependencies:
conf "^10.2.0"
type-fest "^2.17.0"
@@ -400,6 +414,18 @@ env-paths@^2.2.0, env-paths@^2.2.1:
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
es-define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
dependencies:
get-intrinsic "^1.2.4"
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es6-error@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
@@ -441,15 +467,15 @@ extract-zip@^2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
fast-deep-equal@^3.1.1:
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.9:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -458,9 +484,9 @@ fast-glob@^3.2.9:
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
version "1.17.1"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
dependencies:
reusify "^1.0.4"
@@ -471,10 +497,10 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
@@ -485,10 +511,10 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
follow-redirects@^1.15.4:
version "1.15.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
form-data@^4.0.0:
version "4.0.0"
@@ -522,20 +548,21 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
get-stream@^5.1.0:
version "5.2.0"
@@ -581,11 +608,12 @@ global-agent@^3.0.0:
serialize-error "^7.0.1"
globalthis@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
version "1.0.4"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
dependencies:
define-properties "^1.1.3"
define-properties "^1.2.1"
gopd "^1.0.1"
globby@^11.0.1:
version "11.1.0"
@@ -599,6 +627,13 @@ globby@^11.0.1:
merge2 "^1.4.1"
slash "^3.0.0"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
dependencies:
get-intrinsic "^1.1.3"
got@^11.8.5:
version "11.8.6"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
@@ -622,28 +657,28 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
has-property-descriptors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
get-intrinsic "^1.1.1"
es-define-property "^1.0.0"
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
has-symbols@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
hasown@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.1"
function-bind "^1.1.2"
http-cache-semantics@^4.0.0:
version "4.1.1"
@@ -664,9 +699,9 @@ human-signals@^2.1.0:
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
ignore@^5.2.0:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
indent-string@^4.0.0:
version "4.0.0"
@@ -772,9 +807,9 @@ jsonfile@^6.0.1:
graceful-fs "^4.1.6"
keyv@^4.0.0:
version "4.5.3"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25"
integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
dependencies:
json-buffer "3.0.1"
@@ -811,13 +846,6 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
luxon@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
@@ -841,11 +869,11 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
version "4.0.7"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
dependencies:
braces "^3.0.2"
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
@@ -1010,9 +1038,9 @@ pump@^3.0.0:
once "^1.3.1"
punycode@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
queue-microtask@^1.2.2:
version "1.2.3"
@@ -1073,9 +1101,9 @@ run-parallel@^1.1.9:
queue-microtask "^1.2.2"
sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
version "1.4.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
semver-compare@^1.0.0:
version "1.0.0"
@@ -1088,11 +1116,9 @@ semver@^6.2.0:
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.2, semver@^7.3.5:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
version "7.6.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
serialize-error@^7.0.1:
version "7.0.1"
@@ -1124,9 +1150,9 @@ slash@^3.0.0:
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
sprintf-js@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
version "1.1.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
strip-final-newline@^2.0.0:
version "2.0.0"
@@ -1157,17 +1183,22 @@ type-fest@^2.17.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
uri-js@^4.2.2:
uri-js@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
@@ -1186,11 +1217,6 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"

View File

@@ -1,12 +1,12 @@
;;; khoj.el --- AI copilot for your Second Brain -*- lexical-binding: t -*-
;;; khoj.el --- Your Second Brain -*- lexical-binding: t -*-
;; Copyright (C) 2021-2023 Khoj Inc.
;; Author: Debanjum Singh Solanky <debanjum@khoj.dev>
;; Saba Imran <saba@khoj.dev>
;; Description: An AI copilot for your Second Brain
;; Keywords: search, chat, org-mode, outlines, markdown, pdf, image
;; Version: 1.14.0
;; Description: Your Second Brain
;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image
;; Version: 1.21.3
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
@@ -29,19 +29,20 @@
;;; Commentary:
;; Create an AI copilot to your `org-mode', `markdown' notes,
;; PDFs and images. The copilot exposes 2 modes, search and chat:
;; Bootstrap your Second Brain from your `org-mode', `markdown' notes,
;; PDFs and images. Khoj exposes 2 modes, search and chat:
;;
;; Chat provides faster answers, iterative discovery and assisted
;; creativity. It requires your OpenAI API key to access GPT models
;; creativity.
;;
;; Search allows natural language, incremental and local search.
;; It relies on AI models that run locally on your machine.
;; Search allows natural language, incremental search.
;;
;; Quickstart
;; -------------
;; 1. Install khoj.el from MELPA Stable
;; (use-package khoj :pin melpa-stable :bind ("C-c s" . 'khoj))
;; 2. Set API key from https://app.khoj.dev/settings#clients (if not self-hosting)
;; (setq khoj-api-key "YOUR_KHOJ_API_KEY")
;; 2. Start khoj from Emacs
;; C-c s or M-x khoj
;;
@@ -83,7 +84,7 @@
:group 'khoj
:type 'integer)
(defcustom khoj-results-count 5
(defcustom khoj-results-count 8
"Number of results to show in search and use for chat responses."
:group 'khoj
:type 'integer)
@@ -93,8 +94,13 @@
:group 'khoj
:type 'number)
(defcustom khoj-auto-find-similar t
"Should try find similar notes automatically."
:group 'khoj
:type 'boolean)
(defcustom khoj-api-key nil
"API Key to your Khoj. Default at https://app.khoj.dev/config#clients."
"API Key to your Khoj. Default at https://app.khoj.dev/settings#clients."
:group 'khoj
:type 'string)
@@ -158,28 +164,18 @@ NO-PAGING FILTER))
(defun khoj--keybindings-info-message ()
"Show available khoj keybindings in-context, when khoj invoked."
(let ((enabled-content-types (khoj--get-enabled-content-types)))
(concat
"
(concat
"
Set Content Type
-------------------------\n"
("C-c RET | improve sort \n")
(when (member 'markdown enabled-content-types)
"C-x m | markdown\n")
(when (member 'org enabled-content-types)
"C-x o | org-mode\n")
(when (member 'image enabled-content-types)
"C-x i | image\n")
(when (member 'pdf enabled-content-types)
"C-x p | pdf\n"))))
"C-c RET | improve sort \n"))
(defvar khoj--rerank nil "Track when re-rank of results triggered.")
(defvar khoj--reference-count 0 "Track number of references currently in chat bufffer.")
(defun khoj--improve-sort () "Use cross-encoder to improve sorting of search results." (interactive) (khoj--incremental-search t))
(defun khoj--make-search-keymap (&optional existing-keymap)
"Setup keymap to configure Khoj search. Build of EXISTING-KEYMAP when passed."
(let ((enabled-content-types (khoj--get-enabled-content-types))
(kmap (or existing-keymap (make-sparse-keymap))))
(let ((kmap (or existing-keymap (make-sparse-keymap))))
(define-key kmap (kbd "C-c RET") #'khoj--improve-sort)
kmap))
@@ -194,6 +190,9 @@ Use `which-key` if available, else display simple message in echo area"
nil t t))
(message "%s" (khoj--keybindings-info-message))))
(defvar khoj--last-heading-pos nil
"The last heading position point was in.")
;; ----------------
;; Khoj Setup
@@ -249,12 +248,12 @@ for example), set this to the full interpreter path."
(make-obsolete-variable 'khoj-org-files 'khoj-index-files "1.2.0" 'set)
(defcustom khoj-index-files (org-agenda-files t t)
"List of org, markdown, pdf and other plaintext to index on khoj server."
"List of org, md, text, pdf to index on khoj server."
:type '(repeat string)
:group 'khoj)
(defcustom khoj-index-directories nil
"List of directories with org, markdown, pdf and other plaintext files to index on khoj server."
"List of directories with org, md, text, pdf to index on khoj server."
:type '(repeat string)
:group 'khoj)
@@ -285,9 +284,9 @@ Auto invokes setup steps on calling main entrypoint."
(if (/= (apply #'call-process khoj-server-python-command
nil t nil
"-m" "pip" "install" "--upgrade"
'("khoj-assistant"))
'("khoj"))
0)
(message "khoj.el: Failed to install Khoj server. Please install it manually using pip install `khoj-assistant'.\n%s" (buffer-string))
(message "khoj.el: Failed to install Khoj server. Please install it manually using pip install `khoj'.\n%s" (buffer-string))
(message "khoj.el: Installed and upgraded Khoj server version: %s" (khoj--server-get-version)))))
(defun khoj--server-start ()
@@ -407,8 +406,10 @@ Auto invokes setup steps on calling main entrypoint."
;; This is a temporary change. `khoj-org-directories', `khoj-org-files' are deprecated. They will be removed in a future release
(content-directories (or khoj-index-directories khoj-org-directories))
(content-files (or khoj-index-files khoj-org-files))
(files-to-index (or file-paths
(append (mapcan (lambda (dir) (directory-files-recursively dir "\\.\\(org\\|md\\|markdown\\|pdf\\|txt\\|rst\\|xml\\|htm\\|html\\)$")) content-directories) content-files)))
(files-to-index (mapcar
#'expand-file-name
(or file-paths
(append (mapcan (lambda (dir) (directory-files-recursively dir "\\.\\(org\\|md\\|markdown\\|pdf\\|txt\\|rst\\|xml\\|htm\\|html\\)$")) content-directories) content-files))))
(type-query (if (or (equal content-type "all") (not content-type)) "" (format "t=%s" content-type)))
(delete-files (-difference khoj--indexed-files files-to-index))
(inhibit-message t)
@@ -424,12 +425,12 @@ Auto invokes setup steps on calling main entrypoint."
"Send multi-part form `BODY' of `CONTENT-TYPE' in request to khoj server.
Append 'TYPE-QUERY' as query parameter in request url.
Specify `BOUNDARY' used to separate files in request header."
(let ((url-request-method "POST")
(let ((url-request-method (if force "PUT" "PATCH"))
(url-request-data body)
(url-request-extra-headers `(("content-type" . ,(format "multipart/form-data; boundary=%s" boundary))
("Authorization" . ,(format "Bearer %s" khoj-api-key)))))
(with-current-buffer
(url-retrieve (format "%s/api/v1/index/update?%s&force=%s&client=emacs" khoj-server-url type-query (or force "false"))
(url-retrieve (format "%s/api/content?%s&client=emacs" khoj-server-url type-query)
;; render response from indexing API endpoint on server
(lambda (status)
(if (not (plist-get status :error))
@@ -501,11 +502,19 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
;; -------------------------------------------
;; Render Response from Khoj server for Emacs
;; -------------------------------------------
(defun khoj--construct-find-similar-title (query)
"Construct title for find-similar QUERY."
(format "Similar to: %s"
(replace-regexp-in-string "^[#\\*]* " "" (car (split-string query "\n")))))
(defun khoj--extract-entries-as-markdown (json-response query)
"Convert JSON-RESPONSE, QUERY from API to markdown entries."
(defun khoj--extract-entries-as-markdown (json-response query is-find-similar)
"Convert JSON-RESPONSE, QUERY from API to markdown entries.
Use IS-FIND-SIMILAR bool to filter out first result.
As first result is the current entry at point."
(thread-last
json-response
;; filter our first result if is find similar as it'll be the current entry at point
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
;; Extract and render each markdown entry from response
(mapcar (lambda (json-response-item)
(thread-last
@@ -516,14 +525,18 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
;; Standardize results to 2nd level heading for consistent rendering
(replace-regexp-in-string "^\#+" "##"))))
;; Render entries into markdown formatted string with query set as as top level heading
(format "# %s\n%s" query)
(format "# %s\n%s" (if is-find-similar (khoj--construct-find-similar-title query) query))
;; remove leading (, ) or SPC from extracted entries string
(replace-regexp-in-string "^[\(\) ]" "")))
(defun khoj--extract-entries-as-org (json-response query)
"Convert JSON-RESPONSE, QUERY from API to `org-mode' entries."
(defun khoj--extract-entries-as-org (json-response query is-find-similar)
"Convert JSON-RESPONSE, QUERY from API to `org-mode' entries.
Use IS-FIND-SIMILAR bool to filter out first result.
As first result is the current entry at point."
(thread-last
json-response
;; filter our first result if is find similar as it'll be the current entry at point
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
;; Extract and render each org-mode entry from response
(mapcar (lambda (json-response-item)
(thread-last
@@ -534,14 +547,18 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
;; Standardize results to 2nd level heading for consistent rendering
(replace-regexp-in-string "^\*+" "**"))))
;; Render entries into org formatted string with query set as as top level heading
(format "* %s\n%s\n" query)
(format "* %s\n%s\n" (if is-find-similar (khoj--construct-find-similar-title query) query))
;; remove leading (, ) or SPC from extracted entries string
(replace-regexp-in-string "^[\(\) ]" "")))
(defun khoj--extract-entries-as-pdf (json-response query)
"Convert QUERY, JSON-RESPONSE from API with PDF results to `org-mode' entries."
(defun khoj--extract-entries-as-pdf (json-response query is-find-similar)
"Convert JSON-RESPONSE, QUERY from API to PDF entries.
Use IS-FIND-SIMILAR bool to filter out first result.
As first result is the current entry at point."
(thread-last
json-response
;; filter our first result if is find similar as it'll be the current entry at point
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
;; Extract and render each pdf entry from response
(mapcar (lambda (json-response-item)
(thread-last
@@ -550,7 +567,7 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
;; Format pdf entry as a org entry string
(format "** %s\n\n"))))
;; Render entries into org formatted string with query set as as top level heading
(format "* %s\n%s\n" query)
(format "* %s\n%s\n" (if is-find-similar (khoj--construct-find-similar-title query) query))
;; remove leading (, ) or SPC from extracted entries string
(replace-regexp-in-string "^[\(\) ]" "")))
@@ -582,9 +599,13 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
;; remove trailing (, ) or SPC from extracted entries string
(replace-regexp-in-string "[\(\) ]$" ""))))
(defun khoj--extract-entries (json-response query)
"Convert JSON-RESPONSE, QUERY from API to text entries."
(defun khoj--extract-entries (json-response query is-find-similar)
"Convert JSON-RESPONSE, QUERY from API to text entries.
Use IS-FIND-SIMILAR bool to filter out first result.
As first result is the current entry at point."
(thread-last json-response
;; filter our first result if is find similar as it'll be the current entry at point
((lambda (response) (if is-find-similar (seq-drop response 1) response)))
;; extract and render entries from API response
(mapcar (lambda (json-response-item)
(thread-last
@@ -598,7 +619,7 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
;; Format entries as org entry string
(format "** %s"))))
;; Set query as heading in rendered results buffer
(format "* %s\n%s\n" query)
(format "* %s\n%s\n" (if is-find-similar (khoj--construct-find-similar-title query) query))
;; remove leading (, ) or SPC from extracted entries string
(replace-regexp-in-string "^[\(\) ]" "")
;; remove trailing (, ) or SPC from extracted entries string
@@ -614,13 +635,30 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request.
((and (member 'markdown enabled-content-types) (or (equal file-extension "markdown") (equal file-extension "md"))) "markdown")
(t khoj-default-content-type))))
(defun khoj--org-cycle-content (&optional arg)
"Show all headlines in the buffer, like a table of contents.
With numerical argument ARG, show content up to level ARG.
Simplified fork of `org-cycle-content' from Emacs 29.1 to work with >=27.1."
(interactive "p")
(save-excursion
(goto-char (point-max))
(let ((regexp (if (and (wholenump arg) (> arg 0))
(format "^\\*\\{1,%d\\} " arg)
"^\\*+ "))
(last (point)))
(while (re-search-backward regexp nil t)
(org-fold-region (line-end-position) last t 'outline)
(setq last (line-end-position 0))))))
;; --------------
;; Query Khoj API
;; --------------
(defun khoj--call-api (path &optional method params callback &rest cbargs)
"Sync call API at PATH with METHOD and query PARAMS as kv assoc list.
Return json parsed response as alist."
Optionally apply CALLBACK with JSON parsed response and CBARGS."
(let* ((url-request-method (or method "GET"))
(url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key))))
(param-string (if params (url-build-query-string params) ""))
@@ -639,7 +677,7 @@ Return json parsed response as alist."
(defun khoj--call-api-async (path &optional method params callback &rest cbargs)
"Async call to API at PATH with METHOD and query PARAMS as kv assoc list.
Return json parsed response as alist."
Optionally apply CALLBACK with JSON parsed response and CBARGS."
(let* ((url-request-method (or method "GET"))
(url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key))))
(param-string (if params (url-build-query-string params) ""))
@@ -660,42 +698,44 @@ Return json parsed response as alist."
(defun khoj--get-enabled-content-types ()
"Get content types enabled for search from API."
(khoj--call-api "/api/config/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
(khoj--call-api "/api/content/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
(defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank title)
"Query Khoj Search API with QUERY, CONTENT-TYPE and (optional) RERANK as query params
Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional) TITLE."
(let ((title (or title query))
(rerank (or rerank "false"))
(params `((q ,query) (t ,content-type) (r ,rerank) (n ,khoj-results-count)))
(path "/api/search"))
(defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank is-find-similar)
"Query Khoj Search API with QUERY, CONTENT-TYPE and RERANK as query params.
Render search results in BUFFER-NAME using CONTENT-TYPE and QUERY.
Filter out first similar result if IS-FIND-SIMILAR set."
(let* ((rerank (or rerank "false"))
(params `((q ,query) (t ,content-type) (r ,rerank) (n ,khoj-results-count)))
(path "/api/search"))
(khoj--call-api-async path
"GET"
params
'khoj--render-search-results
content-type title buffer-name)))
content-type query buffer-name is-find-similar)))
(defun khoj--render-search-results (json-response content-type query buffer-name)
(defun khoj--render-search-results (json-response content-type query buffer-name &optional is-find-similar)
"Render search results in BUFFER-NAME using JSON-RESPONSE, CONTENT-TYPE, QUERY.
Filter out first similar result if IS-FIND-SIMILAR set."
;; render json response into formatted entries
(with-current-buffer buffer-name
(let ((inhibit-read-only t))
(let ((is-find-similar (or is-find-similar nil))
(inhibit-read-only t))
(erase-buffer)
(insert
(cond ((equal content-type "org") (khoj--extract-entries-as-org json-response query))
((equal content-type "markdown") (khoj--extract-entries-as-markdown json-response query))
((equal content-type "pdf") (khoj--extract-entries-as-pdf json-response query))
(cond ((equal content-type "org") (khoj--extract-entries-as-org json-response query is-find-similar))
((equal content-type "markdown") (khoj--extract-entries-as-markdown json-response query is-find-similar))
((equal content-type "pdf") (khoj--extract-entries-as-pdf json-response query is-find-similar))
((equal content-type "image") (khoj--extract-entries-as-images json-response query))
(t (khoj--extract-entries json-response query))))
(t (khoj--extract-entries json-response query is-find-similar))))
(cond ((or (equal content-type "all")
(equal content-type "pdf")
(equal content-type "org"))
(progn (visual-line-mode)
(org-mode)
(setq-local
org-startup-folded "showall"
org-hide-leading-stars t
org-startup-with-inline-images t)
(org-set-startup-visibility)))
(khoj--org-cycle-content 2)))
((equal content-type "markdown") (progn (markdown-mode)
(visual-line-mode)))
((equal content-type "image") (progn (shr-render-region (point-min) (point-max))
@@ -712,60 +752,61 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
;; Khoj Chat
;; ----------------
(defun khoj--chat ()
"Chat with Khoj."
(defun khoj--chat (&optional session-id)
"Chat with Khoj in session with SESSION-ID."
(interactive)
(when (not (get-buffer khoj--chat-buffer-name))
(khoj--load-chat-session khoj--chat-buffer-name))
(khoj--open-side-pane khoj--chat-buffer-name)
(when (or session-id (not (get-buffer khoj--chat-buffer-name)))
(khoj--load-chat-session khoj--chat-buffer-name session-id))
(let ((query (read-string "Query: ")))
(when (not (string-empty-p query))
(khoj--query-chat-api-and-render-messages query khoj--chat-buffer-name))))
(khoj--query-chat-api-and-render-messages query khoj--chat-buffer-name session-id))))
(defun khoj--open-side-pane (buffer-name)
"Open Khoj BUFFER-NAME in right side pane."
(if (get-buffer-window-list buffer-name)
;; if window is already open, switch to it
(progn
(select-window (get-buffer-window buffer-name))
(switch-to-buffer buffer-name))
;; else if window is not open, open it as a right-side window pane
(let ((bottomright-window (some-window (lambda (window) (and (window-at-side-p window 'right) (window-at-side-p window 'bottom))))))
(progn
;; Select the right-most window
(select-window bottomright-window)
;; if bottom-right window is not a vertical pane, split it vertically, else use the existing bottom-right vertical window
(let ((khoj-window (if (window-at-side-p bottomright-window 'left)
(split-window-right)
bottomright-window)))
;; Set the buffer in the khoj window
(set-window-buffer khoj-window buffer-name)
;; Switch to the khoj window
(select-window khoj-window)
;; Resize the window to 1/3 of the frame width
(window-resize khoj-window
(- (truncate (* 0.33 (frame-width))) (window-width))
t))))))
(save-selected-window
(if (get-buffer-window-list buffer-name)
;; if window is already open, switch to it
(progn
(select-window (get-buffer-window buffer-name))
(switch-to-buffer buffer-name))
;; else if window is not open, open it as a right-side window pane
(let ((bottomright-window (some-window (lambda (window) (and (window-at-side-p window 'right) (window-at-side-p window 'bottom))))))
(progn
;; Select the right-most window
(select-window bottomright-window)
;; if bottom-right window is not a vertical pane, split it vertically, else use the existing bottom-right vertical window
(let ((khoj-window (if (window-at-side-p bottomright-window 'left)
(split-window-right)
bottomright-window)))
;; Set the buffer in the khoj window
(set-window-buffer khoj-window buffer-name)
;; Switch to the khoj window
(select-window khoj-window)
;; Resize the window to 1/3 of the frame width
(window-resize khoj-window
(- (truncate (* 0.33 (frame-width))) (window-width))
t)))))
(goto-char (point-max))))
(defun khoj--load-chat-session (buffer-name &optional session-id)
"Load Khoj Chat conversation history into BUFFER-NAME."
"Load Khoj Chat conversation history from SESSION-ID into BUFFER-NAME."
(setq khoj--reference-count 0)
(let ((inhibit-read-only t)
(json-response (cdr (assoc 'chat (cdr (assoc 'response (khoj--get-chat-session session-id)))))))
(with-current-buffer (get-buffer-create buffer-name)
(erase-buffer)
(insert "* Khoj Chat\n")
(when json-response
(thread-last
json-response
;; generate chat messages from Khoj Chat API response
(mapcar #'khoj--format-chat-response)
;; insert chat messages into Khoj Chat Buffer
(mapc #'insert)))
(progn
(erase-buffer)
(insert "* Khoj Chat\n")
(when json-response
(thread-last
json-response
;; generate chat messages from Khoj Chat API response
(mapcar #'khoj--format-chat-response)
;; insert chat messages into Khoj Chat Buffer
(mapc #'insert)))
(org-mode)
(khoj--add-hover-text-to-footnote-refs (point-min))
;; commented add-hover-text func due to perf issues with the implementation
;;(khoj--add-hover-text-to-footnote-refs (point-min))
;; render reference footnotes as superscript
(setq-local
org-startup-folded "showall"
@@ -783,10 +824,11 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
;; enable minor modes for khoj chat
(visual-line-mode)
(read-only-mode t)))))
(read-only-mode t)))
(khoj--open-side-pane buffer-name)))
(defun khoj--close ()
"Kill Khoj buffer and window"
"Kill Khoj buffer and window."
(interactive)
(progn
(kill-buffer (current-buffer))
@@ -816,8 +858,8 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
;; show definition on hover on footnote reference
(overlay-put overlay 'help-echo it)))))))
(defun khoj--query-chat-api-and-render-messages (query buffer-name)
"Send QUERY to Khoj Chat. Render the chat messages from exchange in BUFFER-NAME."
(defun khoj--query-chat-api-and-render-messages (query buffer-name &optional session-id)
"Send QUERY to Chat SESSION-ID. Render the chat messages in BUFFER-NAME."
;; render json response into formatted chat messages
(with-current-buffer (get-buffer buffer-name)
(let ((inhibit-read-only t)
@@ -826,16 +868,19 @@ Render results in BUFFER-NAME using search results, CONTENT-TYPE and (optional)
(insert
(khoj--render-chat-message query "you" query-time))
(khoj--query-chat-api query
session-id
#'khoj--format-chat-response
#'khoj--render-chat-response buffer-name))))
(defun khoj--query-chat-api (query callback &rest cbargs)
"Send QUERY to Khoj Chat API and call CALLBACK with the response.
CBARGS are optional additional arguments to pass to CALLBACK."
(khoj--call-api-async "/api/chat"
"GET"
`(("q" ,query) ("n" ,khoj-results-count))
callback cbargs))
(defun khoj--query-chat-api (query session-id callback &rest cbargs)
"Send QUERY for SESSION-ID to Khoj Chat API.
Call CALLBACK func with response and CBARGS."
(let ((params `(("q" ,query) ("n" ,khoj-results-count))))
(when session-id (push `("conversation_id" ,session-id) params))
(khoj--call-api-async "/api/chat"
"GET"
params
callback cbargs)))
(defun khoj--get-chat-sessions ()
"Get all chat sessions from Khoj server."
@@ -863,8 +908,7 @@ CBARGS are optional additional arguments to pass to CALLBACK."
(defun khoj--open-conversation-session ()
"Menu to select Khoj conversation session to open."
(let ((selected-session-id (khoj--select-conversation-session "Open")))
(khoj--load-chat-session khoj--chat-buffer-name selected-session-id)
(khoj--open-side-pane khoj--chat-buffer-name)))
(khoj--load-chat-session khoj--chat-buffer-name selected-session-id)))
(defun khoj--create-chat-session ()
"Create new chat session."
@@ -872,21 +916,21 @@ CBARGS are optional additional arguments to pass to CALLBACK."
(defun khoj--new-conversation-session ()
"Create new Khoj conversation session."
(let* ((session (khoj--create-chat-session))
(new-session-id (cdr (assoc 'conversation_id session))))
(khoj--load-chat-session khoj--chat-buffer-name new-session-id)
(khoj--open-side-pane khoj--chat-buffer-name)))
(thread-last
(khoj--create-chat-session)
(assoc 'conversation_id)
(cdr)
(khoj--chat)))
(defun khoj--delete-chat-session (session-id)
"Delete new chat session."
"Delete chat session with SESSION-ID."
(khoj--call-api "/api/chat/history" "DELETE" `(("conversation_id" ,session-id))))
(defun khoj--delete-conversation-session ()
"Delete new Khoj conversation session."
(let* ((selected-session-id (khoj--select-conversation-session "Delete"))
(session (khoj--delete-chat-session selected-session-id)))
(khoj--load-chat-session khoj--chat-buffer-name)
(khoj--open-side-pane khoj--chat-buffer-name)))
(thread-last
(khoj--select-conversation-session "Delete")
(khoj--delete-chat-session)))
(defun khoj--render-chat-message (message sender &optional receive-date)
"Render chat messages as `org-mode' list item.
@@ -923,10 +967,11 @@ RECEIVE-DATE is the message receive date."
(format "\n[fn:%x] %s" khoj--reference-count)))))
(defun khoj--generate-online-reference (reference)
"Create `org-mode' footnotes for online REFERENCE."
(setq khoj--reference-count (1+ khoj--reference-count))
(let ((link (cdr (assoc 'link reference)))
(title (cdr (assoc 'title reference)))
(description (cdr (assoc 'description reference))))
(let* ((link (cdr (assoc 'link reference)))
(title (or (cdr (assoc 'title reference)) link))
(description (or (cdr (assoc 'description reference)) title)))
(cons
(propertize (format "^{ [fn:%x]}" khoj--reference-count) 'help-echo (format "%s\n%s" link description))
(thread-last
@@ -935,8 +980,8 @@ RECEIVE-DATE is the message receive date."
(replace-regexp-in-string "\n\n" "\n")
(format "\n[fn:%x] [[%s][%s]]\n%s\n" khoj--reference-count link title)))))
(defun khoj--extract-online-references (result-types searches)
"Extract link, title, and description of specified RESULT-TYPES from SEARCHES."
(defun khoj--extract-online-references (result-types query-result-pairs)
"Extract link, title and description from RESULT-TYPES in QUERY-RESULT-PAIRS."
(let ((result '()))
(-map
(lambda (search)
@@ -949,19 +994,22 @@ RECEIVE-DATE is the message receive date."
(lambda (search-result)
(-map
(lambda (entry)
(let ((link (cdr (or (assoc 'link entry) (assoc 'descriptionLink entry))))
(title (cdr (or (assoc 'title entry) '(title . ,link))))
(description (cdr (or (assoc 'snippet entry) (assoc 'description entry)))))
(let* ((link (cdr (or (assoc 'link entry) (assoc 'descriptionLink entry))))
(title (cdr (or (assoc 'title entry) `(title . ,link))))
(description (cdr (or (assoc 'snippet entry) (assoc 'description entry)))))
(setq result (append result `(((title . ,title) (link . ,link) (description . ,description) (search . ,search-q)))))))
;; wrap search results in a list if it is not already a list
(if (or (equal 'knowledgeGraph (car search-result)) (equal 'webpages (car search-result)))
(list (cdr search-result))
(if (arrayp (cdr search-result))
(list (elt (cdr search-result) 0))
(list (cdr search-result)))
(cdr search-result))))
search-results)))
searches)
query-result-pairs)
result))
(defun khoj--render-chat-response (response buffer-name)
"Insert chat message from RESPONSE into BUFFER-NAME."
(with-current-buffer (get-buffer buffer-name)
(let ((start-pos (point))
(inhibit-read-only t))
@@ -975,7 +1023,8 @@ RECEIVE-DATE is the message receive date."
(re-search-backward "^\*+ 🏮" nil t)))))
(defun khoj--format-chat-response (json-response &optional callback &rest cbargs)
"Render chat message using JSON-RESPONSE from Khoj Chat API."
"Format chat message using JSON-RESPONSE from Khoj Chat API.
Run CALLBACK with CBARGS on formatted message."
(let* ((message (cdr (or (assoc 'response json-response) (assoc 'message json-response))))
(sender (cdr (assoc 'by json-response)))
(receive-date (cdr (assoc 'created json-response)))
@@ -1087,6 +1136,16 @@ RECEIVE-DATE is the message receive date."
;; Similar Search
;; --------------
(defun khoj--get-current-outline-entry-pos ()
"Get heading position of current outline section."
;; get heading position of current outline entry
(cond
;; when at heading of entry
((looking-at outline-regexp)
(point))
;; when within entry
(t (save-excursion (outline-previous-heading) (point)))))
(defun khoj--get-current-outline-entry-text ()
"Get text under current outline section."
(string-trim
@@ -1130,10 +1189,6 @@ Paragraph only starts at first text after blank line."
;; get paragraph, if in text mode
(t
(khoj--get-current-paragraph-text))))
;; extract heading to show in result buffer from query
(query-title
(format "Similar to: %s"
(replace-regexp-in-string "^[#\\*]* " "" (car (split-string query "\n")))))
(buffer-name (get-buffer-create khoj--search-buffer-name)))
(progn
(khoj--query-search-api-and-render-results
@@ -1141,9 +1196,35 @@ Paragraph only starts at first text after blank line."
content-type
buffer-name
rerank
query-title)
(khoj--open-side-pane buffer-name)
(goto-char (point-min)))))
t)
(khoj--open-side-pane buffer-name))))
(defun khoj--auto-find-similar ()
"Call find similar on current element, if point has moved to a new element."
;; Call find similar
(when (and (derived-mode-p 'org-mode)
(org-element-at-point)
(not (string= (buffer-name (current-buffer)) khoj--search-buffer-name))
(get-buffer-window khoj--search-buffer-name))
(let ((current-heading-pos (khoj--get-current-outline-entry-pos)))
(unless (eq current-heading-pos khoj--last-heading-pos)
(setq khoj--last-heading-pos current-heading-pos)
(khoj--find-similar)))))
(defun khoj--setup-auto-find-similar ()
"Setup automatic call to find similar to current element."
(if khoj-auto-find-similar
(add-hook 'post-command-hook #'khoj--auto-find-similar)
(remove-hook 'post-command-hook #'khoj--auto-find-similar)))
(defun khoj-toggle-auto-find-similar ()
"Toggle automatic call to find similar to current element."
(interactive)
(setq khoj-auto-find-similar (not khoj-auto-find-similar))
(khoj--setup-auto-find-similar)
(if khoj-auto-find-similar
(message "Auto find similar enabled")
(message "Auto find similar disabled")))
;; ---------
@@ -1185,7 +1266,7 @@ Paragraph only starts at first text after blank line."
(transient-define-suffix khoj--update-command (&optional args)
"Call khoj API to update index of specified content type."
(interactive (list (transient-args transient-current-command)))
(let* ((force-update (if (member "--force-update" args) "true" "false"))
(let* ((force-update (if (member "--force-update" args) t nil))
;; set content type to: specified > last used > based on current buffer > default type
(content-type (or (transient-arg-value "--content-type=" args) (khoj--buffer-name-to-content-type (buffer-name))))
(url-request-method "GET"))

View File

@@ -64,7 +64,7 @@
")))
(should
(equal
(khoj--extract-entries-as-markdown json-response-from-khoj-backend user-query)
(khoj--extract-entries-as-markdown json-response-from-khoj-backend user-query nil)
"\
# Become God\n\
## Upgrade\n\
@@ -100,7 +100,7 @@ Rule everything\n\n"))))
")))
(should
(equal
(khoj--extract-entries-as-org json-response-from-khoj-backend user-query)
(khoj--extract-entries-as-org json-response-from-khoj-backend user-query nil)
"\
* Become God\n\
** Upgrade\n\

View File

@@ -1,9 +1,9 @@
{
"id": "khoj",
"name": "Khoj",
"version": "1.14.0",
"version": "1.21.3",
"minAppVersion": "0.15.0",
"description": "An AI copilot for your Second Brain",
"description": "Your Second Brain",
"author": "Khoj Inc.",
"authorUrl": "https://github.com/khoj-ai",
"isDesktopOnly": false

View File

@@ -1,7 +1,7 @@
{
"name": "Khoj",
"version": "1.14.0",
"description": "An AI copilot for your Second Brain",
"version": "1.21.3",
"description": "Your Second Brain",
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
"license": "GPL-3.0-or-later",
"main": "src/main.js",
@@ -14,13 +14,14 @@
"search",
"chat",
"AI",
"assistant"
"assistant",
"second brain"
],
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
"builtin-modules": "3.3.0",
"esbuild": "0.14.47",
"obsidian": "latest",

View File

@@ -1,8 +1,9 @@
import { MarkdownRenderer, WorkspaceLeaf, request, requestUrl, setIcon } from 'obsidian';
import {ItemView, MarkdownRenderer, Scope, WorkspaceLeaf, request, requestUrl, setIcon, Platform} from 'obsidian';
import * as DOMPurify from 'dompurify';
import { KhojSetting } from 'src/settings';
import { KhojPaneView } from 'src/pane_view';
import { KhojView, createCopyParentText, getLinkToEntry, pasteTextAtCursor } from 'src/utils';
import { KhojSearchModal } from './search_modal';
export interface ChatJsonResult {
image?: string;
@@ -11,6 +12,25 @@ export interface ChatJsonResult {
inferredQueries?: string[];
}
interface ChunkResult {
objects: string[];
remainder: string;
}
interface MessageChunk {
type: string;
data: any;
}
interface ChatMessageState {
newResponseTextEl: HTMLElement | null;
newResponseEl: HTMLElement | null;
loadingEllipsis: HTMLElement | null;
references: any;
rawResponse: string;
rawQuery: string;
isVoice: boolean;
}
interface Location {
region: string;
@@ -24,10 +44,23 @@ export class KhojChatView extends KhojPaneView {
setting: KhojSetting;
waitingForLocation: boolean;
location: Location;
keyPressTimeout: NodeJS.Timeout | null = null;
userMessages: string[] = []; // Store user sent messages for input history cycling
currentMessageIndex: number = -1; // Track current message index in userMessages array
private currentUserInput: string = ""; // Stores the current user input that is being typed in chat
private startingMessage: string = "Message";
chatMessageState: ChatMessageState;
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
super(leaf, setting);
// Register chat view keybindings
this.scope = new Scope(this.app.scope);
this.scope.register(["Ctrl"], 'n', (_) => this.createNewConversation());
this.scope.register(["Ctrl"], 'o', async (_) => await this.toggleChatSessions());
this.scope.register(["Ctrl"], 'f', (_) => new KhojSearchModal(this.app, this.setting).open());
this.scope.register(["Ctrl"], 'r', (_) => new KhojSearchModal(this.app, this.setting, true).open());
this.waitingForLocation = true;
fetch("https://ipapi.co/json")
@@ -61,18 +94,25 @@ export class KhojChatView extends KhojPaneView {
return "message-circle";
}
async chat() {
async chat(isVoice: boolean = false) {
// Get text in chat input element
let input_el = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
// Clear text after extracting message to send
let user_message = input_el.value.trim();
// Store the message in the array if it's not empty
if (user_message) {
this.userMessages.push(user_message);
// Update starting message after sending a new message
const modifierKey = Platform.isMacOS ? '⌘' : '^';
this.startingMessage = `(${modifierKey}+↑/↓) for prev messages`;
input_el.placeholder = this.startingMessage;
}
input_el.value = "";
this.autoResize();
// Get and render chat response to user message
await this.getChatResponse(user_message);
await this.getChatResponse(user_message, isVoice);
}
async onOpen() {
@@ -92,8 +132,9 @@ export class KhojChatView extends KhojPaneView {
const objectSrc = `object-src 'none';`;
const csp = `${defaultSrc} ${scriptSrc} ${connectSrc} ${styleSrc} ${imgSrc} ${childSrc} ${objectSrc}`;
// Add CSP meta tag to the Khoj Chat modal
document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
// WARNING: CSP DISABLED for now as it breaks other Obsidian plugins. Enable when can scope CSP to only Khoj plugin.
// CSP meta tag for the Khoj Chat modal
// document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
// Create area for chat logs
let chatBodyEl = contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
@@ -104,9 +145,10 @@ export class KhojChatView extends KhojPaneView {
text: "Chat Sessions",
attr: {
class: "khoj-input-row-button clickable-icon",
title: "Show Conversations (^O)",
},
})
chatSessions.addEventListener('click', async (_) => { await this.toggleChatSessions(chatBodyEl) });
chatSessions.addEventListener('click', async (_) => { await this.toggleChatSessions() });
setIcon(chatSessions, "history");
let chatInput = inputRow.createEl("textarea", {
@@ -117,16 +159,25 @@ export class KhojChatView extends KhojPaneView {
},
})
chatInput.addEventListener('input', (_) => { this.onChatInput() });
chatInput.addEventListener('keydown', (event) => { this.incrementalChat(event) });
chatInput.addEventListener('keydown', (event) => {
this.incrementalChat(event);
this.handleArrowKeys(event);
});
// Add event listeners for long press keybinding
this.contentEl.addEventListener('keydown', this.handleKeyDown.bind(this));
this.contentEl.addEventListener('keyup', this.handleKeyUp.bind(this));
let transcribe = inputRow.createEl("button", {
text: "Transcribe",
attr: {
id: "khoj-transcribe",
class: "khoj-transcribe khoj-input-row-button clickable-icon ",
title: "Start Voice Chat (^S)",
},
})
transcribe.addEventListener('mousedown', async (event) => { await this.speechToText(event) });
transcribe.addEventListener('mousedown', (event) => { this.startSpeechToText(event) });
transcribe.addEventListener('mouseup', async (event) => { await this.stopSpeechToText(event) });
transcribe.addEventListener('touchstart', async (event) => { await this.speechToText(event) });
transcribe.addEventListener('touchend', async (event) => { await this.speechToText(event) });
transcribe.addEventListener('touchcancel', async (event) => { await this.speechToText(event) });
@@ -145,7 +196,8 @@ export class KhojChatView extends KhojPaneView {
// Get chat history from Khoj backend and set chat input state
let getChatHistorySucessfully = await this.getChatHistory(chatBodyEl);
let placeholderText = getChatHistorySucessfully ? "Message" : "Configure Khoj to enable chat";
let placeholderText : string = getChatHistorySucessfully ? this.startingMessage : "Configure Khoj to enable chat";
chatInput.placeholder = placeholderText;
chatInput.disabled = !getChatHistorySucessfully;
@@ -160,6 +212,46 @@ export class KhojChatView extends KhojPaneView {
});
}
startSpeechToText(event: KeyboardEvent | MouseEvent | TouchEvent, timeout=200) {
if (!this.keyPressTimeout) {
this.keyPressTimeout = setTimeout(async () => {
// Reset auto send voice message timer, UI if running
if (this.sendMessageTimeout) {
// Stop the auto send voice message countdown timer UI
clearTimeout(this.sendMessageTimeout);
const sendButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-chat-send")[0]
setIcon(sendButton, "arrow-up-circle")
let sendImg = <SVGElement>sendButton.getElementsByClassName("lucide-arrow-up-circle")[0]
sendImg.addEventListener('click', async (_) => { await this.chat() });
// Reset chat input value
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
chatInput.value = "";
}
// Start new voice message
await this.speechToText(event);
}, timeout);
}
}
async stopSpeechToText(event: KeyboardEvent | MouseEvent | TouchEvent) {
if (this.mediaRecorder) {
await this.speechToText(event);
}
if (this.keyPressTimeout) {
clearTimeout(this.keyPressTimeout);
this.keyPressTimeout = null;
}
}
handleKeyDown(event: KeyboardEvent) {
// Start speech to text if keyboard shortcut is pressed
if (event.key === 's' && event.getModifierState('Control')) this.startSpeechToText(event);
}
async handleKeyUp(event: KeyboardEvent) {
// Stop speech to text if keyboard shortcut is released
if (event.key === 's' && event.getModifierState('Control')) await this.stopSpeechToText(event);
}
processOnlineReferences(referenceSection: HTMLElement, onlineContext: any) {
let numOnlineReferences = 0;
for (let subquery in onlineContext) {
@@ -294,6 +386,57 @@ export class KhojChatView extends KhojPaneView {
return referenceButton;
}
textToSpeech(message: string, event: MouseEvent | null = null): void {
// Replace the speaker with a loading icon.
let loader = document.createElement("span");
loader.classList.add("loader");
let speechButton: HTMLButtonElement;
let speechIcon: Element;
if (event === null) {
// Pick the last speech button if none is provided
let speechButtons = document.getElementsByClassName("speech-button");
speechButton = speechButtons[speechButtons.length - 1] as HTMLButtonElement;
let speechIcons = document.getElementsByClassName("speech-icon");
speechIcon = speechIcons[speechIcons.length - 1];
} else {
speechButton = event.currentTarget as HTMLButtonElement;
speechIcon = event.target as Element;
}
speechButton.appendChild(loader);
speechButton.disabled = true;
const context = new AudioContext();
let textToSpeechApi = `${this.setting.khojUrl}/api/chat/speech?text=${encodeURIComponent(message)}`;
fetch(textToSpeechApi, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"Authorization": `Bearer ${this.setting.khojApiKey}`,
},
})
.then(response => response.arrayBuffer())
.then(arrayBuffer => context.decodeAudioData(arrayBuffer))
.then(audioBuffer => {
const source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(context.destination);
source.start(0);
source.onended = function() {
speechButton.removeChild(loader);
speechButton.disabled = false;
};
})
.catch(err => {
console.error("Error playing speech:", err);
speechButton.removeChild(loader);
speechButton.disabled = false; // Consider enabling the button again to allow retrying
});
}
formatHTMLMessage(message: string, raw = false, willReplace = true) {
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for some AI chat model.
message = message.replace(/<s>\[INST\].+(<\/s>)?/g, '');
@@ -302,25 +445,23 @@ export class KhojChatView extends KhojPaneView {
message = DOMPurify.sanitize(message);
// Convert the message to html, sanitize the message html and render it to the real DOM
let chat_message_body_text_el = this.contentEl.createDiv();
chat_message_body_text_el.className = "chat-message-text-response";
chat_message_body_text_el.innerHTML = this.markdownTextToSanitizedHtml(message);
let chatMessageBodyTextEl = this.contentEl.createDiv();
chatMessageBodyTextEl.innerHTML = this.markdownTextToSanitizedHtml(message, this);
// Add a copy button to each chat message, if it doesn't already exist
if (willReplace === true) {
this.renderActionButtons(message, chat_message_body_text_el);
this.renderActionButtons(message, chatMessageBodyTextEl);
}
return chat_message_body_text_el;
return chatMessageBodyTextEl;
}
markdownTextToSanitizedHtml(markdownText: string): string {
markdownTextToSanitizedHtml(markdownText: string, component: ItemView): string {
// Render markdown to an unlinked DOM element
let virtualChatMessageBodyTextEl = document.createElement("div");
// Convert the message to html
// @ts-ignore
MarkdownRenderer.renderMarkdown(markdownText, virtualChatMessageBodyTextEl, '', null);
MarkdownRenderer.renderMarkdown(markdownText, virtualChatMessageBodyTextEl, '', component);
// Remove image HTML elements with any non whitelisted src prefix
virtualChatMessageBodyTextEl.innerHTML = virtualChatMessageBodyTextEl.innerHTML.replace(
@@ -396,23 +537,23 @@ export class KhojChatView extends KhojPaneView {
class: `khoj-chat-message ${sender}`
},
})
let chat_message_body_el = chatMessageEl.createDiv();
chat_message_body_el.addClasses(["khoj-chat-message-text", sender]);
let chat_message_body_text_el = chat_message_body_el.createDiv();
let chatMessageBodyEl = chatMessageEl.createDiv();
chatMessageBodyEl.addClasses(["khoj-chat-message-text", sender]);
let chatMessageBodyTextEl = chatMessageBodyEl.createDiv();
// Sanitize the markdown to render
message = DOMPurify.sanitize(message);
if (raw) {
chat_message_body_text_el.innerHTML = message;
chatMessageBodyTextEl.innerHTML = message;
} else {
// @ts-ignore
chat_message_body_text_el.innerHTML = this.markdownTextToSanitizedHtml(message);
chatMessageBodyTextEl.innerHTML = this.markdownTextToSanitizedHtml(message, this);
}
// Add action buttons to each chat message element
if (willReplace === true) {
this.renderActionButtons(message, chat_message_body_text_el);
this.renderActionButtons(message, chatMessageBodyTextEl);
}
// Remove user-select: none property to make text selectable
@@ -425,56 +566,69 @@ export class KhojChatView extends KhojPaneView {
}
createKhojResponseDiv(dt?: Date): HTMLDivElement {
let message_time = this.formatDate(dt ?? new Date());
let messageTime = this.formatDate(dt ?? new Date());
// Append message to conversation history HTML element.
// The chat logs should display above the message input box to follow standard UI semantics
let chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
let chat_message_el = chat_body_el.createDiv({
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
let chatMessageEl = chatBodyEl.createDiv({
attr: {
"data-meta": `🏮 Khoj at ${message_time}`,
"data-meta": `🏮 Khoj at ${messageTime}`,
class: `khoj-chat-message khoj`
},
}).createDiv({
attr: {
class: `khoj-chat-message-text khoj`
},
}).createDiv();
})
// Scroll to bottom after inserting chat messages
this.scrollChatToBottom();
return chat_message_el;
return chatMessageEl;
}
async renderIncrementalMessage(htmlElement: HTMLDivElement, additionalMessage: string) {
this.result += additionalMessage;
this.chatMessageState.rawResponse += additionalMessage;
htmlElement.innerHTML = "";
// Sanitize the markdown to render
this.result = DOMPurify.sanitize(this.result);
this.chatMessageState.rawResponse = DOMPurify.sanitize(this.chatMessageState.rawResponse);
// @ts-ignore
htmlElement.innerHTML = this.markdownTextToSanitizedHtml(this.result);
htmlElement.innerHTML = this.markdownTextToSanitizedHtml(this.chatMessageState.rawResponse, this);
// Render action buttons for the message
this.renderActionButtons(this.result, htmlElement);
this.renderActionButtons(this.chatMessageState.rawResponse, htmlElement);
// Scroll to bottom of modal, till the send message input box
this.scrollChatToBottom();
}
renderActionButtons(message: string, chat_message_body_text_el: HTMLElement) {
renderActionButtons(message: string, chatMessageBodyTextEl: HTMLElement) {
let copyButton = this.contentEl.createEl('button');
copyButton.classList.add("copy-button");
copyButton.classList.add("chat-action-button");
copyButton.title = "Copy Message to Clipboard";
setIcon(copyButton, "copy-plus");
copyButton.addEventListener('click', createCopyParentText(message));
chat_message_body_text_el.append(copyButton);
// Add button to paste into current buffer
let pasteToFile = this.contentEl.createEl('button');
pasteToFile.classList.add("copy-button");
pasteToFile.classList.add("chat-action-button");
pasteToFile.title = "Paste Message to File";
setIcon(pasteToFile, "clipboard-paste");
pasteToFile.addEventListener('click', (event) => { pasteTextAtCursor(createCopyParentText(message, 'clipboard-paste')(event)); });
chat_message_body_text_el.append(pasteToFile);
// Only enable the speech feature if the user is subscribed
let speechButton = null;
if (this.setting.userInfo?.is_active) {
// Create a speech button icon to play the message out loud
speechButton = this.contentEl.createEl('button');
speechButton.classList.add("chat-action-button", "speech-button");
speechButton.title = "Listen to Message";
setIcon(speechButton, "speech")
speechButton.addEventListener('click', (event) => this.textToSpeech(message, event));
}
// Append buttons to parent element
chatMessageBodyTextEl.append(copyButton, pasteToFile);
if (speechButton) {
chatMessageBodyTextEl.append(speechButton);
}
}
formatDate(date: Date): string {
@@ -484,14 +638,25 @@ export class KhojChatView extends KhojPaneView {
return `${time_string}, ${date_string}`;
}
createNewConversation(chatBodyEl: HTMLElement) {
createNewConversation() {
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
chatBodyEl.innerHTML = "";
chatBodyEl.dataset.conversationId = "";
chatBodyEl.dataset.conversationTitle = "";
this.userMessages = [];
this.startingMessage = "Message";
// Update the placeholder of the chat input
const chatInput = this.contentEl.querySelector('.khoj-chat-input') as HTMLTextAreaElement;
if (chatInput) {
chatInput.placeholder = this.startingMessage;
}
this.renderMessage(chatBodyEl, "Hey 👋🏾, what's up?", "khoj");
}
async toggleChatSessions(chatBodyEl: HTMLElement, forceShow: boolean = false): Promise<boolean> {
async toggleChatSessions(forceShow: boolean = false): Promise<boolean> {
this.userMessages = []; // clear user previous message history
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
if (!forceShow && this.contentEl.getElementsByClassName("side-panel")?.length > 0) {
chatBodyEl.innerHTML = "";
return this.getChatHistory(chatBodyEl);
@@ -505,9 +670,10 @@ export class KhojChatView extends KhojPaneView {
const newConversationButtonEl = newConversationEl.createEl("button");
newConversationButtonEl.classList.add("new-conversation-button");
newConversationButtonEl.classList.add("side-panel-button");
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation(chatBodyEl));
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation());
setIcon(newConversationButtonEl, "plus");
newConversationButtonEl.innerHTML += "New";
newConversationButtonEl.title = "New Conversation (^N)";
const existingConversationsEl = sidePanelEl.createDiv("existing-conversations");
const conversationListEl = existingConversationsEl.createDiv("conversation-list");
@@ -579,8 +745,7 @@ export class KhojChatView extends KhojPaneView {
let editConversationTitleButtonEl = this.contentEl.createEl('button');
setIcon(editConversationTitleButtonEl, "edit");
editConversationTitleButtonEl.title = "Rename";
editConversationTitleButtonEl.classList.add("edit-title-button");
editConversationTitleButtonEl.classList.add("three-dot-menu-button-item");
editConversationTitleButtonEl.classList.add("edit-title-button", "three-dot-menu-button-item", "clickable-icon");
if (selectedConversation) editConversationTitleButtonEl.classList.add("selected-conversation");
editConversationTitleButtonEl.addEventListener('click', (event) => {
event.stopPropagation();
@@ -608,7 +773,7 @@ export class KhojChatView extends KhojPaneView {
let editConversationTitleSaveButtonEl = this.contentEl.createEl('button');
conversationSessionTitleEl.replaceWith(editConversationTitleInputEl);
editConversationTitleSaveButtonEl.innerHTML = "Save";
editConversationTitleSaveButtonEl.classList.add("three-dot-menu-button-item");
editConversationTitleSaveButtonEl.classList.add("three-dot-menu-button-item", "clickable-icon");
if (selectedConversation) editConversationTitleSaveButtonEl.classList.add("selected-conversation");
editConversationTitleSaveButtonEl.addEventListener('click', (event) => {
event.stopPropagation();
@@ -655,8 +820,7 @@ export class KhojChatView extends KhojPaneView {
let deleteConversationButtonEl = this.contentEl.createEl('button');
setIcon(deleteConversationButtonEl, "trash");
deleteConversationButtonEl.title = "Delete";
deleteConversationButtonEl.classList.add("delete-conversation-button");
deleteConversationButtonEl.classList.add("three-dot-menu-button-item");
deleteConversationButtonEl.classList.add("delete-conversation-button", "three-dot-menu-button-item", "clickable-icon");
if (selectedConversation) deleteConversationButtonEl.classList.add("selected-conversation");
deleteConversationButtonEl.addEventListener('click', () => {
// Ask for confirmation before deleting chat session
@@ -669,7 +833,7 @@ export class KhojChatView extends KhojPaneView {
chatBodyEl.innerHTML = "";
chatBodyEl.dataset.conversationId = "";
chatBodyEl.dataset.conversationTitle = "";
this.toggleChatSessions(chatBodyEl, true);
this.toggleChatSessions(true);
})
.catch(err => {
return;
@@ -707,7 +871,6 @@ export class KhojChatView extends KhojPaneView {
chatBodyEl.dataset.conversationId = responseJson.response.conversation_id;
chatBodyEl.dataset.conversationTitle = responseJson.response.slug || `New conversation 🌱`;
let chatLogs = responseJson.response?.conversation_id ? responseJson.response.chat ?? [] : responseJson.response;
chatLogs.forEach((chatLog: any) => {
this.renderMessageWithReferences(
@@ -720,7 +883,23 @@ export class KhojChatView extends KhojPaneView {
chatLog.intent?.type,
chatLog.intent?.["inferred-queries"],
);
// push the user messages to the chat history
if(chatLog.by === "you"){
this.userMessages.push(chatLog.message);
}
});
// Update starting message after loading history
const modifierKey: string = Platform.isMacOS ? '⌘' : '^';
this.startingMessage = this.userMessages.length > 0
? `(${modifierKey}+↑/↓) for prev messages`
: "Message";
// Update the placeholder of the chat input
const chatInput = this.contentEl.querySelector('.khoj-chat-input') as HTMLTextAreaElement;
if (chatInput) {
chatInput.placeholder = this.startingMessage;
}
}
} catch (err) {
let errorMsg = "Unable to get response from Khoj server ❤️‍🩹. Ensure server is running or contact developers for help at [team@khoj.dev](mailto:team@khoj.dev) or in [Discord](https://discord.gg/BDgyabRM6e)";
@@ -730,36 +909,127 @@ export class KhojChatView extends KhojPaneView {
return true;
}
async readChatStream(response: Response, responseElement: HTMLDivElement): Promise<void> {
convertMessageChunkToJson(rawChunk: string): MessageChunk {
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
try {
let jsonChunk = JSON.parse(rawChunk);
if (!jsonChunk.type)
jsonChunk = {type: 'message', data: jsonChunk};
return jsonChunk;
} catch (e) {
return {type: 'message', data: rawChunk};
}
} else if (rawChunk.length > 0) {
return {type: 'message', data: rawChunk};
}
return {type: '', data: ''};
}
processMessageChunk(rawChunk: string): void {
const chunk = this.convertMessageChunkToJson(rawChunk);
console.debug("Chunk:", chunk);
if (!chunk || !chunk.type) return;
if (chunk.type === 'status') {
console.log(`status: ${chunk.data}`);
const statusMessage = chunk.data;
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, statusMessage, this.chatMessageState.loadingEllipsis, false);
} else if (chunk.type === 'start_llm_response') {
console.log("Started streaming", new Date());
} else if (chunk.type === 'end_llm_response') {
console.log("Stopped streaming", new Date());
// Automatically respond with voice if the subscribed user has sent voice message
if (this.chatMessageState.isVoice && this.setting.userInfo?.is_active)
this.textToSpeech(this.chatMessageState.rawResponse);
// Append any references after all the data has been streamed
this.finalizeChatBodyResponse(this.chatMessageState.references, this.chatMessageState.newResponseTextEl);
const liveQuery = this.chatMessageState.rawQuery;
// Reset variables
this.chatMessageState = {
newResponseTextEl: null,
newResponseEl: null,
loadingEllipsis: null,
references: {},
rawResponse: "",
rawQuery: liveQuery,
isVoice: false,
};
} else if (chunk.type === "references") {
this.chatMessageState.references = {"notes": chunk.data.context, "online": chunk.data.onlineContext};
} else if (chunk.type === 'message') {
const chunkData = chunk.data;
if (typeof chunkData === 'object' && chunkData !== null) {
// If chunkData is already a JSON object
this.handleJsonResponse(chunkData);
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
// Try process chunk data as if it is a JSON object
try {
const jsonData = JSON.parse(chunkData.trim());
this.handleJsonResponse(jsonData);
} catch (e) {
this.chatMessageState.rawResponse += chunkData;
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, this.chatMessageState.rawResponse, this.chatMessageState.loadingEllipsis);
}
} else {
this.chatMessageState.rawResponse += chunkData;
this.handleStreamResponse(this.chatMessageState.newResponseTextEl, this.chatMessageState.rawResponse, this.chatMessageState.loadingEllipsis);
}
}
}
handleJsonResponse(jsonData: any): void {
if (jsonData.image || jsonData.detail) {
this.chatMessageState.rawResponse = this.handleImageResponse(jsonData, this.chatMessageState.rawResponse);
} else if (jsonData.response) {
this.chatMessageState.rawResponse = jsonData.response;
}
if (this.chatMessageState.newResponseTextEl) {
this.chatMessageState.newResponseTextEl.innerHTML = "";
this.chatMessageState.newResponseTextEl.appendChild(this.formatHTMLMessage(this.chatMessageState.rawResponse));
}
}
async readChatStream(response: Response): Promise<void> {
// Exit if response body is empty
if (response.body == null) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
const eventDelimiter = '␃🔚␗';
let buffer = '';
while (true) {
const { value, done } = await reader.read();
// Break if the stream is done
if (done) break;
if (done) {
this.processMessageChunk(buffer);
buffer = '';
// Break if the stream is done
break;
}
let responseText = decoder.decode(value);
if (responseText.includes("### compiled references:")) {
// Render any references used to generate the response
const [additionalResponse, rawReference] = responseText.split("### compiled references:", 2);
await this.renderIncrementalMessage(responseElement, additionalResponse);
const chunk = decoder.decode(value, { stream: true });
console.debug("Raw Chunk:", chunk)
// Start buffering chunks until complete event is received
buffer += chunk;
const rawReferenceAsJson = JSON.parse(rawReference);
let references = this.extractReferences(rawReferenceAsJson);
responseElement.appendChild(this.createReferenceSection(references));
} else {
// Render incremental chat response
await this.renderIncrementalMessage(responseElement, responseText);
// Once the buffer contains a complete event
let newEventIndex;
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
// Extract the event from the buffer
const event = buffer.slice(0, newEventIndex);
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
// Process the event
if (event) this.processMessageChunk(event);
}
}
}
async getChatResponse(query: string | undefined | null): Promise<void> {
async getChatResponse(query: string | undefined | null, isVoice: boolean = false): Promise<void> {
// Exit if query is empty
if (!query || query === "") return;
@@ -767,83 +1037,59 @@ export class KhojChatView extends KhojPaneView {
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
this.renderMessage(chatBodyEl, query, "you");
let conversationID = chatBodyEl.dataset.conversationId;
if (!conversationID) {
let conversationId = chatBodyEl.dataset.conversationId;
if (!conversationId) {
let chatUrl = `${this.setting.khojUrl}/api/chat/sessions?client=obsidian`;
let response = await fetch(chatUrl, {
method: "POST",
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` },
});
let data = await response.json();
conversationID = data.conversation_id;
chatBodyEl.dataset.conversationId = conversationID;
conversationId = data.conversation_id;
chatBodyEl.dataset.conversationId = conversationId;
}
// Get chat response from Khoj backend
let encodedQuery = encodeURIComponent(query);
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&n=${this.setting.resultsCount}&client=obsidian&stream=true&region=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`;
let responseElement = this.createKhojResponseDiv();
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&conversation_id=${conversationId}&n=${this.setting.resultsCount}&stream=true&client=obsidian`;
if (!!this.location) chatUrl += `&region=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`;
let newResponseEl = this.createKhojResponseDiv();
let newResponseTextEl = newResponseEl.createDiv();
newResponseTextEl.classList.add("khoj-chat-message-text", "khoj");
// Temporary status message to indicate that Khoj is thinking
this.result = "";
let loadingEllipsis = this.createLoadingEllipse();
responseElement.appendChild(loadingEllipsis);
newResponseTextEl.appendChild(loadingEllipsis);
// Set chat message state
this.chatMessageState = {
newResponseEl: newResponseEl,
newResponseTextEl: newResponseTextEl,
loadingEllipsis: loadingEllipsis,
references: {},
rawQuery: query,
rawResponse: "",
isVoice: isVoice,
};
let response = await fetch(chatUrl, {
method: "GET",
headers: {
"Content-Type": "text/event-stream",
"Content-Type": "text/plain",
"Authorization": `Bearer ${this.setting.khojApiKey}`,
},
})
try {
if (response.body === null) {
throw new Error("Response body is null");
}
if (response.body === null) throw new Error("Response body is null");
// Clear loading status message
if (responseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis) {
responseElement.removeChild(loadingEllipsis);
}
// Reset collated chat result to empty string
this.result = "";
responseElement.innerHTML = "";
if (response.headers.get("content-type") === "application/json") {
let responseText = ""
try {
const responseAsJson = await response.json() as ChatJsonResult;
if (responseAsJson.image) {
// If response has image field, response is a generated image.
if (responseAsJson.intentType === "text-to-image") {
responseText += `![${query}](data:image/png;base64,${responseAsJson.image})`;
} else if (responseAsJson.intentType === "text-to-image2") {
responseText += `![${query}](${responseAsJson.image})`;
} else if (responseAsJson.intentType === "text-to-image-v3") {
responseText += `![${query}](data:image/webp;base64,${responseAsJson.image})`;
}
const inferredQuery = responseAsJson.inferredQueries?.[0];
if (inferredQuery) {
responseText += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
} else if (responseAsJson.detail) {
responseText = responseAsJson.detail;
}
} catch (error) {
// If the chunk is not a JSON object, just display it as is
responseText = await response.text();
} finally {
await this.renderIncrementalMessage(responseElement, responseText);
}
} else {
// Stream and render chat response
await this.readChatStream(response, responseElement);
}
// Stream and render chat response
await this.readChatStream(response);
} catch (err) {
console.log(`Khoj chat response failed with\n${err}`);
console.error(`Khoj chat response failed with\n${err}`);
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️‍🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
responseElement.innerHTML = errorMsg
newResponseTextEl.textContent = errorMsg;
}
}
@@ -886,7 +1132,7 @@ export class KhojChatView extends KhojPaneView {
sendMessageTimeout: NodeJS.Timeout | undefined;
mediaRecorder: MediaRecorder | undefined;
async speechToText(event: MouseEvent | TouchEvent) {
async speechToText(event: MouseEvent | TouchEvent | KeyboardEvent) {
event.preventDefault();
const transcribeButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-transcribe")[0];
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
@@ -919,9 +1165,19 @@ export class KhojChatView extends KhojPaneView {
});
// Parse response from Khoj backend
let noSpeechText: string[] = [
"Thanks for watching!",
"Thanks for watching.",
"Thank you for watching!",
"Thank you for watching.",
"You",
"Bye."
];
let noSpeech: boolean = false;
if (response.status === 200) {
console.log(response);
chatInput.value += response.json.text.trimStart();
noSpeech = noSpeechText.includes(response.json.text.trimStart());
if (!noSpeech) chatInput.value += response.json.text.trimStart();
this.autoResize();
} else if (response.status === 501) {
throw new Error("⛔️ Configure speech-to-text model on server.");
@@ -931,8 +1187,8 @@ export class KhojChatView extends KhojPaneView {
throw new Error("⛔️ Failed to transcribe audio.");
}
// Don't auto-send empty messages
if (chatInput.value.length === 0) return;
// Don't auto-send empty messages or when no speech is detected
if (chatInput.value.length === 0 || noSpeech) return;
// Show stop auto-send button. It stops auto-send when clicked
setIcon(sendButton, "stop-circle");
@@ -941,6 +1197,7 @@ export class KhojChatView extends KhojPaneView {
// Start the countdown timer UI
stopSendButtonImg.getElementsByTagName("circle")[0].style.animation = "countdown 3s linear 1 forwards";
stopSendButtonImg.getElementsByTagName("circle")[0].style.color = "var(--icon-color-active)";
// Auto send message after 3 seconds
this.sendMessageTimeout = setTimeout(() => {
@@ -950,7 +1207,7 @@ export class KhojChatView extends KhojPaneView {
sendImg.addEventListener('click', async (_) => { await this.chat() });
// Send message
this.chat();
this.chat(true);
}, 3000);
};
@@ -969,21 +1226,23 @@ export class KhojChatView extends KhojPaneView {
});
this.mediaRecorder.start();
setIcon(transcribeButton, "mic-off");
// setIcon(transcribeButton, "mic-off");
transcribeButton.classList.add("loading-encircle")
};
// Toggle recording
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart') {
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart' || event.type === 'mousedown' || event.type === 'keydown') {
navigator.mediaDevices
.getUserMedia({ audio: true })
?.then(handleRecording)
.catch((e) => {
this.flashStatusInChatInput("⛔️ Failed to access microphone");
});
} else if (this.mediaRecorder.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel') {
} else if (this.mediaRecorder?.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel' || event.type === 'mouseup' || event.type === 'keyup') {
this.mediaRecorder.stop();
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
this.mediaRecorder = undefined;
transcribeButton.classList.remove("loading-encircle");
setIcon(transcribeButton, "mic");
}
}
@@ -1009,7 +1268,9 @@ export class KhojChatView extends KhojPaneView {
onChatInput() {
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
chatInput.value = chatInput.value.trimStart();
this.currentMessageIndex = -1;
// store the current input
this.currentUserInput = chatInput.value;
this.autoResize();
}
@@ -1055,30 +1316,21 @@ export class KhojChatView extends KhojPaneView {
handleStreamResponse(newResponseElement: HTMLElement | null, rawResponse: string, loadingEllipsis: HTMLElement | null, replace = true) {
if (!newResponseElement) return;
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis) {
// Remove loading ellipsis if it exists
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
newResponseElement.removeChild(loadingEllipsis);
}
if (replace) {
newResponseElement.innerHTML = "";
}
// Clear the response element if replace is true
if (replace) newResponseElement.innerHTML = "";
// Append response to the response element
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse, false, replace));
// Append loading ellipsis if it exists
if (!replace && loadingEllipsis) newResponseElement.appendChild(loadingEllipsis);
// Scroll to bottom of chat view
this.scrollChatToBottom();
}
handleCompiledReferences(rawResponseElement: HTMLElement | null, chunk: string, references: any, rawResponse: string) {
if (!rawResponseElement || !chunk) return { rawResponse, references };
const [additionalResponse, rawReference] = chunk.split("### compiled references:", 2);
rawResponse += additionalResponse;
rawResponseElement.innerHTML = "";
rawResponseElement.appendChild(this.formatHTMLMessage(rawResponse));
const rawReferenceAsJson = JSON.parse(rawReference);
references = this.extractReferences(rawReferenceAsJson);
return { rawResponse, references };
}
handleImageResponse(imageJson: any, rawResponse: string) {
if (imageJson.image) {
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
@@ -1095,33 +1347,10 @@ export class KhojChatView extends KhojPaneView {
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
}
let references = {};
if (imageJson.context && imageJson.context.length > 0) {
references = this.extractReferences(imageJson.context);
}
if (imageJson.detail) {
// If response has detail field, response is an error message.
rawResponse += imageJson.detail;
}
return { rawResponse, references };
}
// If response has detail field, response is an error message.
if (imageJson.detail) rawResponse += imageJson.detail;
extractReferences(rawReferenceAsJson: any): object {
let references: any = {};
if (rawReferenceAsJson instanceof Array) {
references["notes"] = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
references["online"] = rawReferenceAsJson;
}
return references;
}
addMessageToChatBody(rawResponse: string, newResponseElement: HTMLElement | null, references: any) {
if (!newResponseElement) return;
newResponseElement.innerHTML = "";
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse));
this.finalizeChatBodyResponse(references, newResponseElement);
return rawResponse;
}
finalizeChatBodyResponse(references: object, newResponseElement: HTMLElement | null) {
@@ -1173,4 +1402,27 @@ export class KhojChatView extends KhojPaneView {
return referencesDiv;
}
// function to loop through the user's past messages
handleArrowKeys(event: KeyboardEvent) {
const chatInput = event.target as HTMLTextAreaElement;
const isModKey = Platform.isMacOS ? event.metaKey : event.ctrlKey;
if (isModKey && event.key === 'ArrowUp') {
event.preventDefault();
if (this.currentMessageIndex < this.userMessages.length - 1) {
this.currentMessageIndex++;
chatInput.value = this.userMessages[this.userMessages.length - 1 - this.currentMessageIndex];
}
} else if (isModKey && event.key === 'ArrowDown') {
event.preventDefault();
if (this.currentMessageIndex > 0) {
this.currentMessageIndex--;
chatInput.value = this.userMessages[this.userMessages.length - 1 - this.currentMessageIndex];
} else if (this.currentMessageIndex === 0) {
this.currentMessageIndex = -1;
chatInput.value = this.currentUserInput;
}
}
}
}

View File

@@ -2,7 +2,8 @@ import { Plugin, WorkspaceLeaf } from 'obsidian';
import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings'
import { KhojSearchModal } from 'src/search_modal'
import { KhojChatView } from 'src/chat_view'
import { updateContentIndex, canConnectToBackend, KhojView } from './utils';
import { updateContentIndex, canConnectToBackend, KhojView, jumpToPreviousView } from './utils';
import { KhojPaneView } from './pane_view';
export default class Khoj extends Plugin {
@@ -79,16 +80,30 @@ export default class Khoj extends Plugin {
const leaves = workspace.getLeavesOfType(viewType);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
await leaf?.setViewState({ type: viewType, active: true });
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
await leaf?.setViewState({ type: viewType, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf) workspace.revealLeaf(leaf);
}
if (leaf) {
const activeKhojLeaf = workspace.getActiveViewOfType(KhojPaneView)?.leaf;
// Jump to the previous view if the current view is Khoj Side Pane
if (activeKhojLeaf === leaf) jumpToPreviousView();
// Else Reveal the leaf in case it is in a collapsed sidebar
else {
workspace.revealLeaf(leaf);
if (viewType === KhojView.CHAT) {
// focus on the chat input when the chat view is opened
let chatView = leaf.view as KhojChatView;
let chatInput = <HTMLTextAreaElement>chatView.contentEl.getElementsByClassName("khoj-chat-input")[0];
if (chatInput) chatInput.focus();
}
}
}
}
}

View File

@@ -38,16 +38,24 @@ export abstract class KhojPaneView extends ItemView {
const leaves = workspace.getLeavesOfType(viewType);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
await leaf?.setViewState({ type: viewType, active: true });
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
await leaf?.setViewState({ type: viewType, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf) workspace.revealLeaf(leaf);
}
if (leaf) {
if (viewType === KhojView.CHAT) {
// focus on the chat input when the chat view is opened
let chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
if (chatInput) chatInput.focus();
}
// "Reveal" the leaf in case it is in a collapsed sidebar
workspace.revealLeaf(leaf);
}
}
}

View File

@@ -1,6 +1,6 @@
import { App, SuggestModal, request, MarkdownRenderer, Instruction, Platform } from 'obsidian';
import { KhojSetting } from 'src/settings';
import { createNoteAndCloseModal, getLinkToEntry } from 'src/utils';
import { supportedBinaryFileTypes, createNoteAndCloseModal, getFileFromPath, getLinkToEntry, supportedImageFilesTypes } from 'src/utils';
export interface SearchResult {
entry: string;
@@ -112,28 +112,41 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
let os_path_separator = result.file.includes('\\') ? '\\' : '/';
let filename = result.file.split(os_path_separator).pop();
// Remove YAML frontmatter when rendering string
result.entry = result.entry.replace(/---[\n\r][\s\S]*---[\n\r]/, '');
// Truncate search results to lines_to_render
let entry_snipped_indicator = result.entry.split('\n').length > lines_to_render ? ' **...**' : '';
let snipped_entry = result.entry.split('\n').slice(0, lines_to_render).join('\n');
// Show filename of each search result for context
el.createEl("div",{ cls: 'khoj-result-file' }).setText(filename ?? "");
let result_el = el.createEl("div", { cls: 'khoj-result-entry' })
let resultToRender = "";
let fileExtension = filename?.split(".").pop() ?? "";
if (supportedImageFilesTypes.includes(fileExtension) && filename) {
let linkToEntry: string = filename;
let imageFiles = this.app.vault.getFiles().filter(file => supportedImageFilesTypes.includes(fileExtension));
// Find vault file of chosen search result
let fileInVault = getFileFromPath(imageFiles, result.file);
if (fileInVault)
linkToEntry = this.app.vault.getResourcePath(fileInVault);
resultToRender = `![](${linkToEntry})`;
} else {
// Remove YAML frontmatter when rendering string
result.entry = result.entry.replace(/---[\n\r][\s\S]*---[\n\r]/, '');
// Truncate search results to lines_to_render
let entry_snipped_indicator = result.entry.split('\n').length > lines_to_render ? ' **...**' : '';
let snipped_entry = result.entry.split('\n').slice(0, lines_to_render).join('\n');
resultToRender = `${snipped_entry}${entry_snipped_indicator}`;
}
// @ts-ignore
MarkdownRenderer.renderMarkdown(snipped_entry + entry_snipped_indicator, result_el, result.file, null);
MarkdownRenderer.renderMarkdown(resultToRender, result_el, result.file, null);
}
async onChooseSuggestion(result: SearchResult, _: MouseEvent | KeyboardEvent) {
// Get all markdown and PDF files in vault
// Get all markdown, pdf and image files in vault
const mdFiles = this.app.vault.getMarkdownFiles();
const pdfFiles = this.app.vault.getFiles().filter(file => file.extension === 'pdf');
const binaryFiles = this.app.vault.getFiles().filter(file => supportedBinaryFileTypes.includes(file.extension));
// Find, Open vault file at heading of chosen search result
let linkToEntry = getLinkToEntry(mdFiles.concat(pdfFiles), result.file, result.entry);
let linkToEntry = getLinkToEntry(mdFiles.concat(binaryFiles), result.file, result.entry);
if (linkToEntry) this.app.workspace.openLinkText(linkToEntry, '');
}
}

View File

@@ -10,7 +10,6 @@ export interface UserInfo {
email?: string;
}
export interface KhojSetting {
resultsCount: number;
khojUrl: string;

View File

@@ -48,11 +48,14 @@ function filenameToMimeType (filename: TFile): string {
}
}
export const supportedImageFilesTypes = ['png', 'jpg', 'jpeg'];
export const supportedBinaryFileTypes = ['pdf'].concat(supportedImageFilesTypes);
export const supportedFileTypes = ['md', 'markdown'].concat(supportedBinaryFileTypes);
export async function updateContentIndex(vault: Vault, setting: KhojSetting, lastSync: Map<TFile, number>, regenerate: boolean = false): Promise<Map<TFile, number>> {
// Get all markdown, pdf files in the vault
console.log(`Khoj: Updating Khoj content index...`)
const files = vault.getFiles().filter(file => file.extension === 'md' || file.extension === 'markdown' || file.extension === 'pdf');
const binaryFileTypes = ['pdf']
const files = vault.getFiles().filter(file => supportedFileTypes.includes(file.extension));
let countOfFilesToIndex = 0;
let countOfFilesToDelete = 0;
lastSync = lastSync.size > 0 ? lastSync : new Map<TFile, number>();
@@ -66,7 +69,7 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
}
countOfFilesToIndex++;
const encoding = binaryFileTypes.includes(file.extension) ? "binary" : "utf8";
const encoding = supportedBinaryFileTypes.includes(file.extension) ? "binary" : "utf8";
const mimeType = fileExtensionToMimeType(file.extension) + (encoding === "utf8" ? "; charset=UTF-8" : "");
const fileContent = encoding == 'binary' ? await vault.readBinary(file) : await vault.read(file);
fileData.push({blob: new Blob([fileContent], { type: mimeType }), path: file.path});
@@ -89,10 +92,11 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
for (let i = 0; i < fileData.length; i += 1000) {
const filesGroup = fileData.slice(i, i + 1000);
const formData = new FormData();
const method = regenerate ? "PUT" : "PATCH";
filesGroup.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
// Call Khoj backend to update index with all markdown, pdf files
const response = await fetch(`${setting.khojUrl}/api/v1/index/update?force=${regenerate}&client=obsidian`, {
method: 'POST',
const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, {
method: method,
headers: {
'Authorization': `Bearer ${setting.khojApiKey}`,
},
@@ -201,12 +205,12 @@ export function getBackendStatusMessage(
): string {
// Welcome message with default settings. Khoj cloud always expects an API key.
if (!khojApiKey && khojUrl === 'https://app.khoj.dev')
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`;
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/settings#clients and set it in the Khoj plugin settings on Obsidian`;
if (!connectedToServer)
return `Could not connect to Khoj at ${khojUrl}. Ensure your can access it`;
else if (!userEmail)
return `✅ Connected to Khoj. ❗Get a valid API key from ${khojUrl}/config#clients to log in`;
return `✅ Connected to Khoj. ❗Get a valid API key from ${khojUrl}/settings#clients to log in`;
else if (userEmail === 'default@example.com')
// Logged in as default user in anonymous mode
return `✅ Signed in to Khoj`;
@@ -333,6 +337,12 @@ export function createCopyParentText(message: string, originalButton: string = '
}
}
export function jumpToPreviousView() {
const editor: Editor = this.app.workspace.getActiveFileView()?.editor
if (!editor) return;
editor.focus();
}
export function pasteTextAtCursor(text: string | undefined) {
// Get the current active file's editor
const editor: Editor = this.app.workspace.getActiveFileView()?.editor
@@ -347,15 +357,21 @@ export function pasteTextAtCursor(text: string | undefined) {
}
}
export function getLinkToEntry(sourceFiles: TFile[], chosenFile: string, chosenEntry: string): string | undefined {
export function getFileFromPath(sourceFiles: TFile[], chosenFile: string): TFile | undefined {
// Find the vault file matching file of chosen file, entry
let fileMatch = sourceFiles
// Sort by descending length of path
// This finds longest path match when multiple files have same name
.sort((a, b) => b.path.length - a.path.length)
// The first match is the best file match across OS
// e.g Khoj server on Linux, Obsidian vault on Android
// e.g. Khoj server on Linux, Obsidian vault on Android
.find(file => chosenFile.replace(/\\/g, "/").endsWith(file.path))
return fileMatch;
}
export function getLinkToEntry(sourceFiles: TFile[], chosenFile: string, chosenEntry: string): string | undefined {
// Find the vault file matching file of chosen file, entry
let fileMatch = getFileFromPath(sourceFiles, chosenFile);
// Return link to vault file at heading of chosen search result
if (fileMatch) {

View File

@@ -74,17 +74,23 @@ If your plugin does not need CSS, delete this file.
padding: 10px;
position: relative;
display: inline-block;
max-width: 80%;
text-align: left;
user-select: text;
color: var(--text-normal);
background-color: var(--active-bg);
}
/* color chat bubble by khoj blue */
.khoj-chat-message-text.khoj {
color: var(--khoj-storm-grey);
background: var(--khoj-winter-sun);
border: 1px solid var(--khoj-sun);
margin-left: auto;
white-space: pre-line;
}
/* Override white-space for ul, ol, li under khoj-chat-message-text.khoj */
.khoj-chat-message-text.khoj ul,
.khoj-chat-message-text.khoj ol,
.khoj-chat-message-text.khoj li {
white-space: normal;
}
/* add left protrusion to khoj chat bubble */
.khoj-chat-message-text.khoj:after {
content: '';
@@ -92,14 +98,12 @@ If your plugin does not need CSS, delete this file.
bottom: -2px;
left: -7px;
border: 10px solid transparent;
border-top-color: var(--khoj-winter-sun);
border-bottom: 0;
transform: rotate(-60deg);
}
/* color chat bubble by you dark grey */
.khoj-chat-message-text.you {
color: var(--text-on-accent);
background: var(--khoj-storm-grey);
border: 1px solid var(--color-accent);
margin-right: auto;
}
/* add right protrusion to you chat bubble */
@@ -109,7 +113,6 @@ If your plugin does not need CSS, delete this file.
top: 91%;
right: -2px;
border: 10px solid transparent;
border-left-color: var(--khoj-storm-grey);
border-right: 0;
margin-top: -10px;
transform: rotate(-60deg)
@@ -160,9 +163,8 @@ div.expanded.reference-section {
margin: 10px 0;
}
button.reference-button {
background: var(--khoj-winter-sun);
color: var(--khoj-storm-grey);
border: 1px solid var(--khoj-storm-grey);
background-color: transparent;
border-radius: 5px;
padding: 4px;
font-size: 14px;
@@ -202,8 +204,7 @@ button.reference-button[aria-expanded="true"]::before {
transform: rotate(90deg);
}
button.reference-expand-button {
background: var(--khoj-winter-sun);
color: var(--khoj-storm-grey);
background-color: transparent;
border: 1px solid var(--khoj-storm-grey);
border-radius: 5px;
padding: 8px;
@@ -216,8 +217,8 @@ button.reference-expand-button {
text-align: left;
}
button.reference-expand-button:hover {
background: var(--khoj-sun);
color: var(--khoj-storm-grey);
background: var(--background-modifier-active-hover);
color: var(--text-normal);
}
a.inline-chat-link {
color: #475569;
@@ -229,15 +230,6 @@ a.inline-chat-link {
border-bottom: 1px dotted var(--khoj-storm-grey);
}
button.copy-button {
display: block;
border-radius: 4px;
background-color: var(--color-base-00);
}
button.copy-button:hover {
background: #f5f5f5;
cursor: pointer;
}
img {
max-width: 60%;
}
@@ -270,19 +262,8 @@ div.conversation-session {
grid-template-columns: 1fr auto;
}
.three-dot-menu {
display: block;
/* background: var(--background-color); */
/* border: 1px solid var(--main-text-color); */
border-radius: 5px;
/* position: relative; */
}
button.selected-conversation {
background: var(--khoj-winter-sun);
}
button.three-dot-menu-button-item {
color: var(--color-base-90);
color: var(--text-accent);
border: none;
box-shadow: none;
font-size: 14px;
@@ -296,26 +277,7 @@ button.three-dot-menu-button-item {
}
button.three-dot-menu-button-item:hover {
background: var(--khoj-storm-grey);
color: var(--khoj-winter-sun);
}
.three-dot-menu-button {
background: var(--khoj-winter-sun);
border: none;
box-shadow: none;
font-size: 14px;
font-weight: 300;
line-height: 1.5em;
cursor: pointer;
transition: background 0.3s ease-in-out;
font-family: var(--font-family);
border-radius: 4px;
right: 0;
}
.conversation-button:hover .three-dot-menu {
display: block;
background: var(--background-modifier-active-hover);
}
div.conversation-menu {
@@ -325,13 +287,15 @@ div.conversation-menu {
text-align: right;
border-radius: 5px;
padding: 5px;
display: grid;
grid-gap: 4px;
grid-auto-flow: column;
}
div.conversation-session:hover {
transform: scale(1.03);
}
div.selected-conversation {
background: var(--khoj-winter-sun) !important;
color: var(--khoj-storm-grey) !important;
background: var(--background-modifier-active-hover) !important;
}
#khoj-chat-footer {
@@ -373,9 +337,8 @@ div.selected-conversation {
position: relative;
}
#khoj-chat-send .lucide-arrow-up-circle {
background: var(--khoj-sun);
background: var(--background-modifier-active-hover);
border-radius: 50%;
color: #222;
}
#khoj-chat-send .lucide-stop-circle {
transform: rotateY(-180deg) rotateZ(-90deg);
@@ -488,7 +451,7 @@ div.khoj-logo {
}
.khoj-nav a {
color: var(--main-text-color);
color: var(--text-normal);
text-decoration: none;
font-size: small;
font-weight: normal;
@@ -498,11 +461,11 @@ div.khoj-logo {
margin: 0;
}
.khoj-nav a:hover {
background-color: var(--khoj-sun);
background-color: var(--background-modifier-active-hover);
color: var(--main-text-color);
}
a.khoj-nav-selected {
background-color: var(--khoj-winter-sun);
background-color: var(--background-modifier-active-hover);
}
#similar-nav-icon-svg,
.khoj-nav-icon {
@@ -520,10 +483,12 @@ span.khoj-nav-item-text {
}
/* Copy button */
button.copy-button {
button.chat-action-button {
display: block;
border-radius: 4px;
background-color: var(--background-color);
border: 1px solid var(--main-text-color);
color: var(--text-muted);
background-color: transparent;
border: 1px solid var(--khoj-storm-grey);
text-align: center;
font-size: 16px;
transition: all 0.5s;
@@ -532,28 +497,54 @@ button.copy-button {
margin-top: 8px;
float: right;
}
button.copy-button span {
button.chat-action-button span {
cursor: pointer;
display: inline-block;
position: relative;
transition: 0.5s;
}
button.chat-action-button:hover {
background-color: var(--background-modifier-active-hover);
color: var(--text-normal);
}
img.copy-icon {
width: 16px;
height: 16px;
}
.you button.copy-button {
color: var(--text-on-accent);
/* Circular Loading Spinner */
.loader {
width: 18px;
height: 18px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.khoj button.copy-button {
color: var(--khoj-storm-grey);
.loader::after {
content: '';
box-sizing: border-box;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 18px;
height: 18px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: var(--flower);
}
.you button.copy-button:hover {
color: var(--khoj-storm-grey);
background: var(--text-on-accent);
}
.khoj button.copy-button:hover {
background: var(--text-on-accent);
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Loading Spinner */
@@ -613,6 +604,44 @@ img.copy-icon {
}
}
/* Loading Encircle */
.loading-encircle {
position: relative;
}
.loading-encircle::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 24px;
margin-top: -16px;
margin-left: -16px;
border: 4px solid transparent;
border-color: var(--icon-color-active);
border-radius: 50%;
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.2;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media only screen and (max-width: 600px) {
div.khoj-header {
display: grid;

View File

@@ -51,5 +51,17 @@
"1.12.0": "0.15.0",
"1.12.1": "0.15.0",
"1.13.0": "0.15.0",
"1.14.0": "0.15.0"
"1.14.0": "0.15.0",
"1.15.0": "0.15.0",
"1.16.0": "0.15.0",
"1.17.0": "0.15.0",
"1.20.0": "0.15.0",
"1.20.1": "0.15.0",
"1.20.2": "0.15.0",
"1.20.3": "0.15.0",
"1.20.4": "0.15.0",
"1.21.0": "0.15.0",
"1.21.1": "0.15.0",
"1.21.2": "0.15.0",
"1.21.3": "0.15.0"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
NEXT_PUBLIC_ENV='development'

View File

@@ -0,0 +1 @@
NEXT_PUBLIC_ENV='production'

View File

@@ -0,0 +1,11 @@
{
"extends": [
"next",
"next/core-web-vitals",
"plugin:prettier/recommended"
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "warn"
}
}

36
src/interface/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
yarn run lint-staged
yarn test

View File

@@ -0,0 +1,93 @@
This is a [Next.js](https://nextjs.org/) project.
## Getting Started
First, install the dependencies:
```bash
yarn install
```
In case you run into any dependency linking issues, you can try running:
```bash
yarn add next
```
### Run the development server:
```bash
yarn dev
```
Make sure the `rewrites` in `next.config.mjs` are set up correctly for your environment. The rewrites are used to proxy requests to the API server.
```js
rewrites: async () => {
return [
{
source: '/api/:path*',
destination: 'http://localhost:42110/api/:path*',
},
];
},
```
The `destination` should be the URL of the API server.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying any of the `.tsx` pages. The page auto-updates as you edit the file.
### Testing built files
We've setup a utility command for building and serving the built files. This is useful for testing the production build locally.
1. Exporting code
To build the files once and serve them, run:
```bash
yarn export
```
If you're using Windows:
```bash
yarn windowsexport
```
2. Continuously building code
To keep building the files and serving them, run:
```bash
yarn watch
```
If you're using Windows:
```bash
yarn windowswatch
```
Now you should be able to load your custom pages from the Khoj app at http://localhost:42110/. To server any of the built files, you should update the routes in the `web_client.py` like so, where `new_file` is the new page you've added in this repo:
```python
@web_client.post("/new_route", response_class=FileResponse)
@requires(["authenticated"], redirect="login_page")
def index_post(request: Request):
return templates.TemplateResponse(
"new_file/index.html",
context={
"request": request,
},
)
```
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Next.js App Router](https://nextjs.org/docs/app) - learn about the Next.js router.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -0,0 +1,66 @@
div.titleBar {
padding: 16px 0;
text-align: left;
}
.agentPersonality p {
white-space: inherit;
overflow: hidden;
height: 77px;
line-height: 1.5;
}
div.agentPersonality {
text-align: left;
grid-column: span 3;
overflow: hidden;
}
div.pageLayout {
max-width: 60vw;
margin: auto;
margin-bottom: 2rem;
}
div.sidePanel {
position: fixed;
height: 100%;
z-index: 1;
}
button.infoButton {
border: none;
background-color: transparent !important;
text-align: left;
font-family: inherit;
font-size: medium;
}
div.agentList {
display: grid;
gap: 20px;
padding-top: 30px;
margin-right: auto;
grid-auto-flow: row;
grid-template-columns: 1fr 1fr;
margin-left: auto;
}
@media only screen and (max-width: 768px) {
div.agentList {
width: 100%;
padding: 0;
margin-right: auto;
margin-left: auto;
grid-template-columns: 1fr;
}
div.pageLayout {
max-width: 90vw;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View File

@@ -0,0 +1,52 @@
import type { Metadata } from "next";
import { Noto_Sans } from "next/font/google";
import "../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Agents",
description: "Find a specialized agent that can help you address more specific needs.",
icons: {
icon: "/static/assets/icons/khoj_lantern.ico",
apple: "/static/assets/icons/khoj_lantern_256x256.png",
},
openGraph: {
siteName: "Khoj AI",
title: "Khoj AI - Agents",
description: "Your Second Brain.",
url: "https://app.khoj.dev/agents",
type: "website",
images: [
{
url: "https://assets.khoj.dev/khoj_lantern_256x256.png",
width: 256,
height: 256,
},
],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
media-src * blob:;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,339 @@
"use client";
import styles from "./agents.module.css";
import Image from "next/image";
import useSWR from "swr";
import { useState } from "react";
import { useAuthenticatedData, UserProfile } from "../common/auth";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { PaperPlaneTilt, Lightning, Plus } from "@phosphor-icons/react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import LoginPrompt from "../components/loginPrompt/loginPrompt";
import { InlineLoading } from "../components/loading/loading";
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import { getIconFromIconName } from "../common/iconUtils";
import { convertColorToTextClass } from "../common/colorUtils";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useIsMobileWidth } from "../common/utils";
export interface AgentData {
slug: string;
avatar: string;
name: string;
persona: string;
color: string;
icon: string;
}
async function openChat(slug: string, userData: UserProfile | null) {
const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`;
if (!userData) {
window.location.href = unauthenticatedRedirectUrl;
return;
}
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
const data = await response.json();
if (response.status == 200) {
window.location.href = `/chat?conversationId=${data.conversation_id}`;
} else if (response.status == 403 || response.status == 401) {
window.location.href = unauthenticatedRedirectUrl;
} else {
alert("Failed to start chat session");
}
}
const agentsFetcher = () =>
window
.fetch("/api/agents")
.then((res) => res.json())
.catch((err) => console.log(err));
interface AgentCardProps {
data: AgentData;
userProfile: UserProfile | null;
isMobileWidth: boolean;
}
function AgentCard(props: AgentCardProps) {
const searchParams = new URLSearchParams(window.location.search);
const agentSlug = searchParams.get("agent");
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const userData = props.userProfile;
if (showModal) {
window.history.pushState(
{},
`Khoj AI - Agent ${props.data.slug}`,
`/agents?agent=${props.data.slug}`,
);
}
const stylingString = convertColorToTextClass(props.data.color);
return (
<Card
className={`shadow-sm bg-gradient-to-b from-white 20% to-${props.data.color ? props.data.color : "gray"}-100/50 dark:from-[hsl(var(--background))] dark:to-${props.data.color ? props.data.color : "gray"}-950/50 rounded-xl hover:shadow-md`}
>
{showLoginPrompt && (
<LoginPrompt
loginRedirectMessage={`Sign in to start chatting with ${props.data.name}`}
onOpenChange={setShowLoginPrompt}
/>
)}
<CardHeader>
<CardTitle>
{!props.isMobileWidth ? (
<Dialog
open={showModal}
onOpenChange={() => {
setShowModal(!showModal);
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
}}
>
<DialogTrigger>
<div className="flex items-center relative top-2">
{getIconFromIconName(props.data.icon, props.data.color) || (
<Image
src={props.data.avatar}
alt={props.data.name}
width={50}
height={50}
/>
)}
{props.data.name}
</div>
</DialogTrigger>
<div className="float-right">
{props.userProfile ? (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => openChat(props.data.slug, userData)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
) : (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => setShowLoginPrompt(true)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
)}
</div>
<DialogContent className="whitespace-pre-line max-h-[80vh]">
<DialogHeader>
<div className="flex items-center">
{getIconFromIconName(props.data.icon, props.data.color) || (
<Image
src={props.data.avatar}
alt={props.data.name}
width={32}
height={50}
/>
)}
<p className="font-bold text-lg">{props.data.name}</p>
</div>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
{props.data.persona}
</div>
<DialogFooter>
<Button
className={`pt-6 pb-6 ${stylingString} bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white border-2 border-stone-100 shadow-sm rounded-xl hover:bg-stone-100 dark:hover:bg-neutral-900 dark:border-neutral-700`}
onClick={() => {
openChat(props.data.slug, userData);
setShowModal(false);
}}
>
<PaperPlaneTilt
className={`w-6 h-6 m-2 ${convertColorToTextClass(props.data.color)}`}
/>
Start Chatting
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<Drawer
open={showModal}
onOpenChange={(open) => {
setShowModal(open);
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
}}
>
<DrawerTrigger>
<div className="flex items-center">
{getIconFromIconName(props.data.icon, props.data.color) || (
<Image
src={props.data.avatar}
alt={props.data.name}
width={50}
height={50}
/>
)}
{props.data.name}
</div>
</DrawerTrigger>
<div className="float-right">
{props.userProfile ? (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100`}
onClick={() => openChat(props.data.slug, userData)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
) : (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm`}
onClick={() => setShowLoginPrompt(true)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
)}
</div>
<DrawerContent className="whitespace-pre-line p-2">
<DrawerHeader>
<DrawerTitle>{props.data.name}</DrawerTitle>
<DrawerDescription>Full Prompt</DrawerDescription>
</DrawerHeader>
{props.data.persona}
<DrawerFooter>
<DrawerClose>Done</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className={styles.agentPersonality}>
<button
className={`${styles.infoButton} text-neutral-500 dark:text-white`}
onClick={() => setShowModal(true)}
>
<p>{props.data.persona}</p>
</button>
</div>
</CardContent>
</Card>
);
}
export default function Agents() {
const { data, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
revalidateOnFocus: false,
});
const authenticatedData = useAuthenticatedData();
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const isMobileWidth = useIsMobileWidth();
if (error) {
return (
<main className={styles.main}>
<div className={`${styles.titleBar} text-5xl`}>Agents</div>
<div className={styles.agentList}>Error loading agents</div>
</main>
);
}
if (!data) {
return (
<main className={styles.main}>
<div className={styles.agentList}>
<InlineLoading /> booting up your agents
</div>
</main>
);
}
return (
<main className={`w-full mx-auto`}>
<div className={`grid w-full mx-auto`}>
<div className={`${styles.sidePanel} top-0`}>
<SidePanel
conversationId={null}
uploadedFiles={[]}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={`${styles.pageLayout} w-full`}>
<div className={`pt-6 md:pt-8 flex justify-between`}>
<h1 className="text-3xl flex items-center">Agents</h1>
<div className="ml-auto float-right border p-2 pt-3 rounded-xl font-bold hover:bg-stone-100 dark:hover:bg-neutral-900">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="flex flex-row">
<Plus className="pr-2 w-6 h-6" />
<p className="pr-2">Create Agent</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Coming Soon!</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{showLoginPrompt && (
<LoginPrompt
loginRedirectMessage="Sign in to start chatting with a specialized agent"
onOpenChange={setShowLoginPrompt}
/>
)}
<Alert className="bg-secondary border-none my-4">
<AlertDescription>
<Lightning weight={"fill"} className="h-4 w-4 text-purple-400 inline" />
<span className="font-bold">How it works</span> Use any of these
specialized personas to tune your conversation to your needs.
</AlertDescription>
</Alert>
<div className={`${styles.agentList}`}>
{data.map((agent) => (
<AgentCard
key={agent.slug}
data={agent}
userProfile={authenticatedData}
isMobileWidth={isMobileWidth}
/>
))}
</div>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,37 @@
div.automationsLayout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
div.automationCard {
display: grid;
grid-template-rows: auto 1fr auto;
}
div.pageLayout {
max-width: 60vw;
margin: auto;
margin-bottom: 2rem;
}
div.sidePanel {
position: fixed;
height: 100%;
z-index: 1;
}
@media screen and (max-width: 768px) {
div.automationsLayout {
grid-template-columns: 1fr;
}
div.pageLayout {
max-width: 90vw;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View File

@@ -0,0 +1,40 @@
import type { Metadata } from "next";
import { Toaster } from "@/components/ui/toaster";
import "../globals.css";
export const metadata: Metadata = {
title: "Khoj AI - Automations",
description: "Use Autoomations with Khoj to simplify the process of running repetitive tasks.",
icons: {
icon: "/static/assets/icons/khoj_lantern.ico",
apple: "/static/assets/icons/khoj_lantern_256x256.png",
},
openGraph: {
siteName: "Khoj AI",
title: "Khoj AI - Automations",
description: "Your Second Brain.",
url: "https://app.khoj.dev/automations",
type: "website",
images: [
{
url: "https://assets.khoj.dev/khoj_lantern_256x256.png",
width: 256,
height: 256,
},
],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div>
{children}
<Toaster />
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
div.main {
height: 100dvh;
color: hsla(var(--foreground));
}
.suggestions {
display: flex;
overflow-x: none;
height: 50%;
padding: 10px;
white-space: nowrap;
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); */
gap: 1rem;
/* justify-content: center; */
}
div.inputBox {
border: 1px solid var(--border-color);
margin-bottom: 20px;
gap: 12px;
align-content: center;
}
input.inputBox {
border: none;
}
input.inputBox:focus {
outline: none;
background-color: transparent;
}
div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
div.chatBodyFull {
display: grid;
grid-template-columns: 1fr;
height: 100%;
}
button.inputBox {
border: none;
outline: none;
background-color: transparent;
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem;
background: linear-gradient(var(--calm-green), var(--calm-blue));
}
div.chatBody {
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
}
.inputBox {
color: hsla(var(--foreground));
}
div.chatLayout {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
}
div.chatBox {
display: grid;
height: 100%;
}
div.titleBar {
display: grid;
grid-template-columns: 1fr auto;
}
div.chatBoxBody {
display: grid;
height: 100%;
width: 70%;
margin: auto;
}
div.agentIndicator a {
display: flex;
text-align: center;
align-content: center;
align-items: center;
}
div.agentIndicator {
padding: 10px;
}
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
}
div.chatBoxBody {
width: 100%;
}
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBox {
padding: 0;
height: 100%;
}
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,53 @@
import type { Metadata } from "next";
import { Noto_Sans } from "next/font/google";
import "../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Chat",
description:
"Ask anything. Khoj will use the internet and your docs to answer, paint and even automate stuff for you.",
icons: {
icon: "/static/assets/icons/khoj_lantern.ico",
apple: "/static/assets/icons/khoj_lantern_256x256.png",
},
openGraph: {
siteName: "Khoj AI",
title: "Khoj AI - Chat",
description: "Your Second Brain.",
url: "https://app.khoj.dev/chat",
type: "website",
images: [
{
url: "https://assets.khoj.dev/khoj_lantern_256x256.png",
width: 256,
height: 256,
},
],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
media-src * blob:;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,291 @@
"use client";
import styles from "./chat.module.css";
import React, { Suspense, useEffect, useState } from "react";
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import ChatHistory from "../components/chatHistory/chatHistory";
import { useSearchParams } from "next/navigation";
import Loading from "../components/loading/loading";
import { processMessageChunk } from "../common/chatFunctions";
import "katex/dist/katex.min.css";
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../common/utils";
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
import { useAuthenticatedData } from "../common/auth";
import { AgentData } from "../agents/page";
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
setTitle: (title: string) => void;
onConversationIdChange?: (conversationId: string) => void;
setQueryToProcess: (query: string) => void;
streamedMessages: StreamMessage[];
setUploadedFiles: (files: string[]) => void;
isMobileWidth?: boolean;
isLoggedIn: boolean;
}
function ChatBodyData(props: ChatBodyDataProps) {
const searchParams = useSearchParams();
const conversationId = searchParams.get("conversationId");
const [message, setMessage] = useState("");
const [processingMessage, setProcessingMessage] = useState(false);
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
const setQueryToProcess = props.setQueryToProcess;
const onConversationIdChange = props.onConversationIdChange;
useEffect(() => {
const storedMessage = localStorage.getItem("message");
if (storedMessage) {
setProcessingMessage(true);
setQueryToProcess(storedMessage);
}
}, [setQueryToProcess]);
useEffect(() => {
if (message) {
setProcessingMessage(true);
setQueryToProcess(message);
}
}, [message, setQueryToProcess]);
useEffect(() => {
if (conversationId) {
onConversationIdChange?.(conversationId);
}
}, [conversationId, onConversationIdChange]);
useEffect(() => {
if (
props.streamedMessages &&
props.streamedMessages.length > 0 &&
props.streamedMessages[props.streamedMessages.length - 1].completed
) {
setProcessingMessage(false);
} else {
setMessage("");
}
}, [props.streamedMessages]);
if (!conversationId) {
window.location.href = "/";
return;
}
return (
<>
<div className={false ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory
conversationId={conversationId}
setTitle={props.setTitle}
setAgent={setAgentMetadata}
pendingMessage={processingMessage ? message : ""}
incomingMessages={props.streamedMessages}
/>
</div>
<div
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl`}
>
<ChatInputArea
agentColor={agentMetadata?.color}
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={conversationId}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles}
/>
</div>
</>
);
}
export default function Chat() {
const defaultTitle = "Khoj AI - Chat";
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState(defaultTitle);
const [conversationId, setConversationID] = useState<string | null>(null);
const [messages, setMessages] = useState<StreamMessage[]>([]);
const [queryToProcess, setQueryToProcess] = useState<string>("");
const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const locationData = useIPLocationData();
const authenticatedData = useAuthenticatedData();
const isMobileWidth = useIsMobileWidth();
useEffect(() => {
fetch("/api/chat/options")
.then((response) => response.json())
.then((data: ChatOptions) => {
setLoading(false);
// Render chat options, if any
if (data) {
setChatOptionsData(data);
}
})
.catch((err) => {
console.error(err);
return;
});
welcomeConsole();
}, []);
useEffect(() => {
if (queryToProcess) {
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: new Date().toISOString(),
rawQuery: queryToProcess || "",
};
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true);
}
}, [queryToProcess]);
useEffect(() => {
if (processQuerySignal) {
chat();
}
}, [processQuerySignal]);
async function readChatStream(response: Response) {
if (!response.ok) throw new Error(response.statusText);
if (!response.body) throw new Error("Response body is null");
const reader = response.body.getReader();
const decoder = new TextDecoder();
const eventDelimiter = "␃🔚␗";
let buffer = "";
// Track context used for chat response
let context: Context[] = [];
let onlineContext: OnlineContext = {};
while (true) {
const { done, value } = await reader.read();
if (done) {
setQueryToProcess("");
setProcessQuerySignal(false);
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
let newEventIndex;
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
const event = buffer.slice(0, newEventIndex);
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
if (event) {
const currentMessage = messages.find((message) => !message.completed);
if (!currentMessage) {
console.error("No current message found");
return;
}
// Track context used for chat response. References are rendered at the end of the chat
({ context, onlineContext } = processMessageChunk(
event,
currentMessage,
context,
onlineContext,
));
setMessages([...messages]);
}
}
}
}
async function chat() {
localStorage.removeItem("message");
if (!queryToProcess || !conversationId) return;
let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`;
if (locationData) {
chatAPI += `&region=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`;
}
const response = await fetch(chatAPI);
try {
await readChatStream(response);
} catch (err) {
console.error(err);
// Retrieve latest message being processed
const currentMessage = messages.find((message) => !message.completed);
if (!currentMessage) return;
// Render error message as current message
const errorMessage = (err as Error).message;
currentMessage.rawResponse = `Encountered Error: ${errorMessage}. Please try again later.`;
// Complete message streaming teardown properly
currentMessage.completed = true;
setMessages([...messages]);
setQueryToProcess("");
setProcessQuerySignal(false);
}
}
const handleConversationIdChange = (newConversationId: string) => {
setConversationID(newConversationId);
};
if (isLoading) return <Loading />;
return (
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>
{`${defaultTitle}${!!title && title !== defaultTitle ? `: ${title}` : ""}`}
</title>
<div>
<SidePanel
conversationId={conversationId}
uploadedFiles={uploadedFiles}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={styles.chatBox}>
<div className={styles.chatBoxBody}>
{!isMobileWidth && (
<div
className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}
>
{title && (
<h2
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}
>
{title}
</h2>
)}
</div>
)}
<Suspense fallback={<Loading />}>
<ChatBodyData
isLoggedIn={authenticatedData !== null}
streamedMessages={messages}
chatOptionsData={chatOptionsData}
setTitle={setTitle}
setQueryToProcess={setQueryToProcess}
setUploadedFiles={setUploadedFiles}
isMobileWidth={isMobileWidth}
onConversationIdChange={handleConversationIdChange}
/>
</Suspense>
</div>
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More