diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index a9d47535..9604a786 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -8,7 +8,12 @@ "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json" }, - "keywords": ["search", "chat", "AI", "assistant"], + "keywords": [ + "search", + "chat", + "AI", + "assistant" + ], "author": "Debanjum Singh Solanky", "license": "GPL-3.0-or-later", "devDependencies": { @@ -20,5 +25,9 @@ "obsidian": "latest", "tslib": "2.4.0", "typescript": "4.7.4" + }, + "dependencies": { + "@types/node-fetch": "^2.6.4", + "node-fetch": "3.0.0" } } diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index acca8813..0b5624f8 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -1,6 +1,6 @@ import { App, Modal, request, Setting } from 'obsidian'; import { KhojSetting } from 'src/settings'; - +import fetch from "node-fetch"; export class KhojChatModal extends Modal { result: string; @@ -34,13 +34,8 @@ export class KhojChatModal extends Modal { // Create area for chat logs contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } }); - // Get conversation history from Khoj backend - let chatUrl = `${this.setting.khojUrl}/api/chat?client=obsidian`; - let response = await request(chatUrl); - let chatLogs = JSON.parse(response).response; - chatLogs.forEach((chatLog: any) => { - this.renderMessageWithReferences(chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created)); - }); + // Get chat history from Khoj backend + await this.getChatHistory(); // Add chat input field contentEl.createEl("input", @@ -104,6 +99,35 @@ export class KhojChatModal extends Modal { return chat_message_el } + createKhojResponseDiv(dt?: Date): HTMLDivElement { + let message_time = this.formatDate(dt ?? new Date()); + + // Append message to conversation history HTML element. + // The chat logs should display above the message input box to follow standard UI semantics + let chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0]; + let chat_message_el = chat_body_el.createDiv({ + attr: { + "data-meta": `🏮 Khoj at ${message_time}`, + class: `khoj-chat-message khoj` + }, + }).createDiv({ + attr: { + class: `khoj-chat-message-text khoj` + }, + }) + + // Scroll to bottom after inserting chat messages + this.modalEl.scrollTop = this.modalEl.scrollHeight; + + return chat_message_el + } + + renderIncrementalMessage(htmlElement: HTMLDivElement, additionalMessage: string) { + htmlElement.innerHTML += additionalMessage; + // Scroll to bottom of modal, till the send message input box + this.modalEl.scrollTop = this.modalEl.scrollHeight; + } + formatDate(date: Date): string { // Format date in HH:MM, DD MMM YYYY format let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false }); @@ -111,6 +135,17 @@ export class KhojChatModal extends Modal { return `${time_string}, ${date_string}`; } + + async getChatHistory(): Promise { + // Get chat history from Khoj backend + let chatUrl = `${this.setting.khojUrl}/api/chat/init?client=obsidian`; + let response = await request(chatUrl); + let chatLogs = JSON.parse(response).response; + chatLogs.forEach((chatLog: any) => { + this.renderMessageWithReferences(chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created)); + }); + } + async getChatResponse(query: string | undefined | null): Promise { // Exit if query is empty if (!query || query === "") return; @@ -121,10 +156,30 @@ export class KhojChatModal extends Modal { // Get chat response from Khoj backend let encodedQuery = encodeURIComponent(query); let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&client=obsidian`; - let response = await request(chatUrl); - let data = JSON.parse(response); - // Render Khoj response as chat message - this.renderMessage(data.response, "khoj"); + let response = await fetch(chatUrl, { + method: "GET", + headers: { + "Access-Control-Allow-Origin": "*", + "Content-Type": "text/event-stream" + }, + }) + let responseElemeent = this.createKhojResponseDiv(); + + try { + if (response.body == null) { + throw new Error("Response body is null"); + } + + for await (const chunk of response.body) { + const responseText = chunk.toString(); + if (responseText.startsWith("### compiled references:")) { + return; + } + this.renderIncrementalMessage(responseElemeent, responseText); + } + } catch (err) { + this.renderIncrementalMessage(responseElemeent, "Sorry, unable to get response from Khoj backend ❤️‍🩹. Contact developer for help at team@khoj.dev or in Discord") + } } } diff --git a/src/interface/obsidian/yarn.lock b/src/interface/obsidian/yarn.lock index a11ea15e..c5ffbb28 100644 --- a/src/interface/obsidian/yarn.lock +++ b/src/interface/obsidian/yarn.lock @@ -40,6 +40,19 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/node-fetch@^2.6.4": + version "2.6.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" + integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node@*": + version "20.3.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" + integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw== + "@types/node@^16.11.6": version "16.18.12" resolved "https://registry.npmjs.org/@types/node/-/node-16.18.12.tgz" @@ -137,6 +150,11 @@ array-union@^2.1.0: resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + braces@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -149,6 +167,18 @@ builtin-modules@3.3.0: resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +data-uri-to-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== + debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -156,6 +186,11 @@ debug@^4.3.4: dependencies: ms "2.1.2" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -349,6 +384,14 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fetch-blob@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -356,6 +399,15 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" @@ -422,6 +474,18 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + moment@2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" @@ -432,6 +496,19 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0.tgz#79da7146a520036f2c5f644e4a26095f17e411ea" + integrity sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q== + dependencies: + data-uri-to-buffer "^3.0.1" + fetch-blob "^3.1.2" + obsidian@latest: version "1.1.1" resolved "https://registry.npmjs.org/obsidian/-/obsidian-1.1.1.tgz" @@ -513,6 +590,11 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index d5d55956..293852b0 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -77,7 +77,6 @@ // Call specified Khoj API which returns a streamed response of type text/plain fetch(url) .then(response => { - console.log(response); const reader = response.body.getReader(); const decoder = new TextDecoder();