aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ChatBox/ChatBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/ChatBox/ChatBox.tsx')
-rw-r--r--src/client/views/nodes/ChatBox/ChatBox.tsx764
1 files changed, 281 insertions, 483 deletions
diff --git a/src/client/views/nodes/ChatBox/ChatBox.tsx b/src/client/views/nodes/ChatBox/ChatBox.tsx
index 880c332ac..e3a164b3e 100644
--- a/src/client/views/nodes/ChatBox/ChatBox.tsx
+++ b/src/client/views/nodes/ChatBox/ChatBox.tsx
@@ -1,104 +1,95 @@
-import { MathJaxContext } from 'better-react-mathjax';
-import { action, makeObservable, observable, observe, reaction, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, observe, reaction, runInAction, ObservableSet } from 'mobx';
import { observer } from 'mobx-react';
import OpenAI, { ClientOptions } from 'openai';
-import { ImageFile, Message } from 'openai/resources/beta/threads/messages';
-import { RunStep } from 'openai/resources/beta/threads/runs/steps';
import * as React from 'react';
-import { Doc } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
+import { Doc, DocListCast } from '../../../../fields/Doc';
import { CsvCast, DocCast, PDFCast, StrCast } from '../../../../fields/Types';
-import { CsvField } from '../../../../fields/URLField';
import { Networking } from '../../../Network';
-import { DocUtils } from '../../../documents/DocUtils';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
-import { DocumentManager } from '../../../util/DocumentManager';
import { LinkManager } from '../../../util/LinkManager';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
import { FieldView, FieldViewProps } from '../FieldView';
import './ChatBox.scss';
-import MessageComponent from './MessageComponent';
-import { ANNOTATION_LINK_TYPE, ASSISTANT_ROLE, AssistantMessage, DOWNLOAD_TYPE } from './types';
+import MessageComponentBox from './MessageComponent';
+import { ASSISTANT_ROLE, AssistantMessage, AI_Document, Citation, CHUNK_TYPE, Chunk, getChunkType } from './types';
+import { Vectorstore } from './vectorstore/VectorstoreUpload';
+import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
+import { Agent } from './Agent';
+import dotenv from 'dotenv';
+import { DocData, DocViews } from '../../../../fields/DocSymbols';
+import { DocumentView } from '../DocumentView';
+import { AnswerParser } from './AnswerParser';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { UUID } from 'bson';
+import { v4 as uuidv4 } from 'uuid';
+import { aS } from '@fullcalendar/core/internal-common';
+import { computeRect } from '@fullcalendar/core/internal';
+import { DocUtils } from '../../../documents/DocUtils';
+
+dotenv.config();
@observer
export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
- @observable modalStatus = false;
- @observable currentFile = { url: '' };
@observable history: AssistantMessage[] = [];
@observable.deep current_message: AssistantMessage | undefined = undefined;
@observable isLoading: boolean = false;
- @observable isInitializing: boolean = true;
- @observable expandedLogIndex: number | null = null;
- @observable linked_docs_to_add: Doc[] = [];
-
+ @observable isUploadingDocs: boolean = false;
+ @observable expandedScratchpadIndex: number | null = null;
+ @observable inputValue: string = '';
+ @observable private linked_docs_to_add: ObservableSet<Doc> = observable.set();
private openai: OpenAI;
- private interim_history: string = '';
- private assistantID: string = '';
- private threadID: string = '';
+ private vectorstore_id: string;
+ private documents: AI_Document[] = [];
private _oldWheel: any;
- private vectorStoreID: string = '';
- private mathJaxConfig: any;
- private linkedCsvIDs: string[] = [];
+ private vectorstore: Vectorstore;
+ private agent: Agent; // Add the ChatBot instance
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ChatBox, fieldKey);
}
+
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
this.openai = this.initializeOpenAI();
- this.history = [];
- this.threadID = StrCast(this.dataDoc.thread_id);
- this.assistantID = StrCast(this.dataDoc.assistant_id);
- this.vectorStoreID = StrCast(this.dataDoc.vector_store_id);
- this.openai = this.initializeOpenAI();
- if (this.assistantID === '' || this.threadID === '' || this.vectorStoreID === '') {
- this.createAssistant();
+ if (StrCast(this.dataDoc.vectorstore_id) == '') {
+ console.log('new_id');
+ this.vectorstore_id = uuidv4();
+ this.dataDoc.vectorstore_id = this.vectorstore_id;
} else {
- this.retrieveCsvUrls();
- this.isInitializing = false;
+ this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id);
}
- this.mathJaxConfig = {
- loader: { load: ['input/asciimath'] },
- tex: {
- inlineMath: [
- ['$', '$'],
- ['\\(', '\\)'],
- ],
- displayMath: [
- ['$$', '$$'],
- ['[', ']'],
- ],
- },
- };
+ this.vectorstore = new Vectorstore(this.vectorstore_id);
+ this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory);
+
reaction(
- () => this.history.map((msg: AssistantMessage) => ({ role: msg.role, text: msg.text, image: msg.image, tool_logs: msg.tool_logs, links: msg.links })),
+ () => this.history.map((msg: AssistantMessage) => ({ role: msg.role, text_content: msg.text_content, follow_up_questions: msg.follow_up_questions, citations: msg.citations })),
serializableHistory => {
this.dataDoc.data = JSON.stringify(serializableHistory);
}
);
}
- toggleToolLogs = (index: number) => {
- this.expandedLogIndex = this.expandedLogIndex === index ? null : index;
+ @action
+ addDocToVectorstore = async (newLinkedDoc: Doc) => {
+ await this.vectorstore.addAIDoc(newLinkedDoc);
};
- retrieveCsvUrls() {
- const linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d);
+ // @action
+ // uploadNewDocument = async (newDoc: Doc) => {
+ // const local_file_path: string = CsvCast(newDoc.data, PDFCast(newDoc.data)).url.pathname;
+ // const { document_json } = await Networking.PostToServer('/createDocument', { file_path: local_file_path });
+ // this.documents.push(...document_json.map(convertToAIDocument));
+ // //newDoc['ai_document'] = document_json;
+ // };
- linkedDocs.forEach(doc => {
- const aiFieldId = StrCast(doc[this.Document[Id] + '_ai_field_id']);
- if (CsvCast(doc.data)) {
- this.linkedCsvIDs.push(StrCast(aiFieldId));
- console.log(this.linkedCsvIDs);
- }
- });
- }
+ @action
+ toggleToolLogs = (index: number) => {
+ this.expandedScratchpadIndex = this.expandedScratchpadIndex === index ? null : index;
+ };
initializeOpenAI() {
const configuration: ClientOptions = {
@@ -114,390 +105,159 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- createLink = (linkInfo: string, startIndex: number, endIndex: number, linkType: ANNOTATION_LINK_TYPE, annotationIndex: number = 0) => {
- const text = this.interim_history;
- const subString = this.current_message?.text.substring(startIndex, endIndex) ?? '';
- if (!text) return;
- const textToDisplay = `${annotationIndex}`;
- let fileInfo = linkInfo;
- const fileName = subString.split('/')[subString.split('/').length - 1];
- if (linkType === ANNOTATION_LINK_TYPE.DOWNLOAD_FILE) {
- fileInfo = linkInfo + '!!!' + fileName;
- }
-
- const formattedLink = `[${textToDisplay}](${fileInfo}~~~${linkType})`;
- console.log(formattedLink);
- const newText = text.replace(subString, formattedLink);
- runInAction(() => {
- this.interim_history = newText;
- console.log(newText);
- this.current_message?.links?.push({
- start: startIndex,
- end: endIndex,
- url: linkType === ANNOTATION_LINK_TYPE.DOWNLOAD_FILE ? fileName : linkInfo,
- id: linkType === ANNOTATION_LINK_TYPE.DOWNLOAD_FILE ? linkInfo : undefined,
- link_type: linkType,
- });
- });
- };
-
- @action
- createAssistant = async () => {
- this.isInitializing = true;
- try {
- const vectorStore = await this.openai.beta.vectorStores.create({
- name: 'Vector Store for Assistant',
- });
- const assistant = await this.openai.beta.assistants.create({
- name: 'Document Analyser Assistant',
- instructions: `
- You will analyse documents with which you are provided. You will answer questions and provide insights based on the information in the documents.
- For writing math formulas:
- You have a MathJax render environment.
- - Write all in-line equations within a single dollar sign, $, to render them as TeX (this means any time you want to use a dollar sign to represent a dollar sign itself, you must escape it with a backslash: "$");
- - Use a double dollar sign, $$, to render equations on a new line;
- Example: $$x^2 + 3x$$ is output for "x² + 3x" to appear as TeX.`,
- model: 'gpt-4-turbo',
- tools: [{ type: 'file_search' }, { type: 'code_interpreter' }],
- tool_resources: {
- file_search: {
- vector_store_ids: [vectorStore.id],
- },
- code_interpreter: {
- file_ids: this.linkedCsvIDs,
- },
- },
- });
- const thread = await this.openai.beta.threads.create();
-
- runInAction(() => {
- this.dataDoc.assistant_id = assistant.id;
- this.dataDoc.thread_id = thread.id;
- this.dataDoc.vector_store_id = vectorStore.id;
- this.assistantID = assistant.id;
- this.threadID = thread.id;
- this.vectorStoreID = vectorStore.id;
- this.isInitializing = false;
- });
- } catch (error) {
- console.error('Initialization failed:', error);
- this.isInitializing = false;
- }
- };
-
- @action
- runAssistant = async (inputText: string) => {
- // Ensure an assistant and thread are created
- if (!this.assistantID || !this.threadID || !this.vectorStoreID) {
- await this.createAssistant();
- console.log('Assistant and thread created:', this.assistantID, this.threadID);
- }
- let currentText: string = '';
- let currentToolCallMessage: string = '';
-
- // Send the user's input to the assistant
- await this.openai.beta.threads.messages.create(this.threadID, {
- role: 'user',
- content: inputText,
- });
-
- // Listen to the streaming responses
- const stream = this.openai.beta.threads.runs
- .stream(this.threadID, {
- assistant_id: this.assistantID,
- })
- .on('runStepCreated', (runStep: RunStep) => {
- currentText = '';
- runInAction(() => {
- this.current_message = { role: ASSISTANT_ROLE.ASSISTANT, text: currentText, tool_logs: '', links: [] };
- });
- this.isLoading = true;
- })
- .on('toolCallDelta', (toolCallDelta, snapshot) => {
- this.isLoading = false;
- if (toolCallDelta.type === 'code_interpreter') {
- if (toolCallDelta.code_interpreter?.input) {
- currentToolCallMessage += toolCallDelta.code_interpreter.input;
- runInAction(() => {
- if (this.current_message) {
- this.current_message.tool_logs = currentToolCallMessage;
- }
- });
- }
- if (toolCallDelta.code_interpreter?.outputs) {
- currentToolCallMessage += '\n Code interpreter output:';
- toolCallDelta.code_interpreter.outputs.forEach(output => {
- if (output.type === 'logs') {
- runInAction(() => {
- if (this.current_message) {
- this.current_message.tool_logs += '\n|' + output.logs;
- }
- });
- }
- });
- }
- }
- })
- .on('textDelta', (textDelta, snapshot) => {
- this.isLoading = false;
- currentText += textDelta.value;
- runInAction(() => {
- if (this.current_message) {
- // this.current_message = {...this.current_message, text: current_text};
- this.current_message.text = currentText;
- }
- });
- })
- .on('messageDone', async event => {
- console.log(event);
- const textItem = event.content.find(item => item.type === 'text');
- if (textItem && textItem.type === 'text') {
- const { text } = textItem;
- console.log(text.value);
- try {
- runInAction(() => {
- this.interim_history = text.value;
- });
- } catch (e) {
- console.error('Error parsing JSON response:', e);
- }
-
- const { annotations } = text;
- console.log('Annotations: ' + annotations);
- let index = 0;
- annotations.forEach(async annotation => {
- console.log(' ' + annotation);
- console.log(' ' + annotation.text);
- if (annotation.type === 'file_path') {
- const { file_path: filePath } = annotation;
- const fileToDownload = filePath.file_id;
- console.log(fileToDownload);
- if (filePath) {
- console.log(filePath);
- console.log(fileToDownload);
- this.createLink(fileToDownload, annotation.start_index, annotation.end_index, ANNOTATION_LINK_TYPE.DOWNLOAD_FILE);
- }
- } else {
- const { file_citation: fileCitation } = annotation;
- if (fileCitation) {
- const citedFile = await this.openai.files.retrieve(fileCitation.file_id);
- const citationUrl = citedFile.filename;
- this.createLink(citationUrl, annotation.start_index, annotation.end_index, ANNOTATION_LINK_TYPE.DASH_DOC, index);
- index++;
- }
- }
- });
- runInAction(() => {
- if (this.current_message) {
- console.log('current message: ' + this.current_message.text);
- this.current_message.text = this.interim_history;
- this.history.push({ ...this.current_message });
- this.current_message = undefined;
- }
- });
- }
- })
- .on('toolCallDone', toolCall => {
- runInAction(() => {
- if (this.current_message && currentToolCallMessage) {
- this.current_message.tool_logs = currentToolCallMessage;
- }
- });
- })
- .on('imageFileDone', (content: ImageFile, snapshot: Message) => {
- console.log('Image file done:', content);
- })
- .on('end', () => {
- console.log('Streaming done');
- });
- };
-
- @action
- goToLinkedDoc = async (link: string) => {
- const linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d);
-
- const linkedDoc = linkedDocs.find(doc => {
- const docUrl = CsvCast(doc.data, PDFCast(doc.data)).url.pathname.replace('/files/pdfs/', '').replace('/files/csvs/', '');
- console.log('URL: ' + docUrl + ' Citation URL: ' + link);
- return link === docUrl;
- });
-
- if (linkedDoc) {
- await DocumentManager.Instance.showDocument(DocCast(linkedDoc), { willZoomCentered: true }, () => {});
- }
- };
+ // getAssistantResponse() {
+ // return Docs.Create.MessageDocument(text, {});
+ // }
@action
askGPT = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault();
+ this.inputValue = '';
const textInput = event.currentTarget.elements.namedItem('messageInput') as HTMLInputElement;
const trimmedText = textInput.value.trim();
- if (!this.assistantID || !this.threadID) {
- try {
- await this.createAssistant();
- } catch (err) {
- console.error('Error:', err);
- }
- }
-
if (trimmedText) {
try {
textInput.value = '';
runInAction(() => {
- this.history.push({ role: ASSISTANT_ROLE.USER, text: trimmedText });
+ this.history.push({ role: ASSISTANT_ROLE.USER, text_content: trimmedText });
+ this.isLoading = true;
});
- await this.runAssistant(trimmedText);
- this.dataDoc.data = this.history.toString();
+
+ const response = await this.agent.askAgent(trimmedText); // Use the chatbot to get the response
+ runInAction(() => {
+ this.history.push(AnswerParser.parse(response));
+ });
+ this.dataDoc.data = JSON.stringify(this.history);
} catch (err) {
console.error('Error:', err);
+ runInAction(() => {
+ this.history.push({ role: ASSISTANT_ROLE.ASSISTANT, text_content: 'Sorry, I encountered an error while processing your request.' });
+ });
+ } finally {
+ runInAction(() => {
+ this.isLoading = false;
+ });
}
}
};
@action
- uploadLinks = async (linkedDocs: Doc[]) => {
- if (this.isInitializing) {
- console.log('Initialization in progress, upload aborted.');
- return;
+ updateMessageCitations = (index: number, citations: Citation[]) => {
+ if (this.history[index]) {
+ this.history[index].citations = citations;
}
- const urls = linkedDocs.map(doc => CsvCast(doc.data, PDFCast(doc.data)).url.pathname);
- const csvUrls = urls.filter(url => url.endsWith('.csv'));
- console.log(this.assistantID, this.threadID, urls);
+ };
- const { openai_file_ids: openaiFileIds } = await Networking.PostToServer('/uploadPDFToVectorStore', { urls, threadID: this.threadID, assistantID: this.assistantID, vector_store_id: this.vectorStoreID });
+ @action
+ handleCitationClick = (citation: Citation) => {
+ console.log('Citation clicked:', citation);
+ const currentLinkedDocs: Doc[] = this.linkedDocs;
+ const chunk_id = citation.chunk_id;
+ for (let doc of currentLinkedDocs) {
+ if (doc.chunk_simpl) {
+ //console.log(JSON.parse(StrCast(doc.chunk_simpl)));
+ const doc_chunk_simpl = JSON.parse(StrCast(doc.chunk_simpl));
+ console.log(doc_chunk_simpl);
+ const text_chunks = doc_chunk_simpl.text_chunks as [{ chunk_id: string; start_page: number; end_page: number }] | [];
+ const image_chunks = doc_chunk_simpl.image_chunks as [{ chunk_id: string; location: string; page: number }] | [];
+
+ const found_text_chunk = text_chunks.find(chunk => chunk.chunk_id === chunk_id);
+ if (found_text_chunk) {
+ const doc_url = CsvCast(doc.data, PDFCast(doc.data)).url.pathname;
+ console.log('URL: ' + doc_url);
+
+ //const ai_field_id = doc[this.Document[Id] + '_ai_field_id'];
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ console.log(doc.data);
+ //look at context path for each docview and choose the doc view that has as
+ //its parent the same collection view the chatbox is in
+ const first_view = Array.from(doc[DocViews])[0];
+ first_view.ComponentView?.search?.(citation.direct_text);
+ });
+ }
- linkedDocs.forEach((doc, i) => {
- doc[this.Document[Id] + '_ai_field_id'] = openaiFileIds[i];
- console.log('AI Field ID: ' + openaiFileIds[i]);
- });
+ const found_image_chunk = image_chunks.find(chunk => chunk.chunk_id === chunk_id);
+ if (found_image_chunk) {
+ const location_string: string = found_image_chunk.location;
- if (csvUrls.length > 0) {
- for (let i = 0; i < csvUrls.length; i++) {
- this.linkedCsvIDs.push(openaiFileIds[urls.indexOf(csvUrls[i])]);
- }
- console.log('linked csvs:' + this.linkedCsvIDs);
- await this.openai.beta.assistants.update(this.assistantID, {
- tools: [{ type: 'file_search' }, { type: 'code_interpreter' }],
- tool_resources: {
- file_search: {
- vector_store_ids: [this.vectorStoreID],
- },
- code_interpreter: {
- file_ids: this.linkedCsvIDs,
- },
- },
- });
- }
- };
+ // Extract variables from location_string
+ const values = location_string.replace(/[\[\]]/g, '').split(',');
- downloadToComputer = (url: string, fileName: string) => {
- fetch(url, { method: 'get', mode: 'no-cors', referrerPolicy: 'no-referrer' })
- .then(res => res.blob())
- .then(res => {
- const aElement = document.createElement('a');
- aElement.setAttribute('download', fileName);
- const href = URL.createObjectURL(res);
- aElement.href = href;
- aElement.setAttribute('target', '_blank');
- aElement.click();
- URL.revokeObjectURL(href);
- });
- };
+ // Ensure we have exactly 4 values
+ if (values.length !== 4) {
+ console.error('Location string must contain exactly 4 numbers');
+ return; // or handle this error as appropriate
+ }
- createDocumentInDash = async (url: string) => {
- const fileSuffix = url.substring(url.lastIndexOf('.') + 1);
- console.log(fileSuffix);
- let doc: Doc | null = null;
- switch (fileSuffix) {
- case 'pdf':
- doc = DocCast(await DocUtils.DocumentFromType('pdf', url, {}));
- break;
- case 'csv':
- doc = DocCast(await DocUtils.DocumentFromType('csv', url, {}));
- break;
- case 'png':
- case 'jpg':
- case 'jpeg':
- doc = DocCast(await DocUtils.DocumentFromType('image', url, {}));
- break;
- default:
- console.error('Unsupported file type:', fileSuffix);
- break;
- }
- if (doc) {
- doc && this._props.addDocument?.(doc);
- await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
- }
- };
+ const x1 = parseFloat(values[0]) * Doc.NativeWidth(doc);
+ const y1 = parseFloat(values[1]) * Doc.NativeHeight(doc);
+ const x2 = parseFloat(values[2]) * Doc.NativeWidth(doc);
+ const y2 = parseFloat(values[3]) * Doc.NativeHeight(doc);
- downloadFile = async (fileInfo: string, downloadType: DOWNLOAD_TYPE) => {
- try {
- console.log(fileInfo);
- const [fileId, fileName] = fileInfo.split(/!!!/);
- const { file_path: filePath } = await Networking.PostToServer('/downloadFileFromOpenAI', { file_id: fileId, file_name: fileName });
- const fileLink = CsvCast(new CsvField(filePath)).url.href;
- if (downloadType === DOWNLOAD_TYPE.DASH) {
- this.createDocumentInDash(fileLink);
- } else {
- this.downloadToComputer(fileLink, fileName);
- }
- } catch (error) {
- console.error('Error downloading file:', error);
- }
- };
+ const annotationKey = Doc.LayoutFieldKey(doc) + '_annotations';
- handleDownloadToDevice = () => {
- this.downloadFile(this.currentFile.url, DOWNLOAD_TYPE.DEVICE);
- this.modalStatus = false; // Close the modal after the action
- this.currentFile = { url: '' }; // Reset the current file
- };
+ const existingDoc = DocListCast(doc[DocData][annotationKey]).find(d => d.citation_id === citation.citation_id);
+ const highlight_doc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc);
- handleAddToDash = () => {
- // Assuming `downloadFile` is a method that handles adding to Dash
- this.downloadFile(this.currentFile.url, DOWNLOAD_TYPE.DASH);
- this.modalStatus = false; // Close the modal after the action
- this.currentFile = { url: '' }; // Reset the current file
+ DocumentManager.Instance.showDocument(highlight_doc, { willZoomCentered: true }, () => {});
+ }
+ }
+ }
+ // You can implement additional functionality here, such as showing a modal with the full citation content
};
- renderModal = () => {
- if (!this.modalStatus) return null;
-
- return (
- <div className="modal">
- <div className="modal-content">
- <h4>File Actions</h4>
- <p>Choose an action for the file:</p>
- <button type="button" onClick={this.handleDownloadToDevice}>
- Download to Device
- </button>
- <button type="button" onClick={this.handleAddToDash}>
- Add to Dash
- </button>
- <button
- type="button"
- onClick={() => {
- this.modalStatus = false;
- }}>
- Cancel
- </button>
- </div>
- </div>
- );
- };
- @action
- showModal = () => {
- this.modalStatus = true;
+ createImageCitationHighlight = (x1: number, y1: number, x2: number, y2: number, citation: Citation, annotationKey: string, pdfDoc: Doc): Doc => {
+ const highlight_doc = Docs.Create.FreeformDocument([], {
+ x: x1,
+ y: y1,
+ _width: x2 - x1,
+ _height: y2 - y1,
+ backgroundColor: 'rgba(255, 255, 0, 0.5)',
+ });
+ highlight_doc[DocData].citation_id = citation.citation_id;
+ Doc.AddDocToList(pdfDoc[DocData], annotationKey, highlight_doc);
+ highlight_doc.annotationOn = pdfDoc;
+ Doc.SetContainer(highlight_doc, pdfDoc);
+ return highlight_doc;
};
- @action
- setCurrentFile = (file: { url: string }) => {
- this.currentFile = file;
- };
+ // @action
+ // uploadLinks = async (linkedDocs: Doc[]) => {
+ // if (this.isUploadingDocs) {
+ // console.log('Initialization in progress, upload aborted.');
+ // return;
+ // }
+ // const urls: string[] = linkedDocs.map(doc => CsvCast(doc.data, PDFCast(doc.data)).url.pathname);
+ // const csvUrls: string[] = urls.filter(url => url.endsWith('.csv'));
+ // console.log(this.assistantID, this.threadID, urls);
+
+ // await Networking.PostToServer('/uploadPDFs', { file_path: urls[0] });
+
+ // // linkedDocs.forEach((doc, i) => {
+ // // doc[this.Document[Id] + '_ai_field_id'] = openaiFileIds[i];
+ // // console.log('AI Field ID: ' + openaiFileIds[i]);
+ // // });
+
+ // // if (csvUrls.length > 0) {
+ // // for (let i = 0; i < csvUrls.length; i++) {
+ // // this.linkedCsvIDs.push(openaiFileIds[urls.indexOf(csvUrls[i])]);
+ // // }
+ // // console.log('linked csvs:' + this.linkedCsvIDs);
+ // // await this.openai.beta.assistants.update(this.assistantID, {
+ // // tools: [{ type: 'file_search' }, { type: 'code_interpreter' }],
+ // // tool_resources: {
+ // // file_search: {
+ // // vector_store_ids: [this.vectorStoreID],
+ // // },
+ // // code_interpreter: {
+ // // file_ids: this.linkedCsvIDs,
+ // // },
+ // // },
+ // // });
+ // // }
+ // };
componentDidMount() {
this._props.setContentViewBox?.(this);
@@ -505,17 +265,20 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
const storedHistory = JSON.parse(StrCast(this.dataDoc.data));
runInAction(() => {
- this.history = storedHistory.map((msg: AssistantMessage) => ({
- role: msg.role,
- text: msg.text,
- quote: msg.quote,
- tool_logs: msg.tool_logs,
- image: msg.image,
- }));
+ this.history.push(
+ ...storedHistory.map((msg: AssistantMessage) => ({
+ role: msg.role,
+ text_content: msg.text_content,
+ follow_up_questions: msg.follow_up_questions,
+ citations: msg.citations,
+ }))
+ );
});
} catch (e) {
console.error('Failed to parse history from dataDoc:', e);
}
+ } else {
+ this.history = [{ role: ASSISTANT_ROLE.ASSISTANT, text_content: 'Welcome to the Document Analyser Assistant! Link a document or ask questions to get started.' }];
}
reaction(
() => {
@@ -526,79 +289,114 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return linkedDocs;
},
- linked => this.linked_docs_to_add.push(...linked.filter(linkedDoc => !this.linked_docs_to_add.includes(linkedDoc)))
+ linked => linked.forEach(doc => this.linked_docs_to_add.add(doc))
);
- observe(
- // right now this skips during initialization which is necessary because it would be blank
- // However, it will upload the same link twice when it is
- this.linked_docs_to_add,
- change => {
- // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
- switch (change.type as any) {
- case 'splice':
- if ((change as any).addedCount > 0) {
- // maybe check here if its already in the urls datadoc array so doesn't add twice
- console.log((change as any).added as Doc[]);
- this.uploadLinks((change as any).added as Doc[]);
- }
- // (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- break;
- case 'update': // let oldValue = change.oldValue;
- default:
- }
- },
- true
+ observe(this.linked_docs_to_add, change => {
+ if (change.type === 'add') {
+ runInAction(() => {
+ this.isUploadingDocs = true;
+ });
+ this.addDocToVectorstore(change.newValue);
+ runInAction(() => {
+ this.isUploadingDocs = false;
+ });
+ } else if (change.type === 'delete') {
+ console.log('Deleted docs: ', change.oldValue);
+ }
+ });
+ }
+
+ // case 'splice':
+ // if ((change as any).addedCount > 0) {
+ // // maybe check here if its already in the urls datadoc array so doesn't add twice
+ // console.log((change as any).added as Doc[]);
+ // this.addDocsToVectorstore((change as any).added as Doc[]);
+ // }
+ // // (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ // break;
+ // case 'update': // let oldValue = change.oldValue;
+ // default:
+
+ @computed
+ get linkedDocs() {
+ //return (CollectionFreeFormDocumentView.from(this._props.DocumentView?.())?._props.parent as CollectionFreeFormView)?.childDocs.filter(doc => doc != this.Document) ?? [];
+ return LinkManager.Instance.getAllRelatedLinks(this.Document)
+ .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
+ .map(d => DocCast(d?.annotationOn, d))
+ .filter(d => d);
+ }
+
+ @computed
+ get summaries(): string {
+ return (
+ LinkManager.Instance.getAllRelatedLinks(this.Document)
+ .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
+ .map(d => DocCast(d?.annotationOn, d))
+ .filter(d => d)
+ .map((doc, index) => `${index + 1}) ${doc.summary}`)
+ .join('\n') + '\n'
);
}
+ @computed
+ get formattedHistory(): string {
+ let history = '<chat_history>\n';
+ for (const message of this.history) {
+ history += `<${message.role}>${message.text_content}</${message.role}>\n`;
+ }
+ history += '</chat_history>';
+ return history;
+ }
+
+ retrieveSummaries = () => {
+ return this.summaries;
+ };
+
+ retrieveFormattedHistory = () => {
+ return this.formattedHistory;
+ };
+
+ @action
+ handleFollowUpClick = (question: string) => {
+ console.log('Follow-up question clicked:', question);
+ this.inputValue = question;
+ };
render() {
return (
- <MathJaxContext config={this.mathJaxConfig}>
- <div className="chatBox">
- {this.isInitializing && <div className="initializing-overlay">Initializing...</div>}
- {this.renderModal()}
- <div
- className="scroll-box chat-content"
- ref={r => {
- this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
- this._oldWheel = r;
- r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
- }}>
- <div className="messages">
- {this.history.map((message, index) => (
- <MessageComponent
- key={index}
- message={message}
- toggleToolLogs={this.toggleToolLogs}
- expandedLogIndex={this.expandedLogIndex}
- index={index}
- showModal={this.showModal}
- goToLinkedDoc={this.goToLinkedDoc}
- setCurrentFile={this.setCurrentFile}
- />
- ))}
- {!this.current_message ? null : (
- <MessageComponent
- key={this.history.length}
- message={this.current_message}
- toggleToolLogs={this.toggleToolLogs}
- expandedLogIndex={this.expandedLogIndex}
- index={this.history.length}
- showModal={this.showModal}
- goToLinkedDoc={this.goToLinkedDoc}
- setCurrentFile={this.setCurrentFile}
- isCurrent
- />
- )}
- </div>
+ <div className="chatBox">
+ {this.isUploadingDocs && <div className="uploading-overlay"></div>}
+ <div
+ className="scroll-box chat-content"
+ ref={r => {
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = r;
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ }}>
+ <div className="messages">
+ {this.history.map((message, index) => (
+ //<DocumentView key={index} Document={message} index={index} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
+ <MessageComponentBox key={index} message={message} index={index} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
+ ))}
+ {this.current_message && (
+ <MessageComponentBox
+ key={this.history.length}
+ message={this.current_message}
+ index={this.history.length}
+ onFollowUpClick={this.handleFollowUpClick}
+ onCitationClick={this.handleCitationClick}
+ updateMessageCitations={this.updateMessageCitations}
+ />
+ )}
</div>
- <form onSubmit={this.askGPT} className="chat-form">
- <input type="text" name="messageInput" autoComplete="off" placeholder="Type a message..." />
- <button type="submit">Send</button>
- </form>
</div>
- </MathJaxContext>
+ <form onSubmit={this.askGPT} className="chat-form">
+ <input type="text" name="messageInput" autoComplete="off" placeholder="Type a message..." value={this.inputValue} onChange={e => (this.inputValue = e.target.value)} />
+ <button type="submit" disabled={this.isLoading}>
+ {this.isLoading ? 'Thinking...' : 'Send'}
+ </button>
+ </form>
+ </div>
);
}
}