mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 08:04:21 +00:00
Stream steps taken to generate response in Obsidian chat pane
- Setup websocket using Khoj web app as reference. - Moved the geolocating code to chat view out from the general pane view - Use loading spinner from web instead of the thinking emoji
This commit is contained in:
parent
afcd22d30c
commit
14a2006c76
4 changed files with 576 additions and 32 deletions
|
@ -10,17 +10,59 @@ export interface ChatJsonResult {
|
|||
inferredQueries?: string[];
|
||||
}
|
||||
|
||||
interface WebSocketState {
|
||||
newResponseTextEl: HTMLElement | null,
|
||||
newResponseEl: HTMLElement | null,
|
||||
loadingEllipsis: HTMLElement | null,
|
||||
references: object,
|
||||
rawResponse: string,
|
||||
}
|
||||
|
||||
export class KhojChatView extends KhojPaneView {
|
||||
result: string;
|
||||
setting: KhojSetting;
|
||||
interface Location {
|
||||
region: string;
|
||||
city: string;
|
||||
countryName: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export class KhojChatView extends KhojPaneView {
|
||||
result: string;
|
||||
setting: KhojSetting;
|
||||
waitingForLocation: boolean;
|
||||
websocket: WebSocket;
|
||||
websocketState: WebSocketState;
|
||||
location: Location;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf, setting);
|
||||
|
||||
this.waitingForLocation = true;
|
||||
this.websocketState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
};
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.location = {
|
||||
region: data.region,
|
||||
city: data.city,
|
||||
countryName: data.country_name,
|
||||
timezone: data.timezone,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.waitingForLocation = false;
|
||||
this.setupWebSocket();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
|
@ -36,6 +78,11 @@ export class KhojChatView extends KhojPaneView {
|
|||
}
|
||||
|
||||
async chat() {
|
||||
if (this.websocket?.readyState === WebSocket.OPEN){
|
||||
this.sendMessageViaWebSocket();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get text in chat input element
|
||||
let input_el = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
|
||||
|
@ -119,6 +166,93 @@ export class KhojChatView extends KhojPaneView {
|
|||
});
|
||||
}
|
||||
|
||||
processOnlineReferences(referenceSection: HTMLElement, onlineContext: any) {
|
||||
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 = this.generateOnlineReference(referenceSection, 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 = this.generateOnlineReference(referenceSection, 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 = this.generateOnlineReference(referenceSection, 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 = this.generateOnlineReference(referenceSection, reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return numOnlineReferences;
|
||||
}
|
||||
|
||||
generateOnlineReference(messageEl: Element, reference: any, index: string) {
|
||||
// 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 = messageEl.createEl('a');
|
||||
linkElement.setAttribute('href', link);
|
||||
linkElement.setAttribute('target', '_blank');
|
||||
linkElement.setAttribute('rel', 'noopener noreferrer');
|
||||
linkElement.classList.add("reference-link");
|
||||
linkElement.setAttribute('title', title);
|
||||
linkElement.textContent = title;
|
||||
|
||||
let referenceButton = messageEl.createEl('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;
|
||||
}
|
||||
|
||||
generateReference(messageEl: Element, reference: string, index: number) {
|
||||
// Escape reference for HTML rendering
|
||||
let escaped_ref = reference.replace(/"/g, """)
|
||||
|
@ -150,6 +284,47 @@ export class KhojChatView extends KhojPaneView {
|
|||
return referenceButton;
|
||||
}
|
||||
|
||||
formatHTMLMessage(message: string, raw=false, willReplace=true) {
|
||||
let rendered_msg = message;
|
||||
|
||||
// Replace LaTeX delimiters with placeholders
|
||||
rendered_msg = rendered_msg.replace(/\\\(/g, 'LEFTPAREN').replace(/\\\)/g, 'RIGHTPAREN')
|
||||
.replace(/\\\[/g, 'LEFTBRACKET').replace(/\\\]/g, 'RIGHTBRACKET');
|
||||
|
||||
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for the AI chat model.
|
||||
rendered_msg = rendered_msg.replace(/<s>\[INST\].+(<\/s>)?/g, '');
|
||||
|
||||
// Render markdow to HTML DOM element
|
||||
let chat_message_body_text_el = this.contentEl.createDiv();
|
||||
chat_message_body_text_el.className = "chat-message-text-response";
|
||||
MarkdownRenderer.renderMarkdown(message, chat_message_body_text_el, '', null);
|
||||
|
||||
// Replace placeholders with LaTeX delimiters
|
||||
rendered_msg = chat_message_body_text_el.innerHTML;
|
||||
chat_message_body_text_el.innerHTML = rendered_msg.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
|
||||
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
|
||||
|
||||
// Add a copy button to each chat message, if it doesn't already exist
|
||||
if (willReplace === true) {
|
||||
let copyButton = this.contentEl.createEl('button');
|
||||
copyButton.classList.add("copy-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.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);
|
||||
}
|
||||
|
||||
return chat_message_body_text_el;
|
||||
}
|
||||
|
||||
renderMessageWithReferences(chatEl: Element, message: string, sender: string, context?: string[], dt?: Date, intentType?: string, inferredQueries?: string) {
|
||||
if (!message) {
|
||||
return;
|
||||
|
@ -283,7 +458,7 @@ export class KhojChatView extends KhojPaneView {
|
|||
// Scroll to bottom after inserting chat messages
|
||||
this.scrollChatToBottom();
|
||||
|
||||
return chat_message_el
|
||||
return chat_message_el;
|
||||
}
|
||||
|
||||
async renderIncrementalMessage(htmlElement: HTMLDivElement, additionalMessage: string) {
|
||||
|
@ -302,9 +477,13 @@ export class KhojChatView extends KhojPaneView {
|
|||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
async getChatHistory(chatBodyEl: Element): Promise<boolean> {
|
||||
async getChatHistory(chatBodyEl: HTMLElement): Promise<boolean> {
|
||||
// Get chat history from Khoj backend
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat/history?client=obsidian`;
|
||||
if (chatBodyEl.dataset.conversationId) {
|
||||
chatUrl += `&conversation_id=${chatBodyEl.dataset.conversationId}`;
|
||||
this.setupWebSocket();
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await fetch(chatUrl, {
|
||||
|
@ -313,6 +492,7 @@ export class KhojChatView extends KhojPaneView {
|
|||
});
|
||||
|
||||
let responseJson: any = await response.json();
|
||||
chatBodyEl.dataset.conversationId = responseJson.conversation_id;
|
||||
|
||||
if (responseJson.detail) {
|
||||
// If the server returns error details in response, render a setup hint.
|
||||
|
@ -321,6 +501,12 @@ export class KhojChatView extends KhojPaneView {
|
|||
|
||||
return false;
|
||||
} else if (responseJson.response) {
|
||||
// Render conversation history, if any
|
||||
chatBodyEl.dataset.conversationId = responseJson.response.conversation_id;
|
||||
this.setupWebSocket();
|
||||
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(
|
||||
|
@ -409,17 +595,30 @@ export class KhojChatView extends KhojPaneView {
|
|||
if (!query || query === "") return;
|
||||
|
||||
// Render user query as chat message
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
let chatBodyEl = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
this.renderMessage(chatBodyEl, query, "you");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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®ion=${this.region}&city=${this.city}&country=${this.countryName}&timezone=${this.timezone}`;
|
||||
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&n=${this.setting.resultsCount}&client=obsidian&stream=true®ion=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`;
|
||||
let responseElement = this.createKhojResponseDiv();
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
this.result = "";
|
||||
await this.renderIncrementalMessage(responseElement, "🤔");
|
||||
let loadingEllipsis = this.createLoadingEllipse();
|
||||
responseElement.appendChild(loadingEllipsis);
|
||||
|
||||
let response = await fetch(chatUrl, {
|
||||
method: "GET",
|
||||
|
@ -434,9 +633,9 @@ export class KhojChatView extends KhojPaneView {
|
|||
throw new Error("Response body is null");
|
||||
}
|
||||
|
||||
// Clear thinking status message
|
||||
if (responseElement.innerHTML === "🤔") {
|
||||
responseElement.innerHTML = "";
|
||||
// Clear loading status message
|
||||
if (responseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis) {
|
||||
responseElement.removeChild(loadingEllipsis);
|
||||
}
|
||||
|
||||
// Reset collated chat result to empty string
|
||||
|
@ -492,7 +691,7 @@ export class KhojChatView extends KhojPaneView {
|
|||
}
|
||||
|
||||
async clearConversationHistory() {
|
||||
let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
|
||||
let response = await request({
|
||||
url: `${this.setting.khojUrl}/api/chat/history?client=obsidian`,
|
||||
|
@ -659,4 +858,312 @@ export class KhojChatView extends KhojPaneView {
|
|||
const chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
if (!!chat_body_el) chat_body_el.scrollTop = chat_body_el.scrollHeight;
|
||||
}
|
||||
|
||||
createLoadingEllipse() {
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
let loadingEllipsis = this.contentEl.createEl("div");
|
||||
loadingEllipsis.classList.add("lds-ellipsis");
|
||||
|
||||
let firstEllipsis = this.contentEl.createEl("div");
|
||||
firstEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let secondEllipsis = this.contentEl.createEl("div");
|
||||
secondEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let thirdEllipsis = this.contentEl.createEl("div");
|
||||
thirdEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
let fourthEllipsis = this.contentEl.createEl("div");
|
||||
fourthEllipsis.classList.add("lds-ellipsis-item");
|
||||
|
||||
loadingEllipsis.appendChild(firstEllipsis);
|
||||
loadingEllipsis.appendChild(secondEllipsis);
|
||||
loadingEllipsis.appendChild(thirdEllipsis);
|
||||
loadingEllipsis.appendChild(fourthEllipsis);
|
||||
|
||||
return loadingEllipsis;
|
||||
}
|
||||
|
||||
handleStreamResponse(newResponseElement: HTMLElement | null, rawResponse: string, loadingEllipsis: HTMLElement | null, replace=true) {
|
||||
if (!newResponseElement) return;
|
||||
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis) {
|
||||
newResponseElement.removeChild(loadingEllipsis);
|
||||
}
|
||||
if (replace) {
|
||||
newResponseElement.innerHTML = "";
|
||||
}
|
||||
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse, false, replace));
|
||||
this.scrollChatToBottom();
|
||||
}
|
||||
|
||||
handleCompiledReferences(rawResponseElement: HTMLElement | null, chunk: string, references: any, rawResponse: string) {
|
||||
if (!rawResponseElement || !chunk) return { rawResponse, references };
|
||||
const additionalResponse = chunk.split("### compiled references:")[0];
|
||||
rawResponse += additionalResponse;
|
||||
rawResponseElement.innerHTML = "";
|
||||
rawResponseElement.appendChild(this.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;
|
||||
}
|
||||
return { rawResponse, references };
|
||||
}
|
||||
|
||||
handleImageResponse(imageJson: any, rawResponse: string) {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
let references: any = {};
|
||||
if (imageJson.context && imageJson.context.length > 0) {
|
||||
const rawReferenceAsJson = imageJson.context;
|
||||
if (rawReferenceAsJson instanceof Array) {
|
||||
references["notes"] = rawReferenceAsJson;
|
||||
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
||||
references["online"] = rawReferenceAsJson;
|
||||
}
|
||||
}
|
||||
if (imageJson.detail) {
|
||||
// If response has detail field, response is an error message.
|
||||
rawResponse += imageJson.detail;
|
||||
}
|
||||
return { rawResponse, references };
|
||||
}
|
||||
|
||||
addMessageToChatBody(rawResponse: string, newResponseElement: HTMLElement | null, references: any) {
|
||||
if (!newResponseElement) return;
|
||||
newResponseElement.innerHTML = "";
|
||||
newResponseElement.appendChild(this.formatHTMLMessage(rawResponse));
|
||||
|
||||
this.finalizeChatBodyResponse(references, newResponseElement);
|
||||
}
|
||||
|
||||
finalizeChatBodyResponse(references: object, newResponseElement: HTMLElement | null) {
|
||||
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
|
||||
newResponseElement.appendChild(this.createReferenceSection(references));
|
||||
}
|
||||
this.scrollChatToBottom();
|
||||
let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
if (chatInput) chatInput.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
createReferenceSection(references: any) {
|
||||
let referenceSection = this.contentEl.createEl('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: any, index: number) => {
|
||||
let polishedReference = this.generateReference(referenceSection, reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
});
|
||||
}
|
||||
if (references.hasOwnProperty("online")) {
|
||||
numReferences += this.processOnlineReferences(referenceSection, references["online"]);
|
||||
}
|
||||
|
||||
let referenceExpandButton = this.contentEl.createEl('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 = this.contentEl.createEl('div');
|
||||
referencesDiv.classList.add("references");
|
||||
referencesDiv.appendChild(referenceExpandButton);
|
||||
referencesDiv.appendChild(referenceSection);
|
||||
|
||||
return referencesDiv;
|
||||
}
|
||||
|
||||
setupWebSocket() {
|
||||
let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
let wsProtocol = this.setting.khojUrl.startsWith('https:') ? 'wss:' : 'ws:';
|
||||
let baseUrl = this.setting.khojUrl.replace(/^https?:\/\//, '');
|
||||
let webSocketUrl = `${wsProtocol}//${baseUrl}/api/chat/ws`;
|
||||
|
||||
if (this.waitingForLocation) {
|
||||
console.debug("Waiting for location data to be fetched. Will setup WebSocket once location data is available.");
|
||||
return;
|
||||
}
|
||||
if (!chatBody) return;
|
||||
|
||||
this.websocketState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
}
|
||||
|
||||
if (chatBody.dataset.conversationId) {
|
||||
webSocketUrl += `?conversation_id=${chatBody.dataset.conversationId}`;
|
||||
webSocketUrl += !!this.location ? `®ion=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}` : '';
|
||||
|
||||
this.websocket = new WebSocket(webSocketUrl);
|
||||
this.websocket.onmessage = (event) => {
|
||||
// Get the last element in the chat-body
|
||||
let chunk = event.data;
|
||||
if (chunk == "start_llm_response") {
|
||||
console.log("Started streaming", new Date());
|
||||
} else if(chunk == "end_llm_response") {
|
||||
console.log("Stopped streaming", new Date());
|
||||
// Append any references after all the data has been streamed
|
||||
this.finalizeChatBodyResponse(this.websocketState.references, this.websocketState.newResponseTextEl);
|
||||
|
||||
// Reset variables
|
||||
this.websocketState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (chunk.includes("application/json")) {
|
||||
chunk = JSON.parse(chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
// If the chunk is not a JSON object, continue.
|
||||
}
|
||||
|
||||
const contentType = chunk["content-type"]
|
||||
if (contentType === "application/json") {
|
||||
// Handle JSON response
|
||||
try {
|
||||
if (chunk.image || chunk.detail) {
|
||||
const { rawResponse, references } = this.handleImageResponse(chunk, this.websocketState.rawResponse);
|
||||
this.websocketState.rawResponse = rawResponse;
|
||||
this.websocketState.references = references;
|
||||
} else if (chunk.type == "status") {
|
||||
this.handleStreamResponse(this.websocketState.newResponseTextEl, chunk.message, null, false);
|
||||
} else if (chunk.type == "rate_limit") {
|
||||
this.handleStreamResponse(this.websocketState.newResponseTextEl, chunk.message, this.websocketState.loadingEllipsis, true);
|
||||
} else {
|
||||
this.websocketState.rawResponse = chunk.response;
|
||||
}
|
||||
} catch (error) {
|
||||
// If the chunk is not a JSON object, just display it as is
|
||||
this.websocketState.rawResponse += chunk;
|
||||
} finally {
|
||||
if (chunk.type != "status" && chunk.type != "rate_limit") {
|
||||
this.addMessageToChatBody(this.websocketState.rawResponse, this.websocketState.newResponseTextEl, this.websocketState.references);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle streamed response of type text/event-stream or text/plain
|
||||
if (chunk && chunk.includes("### compiled references:")) {
|
||||
const { rawResponse, references } = this.handleCompiledReferences(this.websocketState.newResponseTextEl, chunk, this.websocketState.references, this.websocketState.rawResponse);
|
||||
this.websocketState.rawResponse = rawResponse;
|
||||
this.websocketState.references = references;
|
||||
} else {
|
||||
// If the chunk is not a JSON object, just display it as is
|
||||
this.websocketState.rawResponse += chunk;
|
||||
if (this.websocketState.newResponseTextEl) {
|
||||
this.handleStreamResponse(this.websocketState.newResponseTextEl, this.websocketState.rawResponse, this.websocketState.loadingEllipsis);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to bottom of chat window as chat response is streamed
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!this.websocket) return;
|
||||
this.websocket.onclose = (event: Event) => {
|
||||
console.log("WebSocket is closed now.");
|
||||
let statusDotIcon = document.getElementById("connection-status-icon");
|
||||
let statusDotText = document.getElementById("connection-status-text");
|
||||
if (!statusDotIcon || !statusDotText) return;
|
||||
statusDotIcon.style.backgroundColor = "red";
|
||||
statusDotText.style.marginTop = "5px";
|
||||
statusDotText.innerHTML = '<button onclick="setupWebSocket()">Reconnect to Server</button>';
|
||||
}
|
||||
this.websocket.onerror = (event: Event) => {
|
||||
console.log("WebSocket error observed:", event);
|
||||
}
|
||||
this.websocket.onopen = (event: Event) => {
|
||||
console.log("WebSocket is open now.")
|
||||
let statusDotIcon = document.getElementById("connection-status-icon");
|
||||
let statusDotText = document.getElementById("connection-status-text");
|
||||
if (!statusDotIcon || !statusDotText) return;
|
||||
statusDotIcon.style.backgroundColor = "green";
|
||||
statusDotText.style.marginTop = "10px";
|
||||
statusDotText.textContent = "Connected to Server";
|
||||
}
|
||||
}
|
||||
|
||||
sendMessageViaWebSocket() {
|
||||
let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0] as HTMLElement;
|
||||
let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0] as HTMLTextAreaElement;
|
||||
let query = chatInput?.value.trim();
|
||||
if (!chatInput || !chatBody || !query) return;
|
||||
console.log(`Query: ${query}`);
|
||||
|
||||
// Add message by user to chat body
|
||||
this.renderMessage(chatBody, query, "you");
|
||||
chatInput.value = "";
|
||||
this.autoResize();
|
||||
chatInput.setAttribute("disabled", "disabled");
|
||||
|
||||
let newResponseEl = this.contentEl.createDiv();
|
||||
newResponseEl.classList.add("khoj-chat-message", "khoj");
|
||||
newResponseEl.setAttribute("data-meta", "🏮 Khoj at " + this.formatDate(new Date()));
|
||||
chatBody.appendChild(newResponseEl);
|
||||
|
||||
let newResponseTextEl = this.contentEl.createDiv();
|
||||
newResponseTextEl.classList.add("khoj-chat-message-text", "khoj");
|
||||
newResponseEl.appendChild(newResponseTextEl);
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
let loadingEllipsis = this.createLoadingEllipse();
|
||||
|
||||
newResponseTextEl.appendChild(loadingEllipsis);
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
|
||||
// let chatTooltip = document.getElementById("chat-tooltip");
|
||||
// if (chatTooltip) chatTooltip.style.display = "none";
|
||||
|
||||
chatInput.classList.remove("option-enabled");
|
||||
|
||||
// Call specified Khoj API
|
||||
this.websocket.send(query);
|
||||
|
||||
this.websocketState = {
|
||||
newResponseTextEl,
|
||||
newResponseEl,
|
||||
loadingEllipsis,
|
||||
references: [],
|
||||
rawResponse: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,7 @@ import { KhojSearchModal } from 'src/search_modal';
|
|||
import { KhojView, populateHeaderPane } from './utils';
|
||||
|
||||
export abstract class KhojPaneView extends ItemView {
|
||||
result: string;
|
||||
setting: KhojSetting;
|
||||
region: string;
|
||||
city: string;
|
||||
countryName: string;
|
||||
timezone: string;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, setting: KhojSetting) {
|
||||
super(leaf);
|
||||
|
@ -18,19 +13,6 @@ export abstract class KhojPaneView extends ItemView {
|
|||
|
||||
// Register Modal Keybindings to send user message
|
||||
// this.scope.register([], 'Enter', async () => { await this.chat() });
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.region = data.region;
|
||||
this.city = data.city;
|
||||
this.countryName = data.country_name;
|
||||
this.timezone = data.timezone;
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
|
|
|
@ -454,6 +454,63 @@ img.copy-icon {
|
|||
background: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.lds-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
}
|
||||
.lds-ellipsis div {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--main-text-color);
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
div.khoj-header {
|
||||
display: grid;
|
||||
|
|
|
@ -2831,7 +2831,5 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</html>
|
||||
|
|
Loading…
Add table
Reference in a new issue