khoj/src/interface/web/app/common/chatFunctions.ts
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

301 lines
9.6 KiB
TypeScript

import { Context, OnlineContextData } from "../components/chatMessage/chatMessage";
interface ResponseWithReferences {
context?: Context[];
online?: {
[key: string]: OnlineContextData
}
response?: string;
}
export function handleCompiledReferences(chunk: string, currentResponse: string) {
const rawReference = chunk.split("### compiled references:")[1];
const rawResponse = chunk.split("### compiled references:")[0];
let references: ResponseWithReferences = {};
// Set the initial response
references.response = currentResponse + rawResponse;
const rawReferenceAsJson = JSON.parse(rawReference);
if (rawReferenceAsJson instanceof Array) {
references.context = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
references.online = rawReferenceAsJson;
}
return references;
}
async function sendChatStream(
message: string,
conversationId: string,
setIsLoading: (loading: boolean) => void,
setInitialResponse: (response: string) => void,
setInitialReferences: (references: ResponseWithReferences) => void) {
setIsLoading(true);
// Send a message to the chat server to verify the fact
const chatURL = "/api/chat";
const apiURL = `${chatURL}?q=${encodeURIComponent(message)}&client=web&stream=true&conversation_id=${conversationId}`;
try {
const response = await fetch(apiURL);
if (!response.body) throw new Error("No response body found");
const reader = response.body?.getReader();
let decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
let chunk = decoder.decode(value, { stream: true });
if (chunk.includes("### compiled references:")) {
const references = handleCompiledReferences(chunk, result);
if (references.response) {
result = references.response;
setInitialResponse(references.response);
setInitialReferences(references);
}
} else {
result += chunk;
setInitialResponse(result);
}
}
} catch (error) {
console.error("Error verifying statement: ", error);
} finally {
setIsLoading(false);
}
}
export const setupWebSocket = async (conversationId: string, initialMessage?: string) => {
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:42110';
let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`;
if (conversationId === null) {
return null;
}
if (conversationId) {
webSocketUrl += `?conversation_id=${conversationId}`;
}
const chatWS = new WebSocket(webSocketUrl);
chatWS.onopen = () => {
console.log('WebSocket connection established');
if (initialMessage) {
chatWS.send(initialMessage);
}
};
chatWS.onmessage = (event) => {
console.log(event.data);
};
chatWS.onerror = (error) => {
console.error('WebSocket error: ', error);
};
chatWS.onclose = () => {
console.log('WebSocket connection closed');
};
return chatWS;
};
export function handleImageResponse(imageJson: any) {
let 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}`;
}
}
let reference: ResponseWithReferences = {};
if (imageJson.context && imageJson.context.length > 0) {
const rawReferenceAsJson = imageJson.context;
if (rawReferenceAsJson instanceof Array) {
reference.context = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
reference.online = rawReferenceAsJson;
}
}
if (imageJson.detail) {
// The detail field contains the improved image prompt
rawResponse += imageJson.detail;
}
reference.response = rawResponse;
return reference;
}
export function modifyFileFilterForConversation(
conversationId: string | null,
filenames: string[],
setAddedFiles: (files: string[]) => void,
mode: 'add' | 'remove') {
if (!conversationId) {
console.error("No conversation ID provided");
return;
}
const method = mode === 'add' ? 'POST' : 'DELETE';
const body = {
conversation_id: conversationId,
filenames: filenames,
}
const addUrl = `/api/chat/conversation/file-filters/bulk`;
fetch(addUrl, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
.then(response => response.json())
.then(data => {
console.log("ADDEDFILES DATA: ", data);
setAddedFiles(data);
})
.catch(err => {
console.error(err);
return;
});
}
export function uploadDataForIndexing(
files: FileList,
setWarning: (warning: string) => void,
setUploading: (uploading: boolean) => void,
setError: (error: string) => void,
setUploadedFiles?: (files: string[]) => void,
conversationId?: string | null) {
const allowedExtensions = ['text/org', 'text/markdown', 'text/plain', 'text/html', 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
const allowedFileEndings = ['org', 'md', 'txt', 'html', 'pdf', 'docx'];
const badFiles: string[] = [];
const goodFiles: File[] = [];
const uploadedFiles: string[] = [];
for (let file of files) {
const fileEnding = file.name.split('.').pop();
if (!file || !file.name || !fileEnding) {
if (file) {
badFiles.push(file.name);
}
} else if ((!allowedExtensions.includes(file.type) && !allowedFileEndings.includes(fileEnding.toLowerCase()))) {
badFiles.push(file.name);
} else {
goodFiles.push(file);
}
}
if (goodFiles.length === 0) {
setWarning("No supported files found");
return;
}
if (badFiles.length > 0) {
setWarning("The following files are not supported yet:\n" + badFiles.join('\n'));
}
const formData = new FormData();
// Create an array of Promises for file reading
const fileReadPromises = Array.from(goodFiles).map(file => {
return new Promise<void>((resolve, reject) => {
let reader = new FileReader();
reader.onload = function (event) {
if (event.target === null) {
reject();
return;
}
let fileContents = event.target.result;
let fileType = file.type;
let fileName = file.name;
if (fileType === "") {
let fileExtension = fileName.split('.').pop();
if (fileExtension === "org") {
fileType = "text/org";
} else if (fileExtension === "md") {
fileType = "text/markdown";
} else if (fileExtension === "txt") {
fileType = "text/plain";
} else if (fileExtension === "html") {
fileType = "text/html";
} else if (fileExtension === "pdf") {
fileType = "application/pdf";
} else {
// Skip this file if its type is not supported
resolve();
return;
}
}
if (fileContents === null) {
reject();
return;
}
let fileObj = new Blob([fileContents], { type: fileType });
formData.append("files", fileObj, file.name);
resolve();
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
});
setUploading(true);
// Wait for all files to be read before making the fetch request
Promise.all(fileReadPromises)
.then(() => {
return fetch("/api/content?client=web", {
method: "PATCH",
body: formData,
});
})
.then((data) => {
for (let file of goodFiles) {
uploadedFiles.push(file.name);
if (conversationId && setUploadedFiles) {
modifyFileFilterForConversation(conversationId, [file.name], setUploadedFiles, 'add');
}
}
if (setUploadedFiles) setUploadedFiles(uploadedFiles);
})
.catch((error) => {
console.log(error);
setError(`Error uploading file: ${error}`);
})
.finally(() => {
setUploading(false);
});
}