mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-23 23:48:56 +01:00
Simplify streaming chat function in web client
This commit is contained in:
parent
6b9550238f
commit
2d4b284218
1 changed files with 200 additions and 324 deletions
|
@ -598,11 +598,9 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chat(isVoice=false) {
|
async function chat(isVoice=false) {
|
||||||
renderMessageStream(isVoice);
|
let chatBody = document.getElementById("chat-body");
|
||||||
return;
|
|
||||||
|
|
||||||
let query = document.getElementById("chat-input").value.trim();
|
var query = document.getElementById("chat-input").value.trim();
|
||||||
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
|
||||||
console.log(`Query: ${query}`);
|
console.log(`Query: ${query}`);
|
||||||
|
|
||||||
// Short circuit on empty query
|
// Short circuit on empty query
|
||||||
|
@ -621,31 +619,20 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
document.getElementById("chat-input").value = "";
|
document.getElementById("chat-input").value = "";
|
||||||
autoResize();
|
autoResize();
|
||||||
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
||||||
let chat_body = document.getElementById("chat-body");
|
|
||||||
|
|
||||||
let conversationID = chat_body.dataset.conversationId;
|
let newResponseEl = document.createElement("div");
|
||||||
|
newResponseEl.classList.add("chat-message", "khoj");
|
||||||
|
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||||
|
chatBody.appendChild(newResponseEl);
|
||||||
|
|
||||||
if (!conversationID) {
|
let newResponseTextEl = document.createElement("div");
|
||||||
let response = await fetch('/api/chat/sessions', { method: "POST" });
|
newResponseTextEl.classList.add("chat-message-text", "khoj");
|
||||||
let data = await response.json();
|
newResponseEl.appendChild(newResponseTextEl);
|
||||||
conversationID = data.conversation_id;
|
|
||||||
chat_body.dataset.conversationId = conversationID;
|
|
||||||
refreshChatSessionsPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_response = document.createElement("div");
|
|
||||||
new_response.classList.add("chat-message", "khoj");
|
|
||||||
new_response.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
|
||||||
chat_body.appendChild(new_response);
|
|
||||||
|
|
||||||
let newResponseText = document.createElement("div");
|
|
||||||
newResponseText.classList.add("chat-message-text", "khoj");
|
|
||||||
new_response.appendChild(newResponseText);
|
|
||||||
|
|
||||||
// Temporary status message to indicate that Khoj is thinking
|
// Temporary status message to indicate that Khoj is thinking
|
||||||
let loadingEllipsis = createLoadingEllipse();
|
let loadingEllipsis = createLoadingEllipse();
|
||||||
|
|
||||||
newResponseText.appendChild(loadingEllipsis);
|
newResponseTextEl.appendChild(loadingEllipsis);
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
|
||||||
let chatTooltip = document.getElementById("chat-tooltip");
|
let chatTooltip = document.getElementById("chat-tooltip");
|
||||||
|
@ -654,65 +641,21 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
let chatInput = document.getElementById("chat-input");
|
let chatInput = document.getElementById("chat-input");
|
||||||
chatInput.classList.remove("option-enabled");
|
chatInput.classList.remove("option-enabled");
|
||||||
|
|
||||||
// Generate backend API URL to execute query
|
|
||||||
let url = `/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
|
|
||||||
|
|
||||||
// Call specified Khoj API
|
// Call specified Khoj API
|
||||||
let response = await fetch(url);
|
await sendMessageStream(query);
|
||||||
let rawResponse = "";
|
let rawResponse = "";
|
||||||
let references = null;
|
let references = {};
|
||||||
const contentType = response.headers.get("content-type");
|
|
||||||
|
|
||||||
if (contentType === "application/json") {
|
chatMessageState = {
|
||||||
// Handle JSON response
|
newResponseTextEl,
|
||||||
try {
|
newResponseEl,
|
||||||
const responseAsJson = await response.json();
|
loadingEllipsis,
|
||||||
if (responseAsJson.image || responseAsJson.detail) {
|
references,
|
||||||
({rawResponse, references } = handleImageResponse(responseAsJson, rawResponse));
|
rawResponse,
|
||||||
} else {
|
rawQuery: query,
|
||||||
rawResponse = responseAsJson.response;
|
isVoice: isVoice,
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If the chunk is not a JSON object, just display it as is
|
|
||||||
rawResponse += chunk;
|
|
||||||
} finally {
|
|
||||||
addMessageToChatBody(rawResponse, newResponseText, references);
|
|
||||||
}
|
|
||||||
} 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
|
|
||||||
finalizeChatBodyResponse(references, newResponseText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode message chunk from stream
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
if (chunk.includes("### compiled references:")) {
|
|
||||||
({ rawResponse, references } = handleCompiledReferences(newResponseText, chunk, references, rawResponse));
|
|
||||||
readStream();
|
|
||||||
} else {
|
|
||||||
// If the chunk is not a JSON object, just display it as is
|
|
||||||
rawResponse += chunk;
|
|
||||||
handleStreamResponse(newResponseText, rawResponse, query, loadingEllipsis);
|
|
||||||
readStream();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Scroll to bottom of chat window as chat response is streamed
|
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function createLoadingEllipse() {
|
function createLoadingEllipse() {
|
||||||
// Temporary status message to indicate that Khoj is thinking
|
// Temporary status message to indicate that Khoj is thinking
|
||||||
|
@ -750,22 +693,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCompiledReferences(rawResponseElement, chunk, references, rawResponse) {
|
|
||||||
const additionalResponse = chunk.split("### compiled references:")[0];
|
|
||||||
rawResponse += additionalResponse;
|
|
||||||
rawResponseElement.innerHTML = "";
|
|
||||||
rawResponseElement.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;
|
|
||||||
}
|
|
||||||
return { rawResponse, references };
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleImageResponse(imageJson, rawResponse) {
|
function handleImageResponse(imageJson, rawResponse) {
|
||||||
if (imageJson.image) {
|
if (imageJson.image) {
|
||||||
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
|
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
|
||||||
|
@ -806,11 +733,188 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeChatBodyResponse(references, newResponseElement) {
|
function finalizeChatBodyResponse(references, newResponseElement) {
|
||||||
if (references != null && Object.keys(references).length > 0) {
|
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
|
||||||
newResponseElement.appendChild(createReferenceSection(references));
|
newResponseElement.appendChild(createReferenceSection(references));
|
||||||
}
|
}
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
document.getElementById("chat-input").removeAttribute("disabled");
|
document.getElementById("chat-input")?.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectJsonsInBufferedMessageChunk(chunk) {
|
||||||
|
// Collect list of JSON objects and raw strings in the chunk
|
||||||
|
// Return the list of objects and the remaining raw string
|
||||||
|
console.log("Raw Chunk:", chunk);
|
||||||
|
let startIndex = chunk.indexOf('{');
|
||||||
|
if (startIndex === -1) return { objects: [chunk], remainder: '' };
|
||||||
|
const objects = [chunk.slice(0, startIndex)];
|
||||||
|
let openBraces = 0;
|
||||||
|
let currentObject = '';
|
||||||
|
|
||||||
|
for (let i = startIndex; i < chunk.length; i++) {
|
||||||
|
if (chunk[i] === '{') {
|
||||||
|
if (openBraces === 0) startIndex = i;
|
||||||
|
openBraces++;
|
||||||
|
}
|
||||||
|
if (chunk[i] === '}') {
|
||||||
|
openBraces--;
|
||||||
|
if (openBraces === 0) {
|
||||||
|
currentObject = chunk.slice(startIndex, i + 1);
|
||||||
|
objects.push(currentObject);
|
||||||
|
currentObject = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
objects: objects,
|
||||||
|
remainder: openBraces > 0 ? chunk.slice(startIndex) : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, null, 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") {
|
||||||
|
const rawReferenceAsJson = JSON.parse(chunk.data);
|
||||||
|
chatMessageState.references = {"notes": rawReferenceAsJson.context, "online": rawReferenceAsJson.online_results};
|
||||||
|
} else if (chunk.type === 'message') {
|
||||||
|
const chunkData = chunk.data;
|
||||||
|
if (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) {
|
||||||
|
let { rawResponse, references } = handleImageResponse(jsonData, chatMessageState.rawResponse);
|
||||||
|
chatMessageState.rawResponse = rawResponse;
|
||||||
|
chatMessageState.references = references;
|
||||||
|
} else if (jsonData.response) {
|
||||||
|
chatMessageState.rawResponse = jsonData.response;
|
||||||
|
chatMessageState.references = {
|
||||||
|
notes: jsonData.context || {},
|
||||||
|
online: jsonData.online_results || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
addMessageToChatBody(chatMessageState.rawResponse, chatMessageState.newResponseTextEl, chatMessageState.references);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessageStream(query) {
|
||||||
|
let chatBody = document.getElementById("chat-body");
|
||||||
|
let conversationId = chatBody.dataset.conversationId;
|
||||||
|
|
||||||
|
if (!conversationId) {
|
||||||
|
let response = await fetch('/api/chat/sessions', { method: "POST" });
|
||||||
|
let data = await response.json();
|
||||||
|
conversationId = data.conversation_id;
|
||||||
|
chatBody.dataset.conversationId = conversationId;
|
||||||
|
refreshChatSessionsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
let chatStreamUrl = `/api/chat/stream?q=${encodeURIComponent(query)}&conversation_id=${conversationId}&client=web`;
|
||||||
|
chatStreamUrl += (!!region && !!city && !!countryName && !!timezone)
|
||||||
|
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
fetch(chatStreamUrl)
|
||||||
|
.then(response => {
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
let netBracketCount = 0;
|
||||||
|
|
||||||
|
function readStream() {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
// If the stream is done
|
||||||
|
if (done) {
|
||||||
|
// Process the last chunk
|
||||||
|
processMessageChunk(buffer);
|
||||||
|
buffer = '';
|
||||||
|
console.log("Stream complete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read chunk from stream and append it to the buffer
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
// Check if the buffer contains (0 or more) complete JSON objects
|
||||||
|
netBracketCount += (chunk.match(/{/g) || []).length - (chunk.match(/}/g) || []).length;
|
||||||
|
if (netBracketCount === 0) {
|
||||||
|
let chunks = collectJsonsInBufferedMessageChunk(buffer);
|
||||||
|
chunks.objects.forEach(processMessageChunk);
|
||||||
|
buffer = chunks.remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue reading the stream
|
||||||
|
readStream();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readStream();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis) {
|
||||||
|
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
|
||||||
|
}
|
||||||
|
chatMessageState.newResponseTextEl.textContent += "Failed to get response! Try again or contact developers at team@khoj.dev"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementalChat(event) {
|
function incrementalChat(event) {
|
||||||
|
@ -1083,234 +1187,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMessageStream(query) {
|
|
||||||
let chatBody = document.getElementById("chat-body");
|
|
||||||
let chatStreamUrl = `/api/chat/stream?q=${query}`;
|
|
||||||
|
|
||||||
if (chatBody.dataset.conversationId) {
|
|
||||||
chatStreamUrl += `&conversation_id=${chatBody.dataset.conversationId}`;
|
|
||||||
chatStreamUrl += (!!region && !!city && !!countryName && !!timezone)
|
|
||||||
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
fetch(chatStreamUrl)
|
|
||||||
.then(response => {
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
let netBracketCount = 0;
|
|
||||||
|
|
||||||
function readStream() {
|
|
||||||
reader.read().then(({ done, value }) => {
|
|
||||||
if (done) {
|
|
||||||
console.log("Stream complete");
|
|
||||||
handleChunk(buffer);
|
|
||||||
buffer = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
buffer += chunk;
|
|
||||||
|
|
||||||
netBracketCount += (chunk.match(/{/g) || []).length - (chunk.match(/}/g) || []).length;
|
|
||||||
if (netBracketCount === 0) {
|
|
||||||
chunks = processJsonObjects(buffer);
|
|
||||||
chunks.objects.forEach(obj => handleChunk(obj));
|
|
||||||
buffer = chunks.remainder;
|
|
||||||
}
|
|
||||||
readStream();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readStream();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis) {
|
|
||||||
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
|
|
||||||
}
|
|
||||||
chatMessageState.newResponseTextEl.textContent += "Failed to get response! Try again or contact developers at team@khoj.dev"
|
|
||||||
});
|
|
||||||
|
|
||||||
function processJsonObjects(str) {
|
|
||||||
let startIndex = str.indexOf('{');
|
|
||||||
if (startIndex === -1) return { objects: [str], remainder: '' };
|
|
||||||
const objects = [str.slice(0, startIndex)];
|
|
||||||
let openBraces = 0;
|
|
||||||
let currentObject = '';
|
|
||||||
|
|
||||||
for (let i = startIndex; i < str.length; i++) {
|
|
||||||
if (str[i] === '{') {
|
|
||||||
if (openBraces === 0) startIndex = i;
|
|
||||||
openBraces++;
|
|
||||||
}
|
|
||||||
if (str[i] === '}') {
|
|
||||||
openBraces--;
|
|
||||||
if (openBraces === 0) {
|
|
||||||
currentObject = str.slice(startIndex, i + 1);
|
|
||||||
objects.push(currentObject);
|
|
||||||
currentObject = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
objects: objects,
|
|
||||||
remainder: openBraces > 0 ? str.slice(startIndex) : ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChunk(rawChunk) {
|
|
||||||
// Split the chunk into lines
|
|
||||||
console.log("Chunk:", rawChunk);
|
|
||||||
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
|
|
||||||
try {
|
|
||||||
let jsonChunk = JSON.parse(rawChunk);
|
|
||||||
if (!jsonChunk.type)
|
|
||||||
jsonChunk = {type: 'message', data: jsonChunk};
|
|
||||||
processChunk(jsonChunk);
|
|
||||||
} catch (e) {
|
|
||||||
const jsonChunk = {type: 'message', data: rawChunk};
|
|
||||||
processChunk(jsonChunk);
|
|
||||||
}
|
|
||||||
} else if (rawChunk.length > 0) {
|
|
||||||
const jsonChunk = {type: 'message', data: rawChunk};
|
|
||||||
processChunk(jsonChunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function processChunk(chunk) {
|
|
||||||
console.log(chunk);
|
|
||||||
if (chunk.type ==='status') {
|
|
||||||
console.log(`status: ${chunk.data}`);
|
|
||||||
const statusMessage = chunk.data;
|
|
||||||
handleStreamResponse(chatMessageState.newResponseTextEl, statusMessage, chatMessageState.rawQuery, null, 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,
|
|
||||||
}
|
|
||||||
} else if (chunk.type === "references") {
|
|
||||||
const rawReferenceAsJson = JSON.parse(chunk.data);
|
|
||||||
console.log(`${chunk.type}: ${rawReferenceAsJson}`);
|
|
||||||
chatMessageState.references = {"notes": rawReferenceAsJson.context, "online": rawReferenceAsJson.online_results};
|
|
||||||
} else if (chunk.type === 'message') {
|
|
||||||
if (chunk.data.trim()?.startsWith("{") && chunk.data.trim()?.endsWith("}")) {
|
|
||||||
// Try process chunk data as if it is a JSON object
|
|
||||||
try {
|
|
||||||
const jsonData = JSON.parse(chunk.data.trim());
|
|
||||||
handleJsonResponse(jsonData);
|
|
||||||
} catch (e) {
|
|
||||||
// Handle text response chunk with compiled references
|
|
||||||
if (chunk?.data.includes("### compiled references:")) {
|
|
||||||
chatMessageState.rawResponse += chunk.data.split("### compiled references:")[0];
|
|
||||||
// Handle text response chunk
|
|
||||||
} else {
|
|
||||||
chatMessageState.rawResponse += chunk.data;
|
|
||||||
}
|
|
||||||
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle text response chunk with compiled references
|
|
||||||
if (chunk?.data.includes("### compiled references:")) {
|
|
||||||
chatMessageState.rawResponse += chunk.data.split("### compiled references:")[0];
|
|
||||||
// Handle text response chunk
|
|
||||||
} else {
|
|
||||||
chatMessageState.rawResponse += chunk.data;
|
|
||||||
}
|
|
||||||
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleJsonResponse(jsonData) {
|
|
||||||
if (jsonData.image || jsonData.detail) {
|
|
||||||
let { rawResponse, references } = handleImageResponse(jsonData, chatMessageState.rawResponse);
|
|
||||||
chatMessageState.rawResponse = rawResponse;
|
|
||||||
chatMessageState.references = references;
|
|
||||||
} else if (jsonData.response) {
|
|
||||||
chatMessageState.rawResponse = jsonData.response;
|
|
||||||
chatMessageState.references = {
|
|
||||||
notes: jsonData.context || {},
|
|
||||||
online: jsonData.online_results || {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
addMessageToChatBody(chatMessageState.rawResponse, chatMessageState.newResponseTextEl, chatMessageState.references);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMessageStream(isVoice=false) {
|
|
||||||
let chatBody = document.getElementById("chat-body");
|
|
||||||
|
|
||||||
var query = document.getElementById("chat-input").value.trim();
|
|
||||||
console.log(`Query: ${query}`);
|
|
||||||
|
|
||||||
if (userMessages.length >= 10) {
|
|
||||||
userMessages.shift();
|
|
||||||
}
|
|
||||||
userMessages.push(query);
|
|
||||||
resetUserMessageIndex();
|
|
||||||
|
|
||||||
// Add message by user to chat body
|
|
||||||
renderMessage(query, "you");
|
|
||||||
document.getElementById("chat-input").value = "";
|
|
||||||
autoResize();
|
|
||||||
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
|
||||||
|
|
||||||
let newResponseEl = document.createElement("div");
|
|
||||||
newResponseEl.classList.add("chat-message", "khoj");
|
|
||||||
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
|
||||||
chatBody.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 = createLoadingEllipse();
|
|
||||||
|
|
||||||
newResponseTextEl.appendChild(loadingEllipsis);
|
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
||||||
|
|
||||||
let chatTooltip = document.getElementById("chat-tooltip");
|
|
||||||
chatTooltip.style.display = "none";
|
|
||||||
|
|
||||||
let chatInput = document.getElementById("chat-input");
|
|
||||||
chatInput.classList.remove("option-enabled");
|
|
||||||
|
|
||||||
// Call specified Khoj API
|
|
||||||
sendMessageStream(query);
|
|
||||||
let rawResponse = "";
|
|
||||||
let references = {};
|
|
||||||
|
|
||||||
chatMessageState = {
|
|
||||||
newResponseTextEl,
|
|
||||||
newResponseEl,
|
|
||||||
loadingEllipsis,
|
|
||||||
references,
|
|
||||||
rawResponse,
|
|
||||||
rawQuery: query,
|
|
||||||
isVoice: isVoice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var userMessages = [];
|
var userMessages = [];
|
||||||
var userMessageIndex = -1;
|
var userMessageIndex = -1;
|
||||||
function loadChat() {
|
function loadChat() {
|
||||||
|
|
Loading…
Reference in a new issue