aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx')
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx610
1 files changed, 452 insertions, 158 deletions
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 44c231c87..6e9307d37 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -13,29 +13,41 @@ import { observer } from 'mobx-react';
import OpenAI, { ClientOptions } from 'openai';
import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { ClientUtils } from '../../../../../ClientUtils';
-import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { ClientUtils, OmitKeys } from '../../../../../ClientUtils';
+import { Doc, DocListCast, Opt } from '../../../../../fields/Doc';
import { DocData, DocViews } from '../../../../../fields/DocSymbols';
-import { CsvCast, DocCast, PDFCast, RTFCast, StrCast } from '../../../../../fields/Types';
-import { Networking } from '../../../../Network';
+import { RichTextField } from '../../../../../fields/RichTextField';
+import { ScriptField } from '../../../../../fields/ScriptField';
+import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast } from '../../../../../fields/Types';
import { DocUtils } from '../../../../documents/DocUtils';
-import { DocumentType } from '../../../../documents/DocumentTypes';
-import { Docs } from '../../../../documents/Documents';
+import { CollectionViewType, DocumentType } from '../../../../documents/DocumentTypes';
+import { Docs, DocumentOptions } from '../../../../documents/Documents';
import { DocumentManager } from '../../../../util/DocumentManager';
+import { ImageUtils } from '../../../../util/Import & Export/ImageUtils';
import { LinkManager } from '../../../../util/LinkManager';
+import { CompileError, CompileScript } from '../../../../util/Scripting';
+import { DictationButton } from '../../../DictationButton';
import { ViewBoxAnnotatableComponent } from '../../../DocComponent';
-import { DocumentView } from '../../DocumentView';
+import { AudioBox } from '../../AudioBox';
+import { DocumentView, DocumentViewInternal } from '../../DocumentView';
import { FieldView, FieldViewProps } from '../../FieldView';
import { PDFBox } from '../../PDFBox';
+import { ScriptingBox } from '../../ScriptingBox';
+import { VideoBox } from '../../VideoBox';
import { Agent } from '../agentsystem/Agent';
+import { supportedDocTypes } from '../tools/CreateDocumentTool';
import { ASSISTANT_ROLE, AssistantMessage, CHUNK_TYPE, Citation, ProcessingInfo, SimplifiedChunk, TEXT_TYPE } from '../types/types';
import { Vectorstore } from '../vectorstore/Vectorstore';
import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
import { ProgressBar } from './ProgressBar';
+import { OpenWhere } from '../../OpenWhere';
+import { Upload } from '../../../../../server/SharedMediaTypes';
dotenv.config();
+export type parsedDocData = { doc_type: string; data: unknown };
+export type parsedDoc = DocumentOptions & parsedDocData;
/**
* ChatBox is the main class responsible for managing the interaction between the user and the assistant,
* handling documents, and integrating with OpenAI for tasks such as document analysis, chat functionality,
@@ -44,17 +56,17 @@ dotenv.config();
@observer
export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// MobX observable properties to track UI state and data
- @observable history: AssistantMessage[] = [];
- @observable.deep current_message: AssistantMessage | undefined = undefined;
- @observable isLoading: boolean = false;
- @observable uploadProgress: number = 0;
- @observable currentStep: string = '';
- @observable expandedScratchpadIndex: number | null = null;
- @observable inputValue: string = '';
- @observable private linked_docs_to_add: ObservableSet = observable.set();
- @observable private linked_csv_files: { filename: string; id: string; text: string }[] = [];
- @observable private isUploadingDocs: boolean = false;
- @observable private citationPopup: { text: string; visible: boolean } = { text: '', visible: false };
+ @observable private _history: AssistantMessage[] = [];
+ @observable.deep private _current_message: AssistantMessage | undefined = undefined;
+ @observable private _isLoading: boolean = false;
+ @observable private _uploadProgress: number = 0;
+ @observable private _currentStep: string = '';
+ @observable private _expandedScratchpadIndex: number | null = null;
+ @observable private _inputValue: string = '';
+ @observable private _linked_docs_to_add: ObservableSet = observable.set();
+ @observable private _linked_csv_files: { filename: string; id: string; text: string }[] = [];
+ @observable private _isUploadingDocs: boolean = false;
+ @observable private _citationPopup: { text: string; visible: boolean } = { text: '', visible: false };
// Private properties for managing OpenAI API, vector store, agent, and UI elements
private openai: OpenAI;
@@ -62,6 +74,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
private vectorstore: Vectorstore;
private agent: Agent;
private messagesRef: React.RefObject<HTMLDivElement>;
+ private _textInputRef: HTMLInputElement | undefined | null;
/**
* Static method that returns the layout string for the field.
@@ -71,6 +84,10 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return FieldView.LayoutString(ChatBox, fieldKey);
}
+ setChatInput = action((input: string) => {
+ this._inputValue = input;
+ });
+
/**
* Constructor initializes the component, sets up OpenAI, vector store, and agent instances,
* and observes changes in the chat history to save the state in dataDoc.
@@ -89,13 +106,13 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id);
}
this.vectorstore = new Vectorstore(this.vectorstore_id, this.retrieveDocIds);
- this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createCSVInDash);
+ this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash);
this.messagesRef = React.createRef<HTMLDivElement>();
// Reaction to update dataDoc when chat history changes
reaction(
() =>
- this.history.map((msg: AssistantMessage) => ({
+ this._history.map((msg: AssistantMessage) => ({
role: msg.role,
content: msg.content,
follow_up_questions: msg.follow_up_questions,
@@ -114,20 +131,22 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
addDocToVectorstore = async (newLinkedDoc: Doc) => {
- this.uploadProgress = 0;
- this.currentStep = 'Initializing...';
- this.isUploadingDocs = true;
+ this._uploadProgress = 0;
+ this._currentStep = 'Initializing...';
+ this._isUploadingDocs = true;
try {
// Add the document to the vectorstore
await this.vectorstore.addAIDoc(newLinkedDoc, this.updateProgress);
} catch (error) {
console.error('Error uploading document:', error);
- this.currentStep = 'Error during upload';
+ this._currentStep = 'Error during upload';
} finally {
- this.isUploadingDocs = false;
- this.uploadProgress = 0;
- this.currentStep = '';
+ runInAction(() => {
+ this._isUploadingDocs = false;
+ this._uploadProgress = 0;
+ this._currentStep = '';
+ });
}
};
@@ -138,8 +157,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
updateProgress = (progress: number, step: string) => {
- this.uploadProgress = progress;
- this.currentStep = step;
+ this._uploadProgress = progress;
+ this._currentStep = step;
};
/**
@@ -176,7 +195,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const csvId = id ?? uuidv4();
// Add CSV details to linked files
- this.linked_csv_files.push({
+ this._linked_csv_files.push({
filename: CsvCast(newLinkedDoc.data).url.pathname,
id: csvId,
text: csvData,
@@ -198,7 +217,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
toggleToolLogs = (index: number) => {
- this.expandedScratchpadIndex = this.expandedScratchpadIndex === index ? null : index;
+ this._expandedScratchpadIndex = this._expandedScratchpadIndex === index ? null : index;
};
/**
@@ -257,7 +276,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
askGPT = async (event: React.FormEvent): Promise<void> => {
event.preventDefault();
- this.inputValue = '';
+ this._inputValue = '';
// Extract the user's message
const textInput = (event.currentTarget as HTMLFormElement).elements.namedItem('messageInput') as HTMLInputElement;
@@ -267,13 +286,13 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
textInput.value = '';
// Add the user's message to the history
- this.history.push({
+ this._history.push({
role: ASSISTANT_ROLE.USER,
content: [{ index: 0, type: TEXT_TYPE.NORMAL, text: trimmedText, citation_ids: null }],
processing_info: [],
});
- this.isLoading = true;
- this.current_message = {
+ this._isLoading = true;
+ this._current_message = {
role: ASSISTANT_ROLE.ASSISTANT,
content: [],
citations: [],
@@ -283,9 +302,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Define callbacks for real-time processing updates
const onProcessingUpdate = (processingUpdate: ProcessingInfo[]) => {
runInAction(() => {
- if (this.current_message) {
- this.current_message = {
- ...this.current_message,
+ if (this._current_message) {
+ this._current_message = {
+ ...this._current_message,
processing_info: processingUpdate,
};
}
@@ -295,9 +314,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const onAnswerUpdate = (answerUpdate: string) => {
runInAction(() => {
- if (this.current_message) {
- this.current_message = {
- ...this.current_message,
+ if (this._current_message) {
+ this._current_message = {
+ ...this._current_message,
content: [{ text: answerUpdate, type: TEXT_TYPE.NORMAL, index: 0, citation_ids: [] }],
};
}
@@ -309,22 +328,26 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Update the history with the final assistant message
runInAction(() => {
- if (this.current_message) {
- this.history.push({ ...finalMessage });
- this.current_message = undefined;
- this.dataDoc.data = JSON.stringify(this.history);
+ if (this._current_message) {
+ this._history.push({ ...finalMessage });
+ this._current_message = undefined;
+ this.dataDoc.data = JSON.stringify(this._history);
}
});
} catch (err) {
console.error('Error:', err);
// Handle error in processing
- this.history.push({
- role: ASSISTANT_ROLE.ASSISTANT,
- content: [{ index: 0, type: TEXT_TYPE.ERROR, text: 'Sorry, I encountered an error while processing your request.', citation_ids: null }],
- processing_info: [],
- });
+ runInAction(() =>
+ this._history.push({
+ role: ASSISTANT_ROLE.ASSISTANT,
+ content: [{ index: 0, type: TEXT_TYPE.ERROR, text: `Sorry, I encountered an error while processing your request: ${err} `, citation_ids: null }],
+ processing_info: [],
+ })
+ );
} finally {
- this.isLoading = false;
+ runInAction(() => {
+ this._isLoading = false;
+ });
this.scrollToBottom();
}
}
@@ -338,8 +361,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
updateMessageCitations = (index: number, citations: Citation[]) => {
- if (this.history[index]) {
- this.history[index].citations = citations;
+ if (this._history[index]) {
+ this._history[index].citations = citations;
}
};
@@ -354,29 +377,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
LinkManager.Instance.addLink(linkDoc);
- let canDisplay;
-
- try {
- // Fetch the URL content through the proxy
- const { data } = await Networking.PostToServer('/proxyFetch', { url });
-
- // Simulating header behavior since we can't fetch headers via proxy
- const xFrameOptions = data.headers?.['x-frame-options'];
-
- if (xFrameOptions && xFrameOptions.toUpperCase() === 'SAMEORIGIN') {
- canDisplay = false;
- } else {
- canDisplay = true;
- }
- } catch (error) {
- console.error('Error fetching the URL from the server:', error);
- }
const chunkToAdd = {
chunkId: id,
chunkType: CHUNK_TYPE.URL,
url: url,
- canDisplay: canDisplay,
};
doc.chunk_simpl = JSON.stringify({ chunks: [chunkToAdd] });
@@ -398,89 +403,364 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @param data The CSV data content.
*/
@action
- createCSVInDash = async (url: string, title: string, id: string, data: string) => {
- const doc = DocCast(await DocUtils.DocumentFromType('csv', url, { title: title, text: RTFCast(data) }));
+ createCSVInDash = (url: string, title: string, id: string, data: string) =>
+ DocUtils.DocumentFromType('csv', url, { title: title, text: RTFCast(data) }).then(doc => {
+ if (doc) {
+ LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
+ this._props.addDocument?.(doc);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}).then(() => this.addCSVForAnalysis(doc, id));
+ }
+ });
+ @action
+ createImageInDash = async (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => {
+ const newImgSrc =
+ result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
+ ? ClientUtils.prepend(result.accessPaths.agnostic.client)
+ : result.accessPaths.agnostic.client;
+ const doc = Docs.Create.ImageDocument(newImgSrc, options);
+ this.addDocument(ImageUtils.AssignImgInfo(doc, result));
const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
LinkManager.Instance.addLink(linkDoc);
-
- doc && this._props.addDocument?.(doc);
+ if (doc) {
+ if (this._props.addDocument) this._props.addDocument(doc);
+ else DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
+ }
await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ };
+
+ /**
+ * Creates a text document in the dashboard and adds it for analysis.
+ * @param title The title of the doc.
+ * @param text_content The text of the document.
+ * @param options Other optional document options (e.g. color)
+ * @param id The unique ID for the document.
+ */
+ @action
+ private createCollectionWithChildren = (data: parsedDoc[], insideCol: boolean): Opt<Doc>[] => data.map(doc => this.whichDoc(doc, insideCol));
+
+ @action
+ whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
+ const options = OmitKeys(doc, ['doct_type', 'data']).omit as DocumentOptions;
+ const data = (doc as parsedDocData).data;
+ const ndoc = (() => {
+ switch (doc.doc_type) {
+ default:
+ case supportedDocTypes.text: return Docs.Create.TextDocument(data as string, options);
+ case supportedDocTypes.comparison: return this.createComparison(JSON.parse(data as string) as parsedDoc[], options);
+ case supportedDocTypes.flashcard: return this.createFlashcard(JSON.parse(data as string) as parsedDoc[], options);
+ case supportedDocTypes.deck: return this.createDeck(JSON.parse(data as string) as parsedDoc[], options);
+ case supportedDocTypes.image: return Docs.Create.ImageDocument(data as string, options);
+ case supportedDocTypes.equation: return Docs.Create.EquationDocument(data as string, options);
+ case supportedDocTypes.notetaking: return Docs.Create.NoteTakingDocument([], options);
+ case supportedDocTypes.web: return Docs.Create.WebDocument(data as string, { ...options, data_useCors: true });
+ case supportedDocTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options);
+ case supportedDocTypes.pdf: return Docs.Create.PdfDocument(data as string, options);
+ case supportedDocTypes.video: return Docs.Create.VideoDocument(data as string, options);
+ case supportedDocTypes.diagram: return Docs.Create.DiagramDocument(undefined, { text: data as unknown as RichTextField, ...options}); // text: can take a string or RichTextField but it's typed for RichTextField.
+
+ // case supportedDocumentTypes.dataviz:
+ // {
+ // const { fileUrl, id } = await Networking.PostToServer('/createCSV', {
+ // filename: (options.title as string).replace(/\s+/g, '') + '.csv',
+ // data: data,
+ // });
+ // const doc = Docs.Create.DataVizDocument(fileUrl, { ...options, text: RTFCast(data as string) });
+ // this.addCSVForAnalysis(doc, id);
+ // return doc;
+ // }
+ case supportedDocTypes.script: {
+ const result = !(data as string).trim() ? ({ compiled: false, errors: [] } as CompileError) : CompileScript(data as string, {});
+ const script_field = result.compiled ? new ScriptField(result, undefined, data as string) : undefined;
+ const sdoc = Docs.Create.ScriptingDocument(script_field, options);
+ DocumentManager.Instance.showDocument(sdoc, { willZoomCentered: true }, () => {
+ const firstView = Array.from(sdoc[DocViews])[0] as DocumentView;
+ (firstView.ComponentView as ScriptingBox)?.onApply?.();
+ (firstView.ComponentView as ScriptingBox)?.onRun?.();
+ });
+ return sdoc;
+ }
+ case supportedDocTypes.collection: {
+ const arr = this.createCollectionWithChildren(JSON.parse(data as string) as parsedDoc[], true).filter(d=>d).map(d => d!);
+ const collOpts = { _width:300, _height: 300, _layout_fitWidth: true, _freeform_backgroundGrid: true, ...options, };
+ return (() => {
+ switch (options.type_collection) {
+ case CollectionViewType.Tree: return Docs.Create.TreeDocument(arr, collOpts);
+ case CollectionViewType.Stacking: return Docs.Create.StackingDocument(arr, collOpts);
+ case CollectionViewType.Masonry: return Docs.Create.MasonryDocument(arr, collOpts);
+ case CollectionViewType.Card: return Docs.Create.CardDeckDocument(arr, collOpts);
+ case CollectionViewType.Carousel: return Docs.Create.CarouselDocument(arr, collOpts);
+ case CollectionViewType.Carousel3D: return Docs.Create.Carousel3DDocument(arr, collOpts);
+ case CollectionViewType.Multicolumn: return Docs.Create.CarouselDocument(arr, collOpts);
+ default: return Docs.Create.FreeformDocument(arr, collOpts);
+ }
+ })();
+ }
+ // case supportedDocumentTypes.map: return Docs.Create.MapDocument([], options);
+ // case supportedDocumentTypes.button: return Docs.Create.ButtonDocument(options);
+ // case supportedDocumentTypes.trail: return Docs.Create.PresDocument(options);
+ } // prettier-ignore
+ })();
+
+ if (ndoc) {
+ ndoc.x = NumCast((options.x as number) ?? 0) + (insideCol ? 0 : NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc.width)) + 100;
+ ndoc.y = NumCast(options.y as number) + (insideCol ? 0 : NumCast(this.layoutDoc.y));
+ }
+ return ndoc;
+ };
+
+ /**
+ * Creates a document in the dashboard.
+ *
+ * @param {string} doc_type - The type of document to create.
+ * @param {string} data - The data used to generate the document.
+ * @param {DocumentOptions} options - Configuration options for the document.
+ * @returns {Promise<void>} A promise that resolves once the document is created and displayed.
+ */
+ @action
+ createDocInDash = (pdoc: parsedDoc) => {
+ const linkAndShowDoc = (doc: Opt<Doc>) => {
+ if (doc) {
+ LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
+ this._props.addDocument?.(doc);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ }
+ };
+ const doc = this.whichDoc(pdoc, false);
+ if (doc) linkAndShowDoc(doc);
+ return doc;
+ };
+
+ /**
+ * Creates a deck of flashcards.
+ *
+ * @param {any} data - The data used to generate the flashcards. Can be a string or an object.
+ * @param {DocumentOptions} options - Configuration options for the flashcard deck.
+ * @returns {Doc} A carousel document containing the flashcard deck.
+ */
+ @action
+ createDeck = (data: parsedDoc[], options: DocumentOptions) => {
+ const flashcardDeck: Doc[] = [];
+ // Process each flashcard document in the `deckData` array
+ if (data.length == 2 && data[0].doc_type == 'text' && data[1].doc_type == 'text') {
+ this.createFlashcard(data, options);
+ } else {
+ data.forEach(doc => {
+ const flashcardDoc = this.createFlashcard((doc as parsedDocData).data as parsedDoc[] | string[], options);
+ if (flashcardDoc) flashcardDeck.push(flashcardDoc);
+ });
+ }
+
+ // Create a carousel to contain the flashcard deck
+ return Docs.Create.CarouselDocument(flashcardDeck, {
+ title: options.title || 'Flashcard Deck',
+ _width: options._width || 300,
+ _height: options._height || 300,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ });
+ };
+
+ /**
+ * Creates a single flashcard document.
+ *
+ * @param {any} data - The data used to generate the flashcard. Can be a string or an object.
+ * @param {any} options - Configuration options for the flashcard.
+ * @returns {Doc | undefined} The created flashcard document, or undefined if the flashcard cannot be created.
+ */
+ @action
+ createFlashcard = (data: parsedDoc[] | string[], options: DocumentOptions) => {
+ const [front, back] = data;
+ const sideOptions = { _height: 300, ...options };
+
+ // Create front and back text documents
+ const side1 = typeof front === 'string' ? Docs.Create.CenteredTextCreator('question', front as string, sideOptions) : this.whichDoc(front, false);
+ const side2 = typeof back === 'string' ? Docs.Create.CenteredTextCreator('answer', back as string, sideOptions) : this.whichDoc(back, false);
- this.addCSVForAnalysis(doc, id);
+ // Create the flashcard document with both sides
+ return Docs.Create.FlashcardDocument('flashcard', side1, side2, sideOptions);
};
/**
+ * Creates a comparison document.
+ *
+ * @param {any} doc - The document data containing left and right components for comparison.
+ * @param {any} options - Configuration options for the comparison document.
+ * @returns {Doc} The created comparison document.
+ */
+ @action
+ createComparison = (doc: parsedDoc[], options: DocumentOptions) =>
+ Docs.Create.ComparisonDocument(options.title as string, {
+ data_back: this.whichDoc(doc[0], false),
+ data_front: this.whichDoc(doc[1], false),
+ _width: options._width,
+ _height: options._height || 300,
+ backgroundColor: options.backgroundColor,
+ });
+
+ /**
* Event handler to manage citations click in the message components.
* @param citation The citation object clicked by the user.
*/
@action
- handleCitationClick = (citation: Citation) => {
+ handleCitationClick = async (citation: Citation) => {
const currentLinkedDocs: Doc[] = this.linkedDocs;
-
const chunkId = citation.chunk_id;
- // Loop through the linked documents to find the matching chunk and handle its display
for (const doc of currentLinkedDocs) {
if (doc.chunk_simpl) {
const docChunkSimpl = JSON.parse(StrCast(doc.chunk_simpl)) as { chunks: SimplifiedChunk[] };
const foundChunk = docChunkSimpl.chunks.find(chunk => chunk.chunkId === chunkId);
+
if (foundChunk) {
- // Handle different types of chunks (image, text, table, etc.)
- switch (foundChunk.chunkType) {
- case CHUNK_TYPE.IMAGE:
- case CHUNK_TYPE.TABLE:
- {
- const values = foundChunk.location?.replace(/[[\]]/g, '').split(',');
+ // Handle media chunks specifically
+
+ if (doc.ai_type == 'video' || doc.ai_type == 'audio') {
+ const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []);
+
+ if (directMatchSegmentStart) {
+ // Navigate to the segment's start time in the media player
+ await this.goToMediaTimestamp(doc, directMatchSegmentStart, doc.ai_type);
+ } else {
+ console.error('No direct matching segment found for the citation.');
+ }
+ } else {
+ // Handle other chunk types as before
+ this.handleOtherChunkTypes(foundChunk, citation, doc);
+ }
+ }
+ }
+ }
+ };
- if (values?.length !== 4) {
- console.error('Location string must contain exactly 4 numbers');
- return;
- }
+ getDirectMatchingSegmentStart = (doc: Doc, citationText: string, indexesOfSegments: string[]): number => {
+ const originalSegments = JSON.parse(StrCast(doc.original_segments!)).map((segment: any, index: number) => ({
+ index: index.toString(),
+ text: segment.text,
+ start: segment.start,
+ end: segment.end,
+ }));
- const x1 = parseFloat(values[0]) * Doc.NativeWidth(doc);
- const y1 = parseFloat(values[1]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc);
- const x2 = parseFloat(values[2]) * Doc.NativeWidth(doc);
- const y2 = parseFloat(values[3]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc);
+ if (!Array.isArray(originalSegments) || originalSegments.length === 0 || !Array.isArray(indexesOfSegments)) {
+ return 0;
+ }
- const annotationKey = Doc.LayoutFieldKey(doc) + '_annotations';
+ // Create itemsToSearch array based on indexesOfSegments
+ const itemsToSearch = indexesOfSegments.map((indexStr: string) => {
+ const index = parseInt(indexStr, 10);
+ const segment = originalSegments[index];
+ return { text: segment.text, start: segment.start };
+ });
- const existingDoc = DocListCast(doc[DocData][annotationKey]).find(d => d.citation_id === citation.citation_id);
- const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc);
+ console.log('Constructed itemsToSearch:', itemsToSearch);
- DocumentManager.Instance.showDocument(highlightDoc, { willZoomCentered: true }, () => {});
- }
- break;
- case CHUNK_TYPE.TEXT:
- this.citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
- setTimeout(() => (this.citationPopup.visible = false), 3000); // Hide after 3 seconds
-
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
- const firstView = Array.from(doc[DocViews])[0] as DocumentView;
- (firstView.ComponentView as PDFBox)?.gotoPage?.(foundChunk.startPage);
- (firstView.ComponentView as PDFBox)?.search?.(citation.direct_text ?? '');
- });
- break;
- case CHUNK_TYPE.URL:
- if (!foundChunk.canDisplay) {
- window.open(StrCast(doc.displayUrl), '_blank');
- } else if (foundChunk.canDisplay) {
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
- }
- break;
- case CHUNK_TYPE.CSV:
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
- break;
- default:
- console.error('Chunk type not recognized:', foundChunk.chunkType);
- break;
- }
- }
+ // Helper function to calculate word overlap score
+ const calculateWordOverlap = (text1: string, text2: string): number => {
+ const words1 = new Set(text1.toLowerCase().split(/\W+/));
+ const words2 = new Set(text2.toLowerCase().split(/\W+/));
+ const intersection = new Set([...words1].filter(word => words2.has(word)));
+ return intersection.size / Math.max(words1.size, words2.size); // Jaccard similarity
+ };
+
+ // Search for the best matching segment
+ let bestMatchStart = 0;
+ let bestScore = 0;
+
+ console.log(`Searching for best match for query: "${citationText}"`);
+ itemsToSearch.forEach(item => {
+ const score = calculateWordOverlap(citationText, item.text);
+ console.log(`Comparing query to segment: "${item.text}" | Score: ${score}`);
+ if (score > bestScore) {
+ bestScore = score;
+ bestMatchStart = item.start;
}
+ });
+
+ console.log('Best match found with score:', bestScore, '| Start time:', bestMatchStart);
+
+ // Return the start time of the best match
+ return bestMatchStart;
+ };
+
+ /**
+ * Navigates to the given timestamp in the media player.
+ * @param doc The document containing the media file.
+ * @param timestamp The timestamp to navigate to.
+ */
+ goToMediaTimestamp = async (doc: Doc, timestamp: number, type: 'video' | 'audio') => {
+ try {
+ // Show the media document in the viewer
+ if (type == 'video') {
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ const firstView = Array.from(doc[DocViews])[0] as DocumentView;
+ (firstView.ComponentView as VideoBox)?.Seek?.(timestamp);
+ });
+ } else {
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ const firstView = Array.from(doc[DocViews])[0] as DocumentView;
+ (firstView.ComponentView as AudioBox)?.playFrom?.(timestamp);
+ });
+ }
+ console.log(`Navigated to timestamp: ${timestamp}s in document ${doc.id}`);
+ } catch (error) {
+ console.error('Error navigating to media timestamp:', error);
}
};
/**
+ * Handles non-media chunk types as before.
+ * @param foundChunk The chunk object.
+ * @param citation The citation object.
+ * @param doc The document containing the chunk.
+ */
+ handleOtherChunkTypes = (foundChunk: SimplifiedChunk, citation: Citation, doc: Doc) => {
+ switch (foundChunk.chunkType) {
+ case CHUNK_TYPE.IMAGE:
+ case CHUNK_TYPE.TABLE:
+ {
+ const values = foundChunk.location?.replace(/[[\]]/g, '').split(',');
+
+ if (values?.length !== 4) {
+ console.error('Location string must contain exactly 4 numbers');
+ return;
+ }
+ if (foundChunk.startPage === undefined || foundChunk.endPage === undefined) {
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ return;
+ }
+ const x1 = parseFloat(values[0]) * Doc.NativeWidth(doc);
+ const y1 = parseFloat(values[1]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc);
+ const x2 = parseFloat(values[2]) * Doc.NativeWidth(doc);
+ const y2 = parseFloat(values[3]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc);
+
+ const annotationKey = Doc.LayoutFieldKey(doc) + '_annotations';
+
+ const existingDoc = DocListCast(doc[DocData][annotationKey]).find(d => d.citation_id === citation.citation_id);
+ const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc);
+
+ DocumentManager.Instance.showDocument(highlightDoc, { willZoomCentered: true }, () => {});
+ }
+ break;
+ case CHUNK_TYPE.TEXT:
+ this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
+ setTimeout(() => (this._citationPopup.visible = false), 3000);
+
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ const firstView = Array.from(doc[DocViews])[0] as DocumentView;
+ (firstView.ComponentView as PDFBox)?.gotoPage?.(foundChunk.startPage ?? 0);
+ (firstView.ComponentView as PDFBox)?.search?.(citation.direct_text ?? '');
+ });
+ break;
+ case CHUNK_TYPE.CSV:
+ case CHUNK_TYPE.URL:
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true });
+ break;
+ default:
+ console.error('Unhandled chunk type:', foundChunk.chunkType);
+ break;
+ }
+ };
+ /**
* Creates an annotation highlight on a PDF document for image citations.
* @param x1 X-coordinate of the top-left corner of the highlight.
* @param y1 Y-coordinate of the top-left corner of the highlight.
@@ -524,7 +804,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
const storedHistory = JSON.parse(StrCast(this.dataDoc.data));
runInAction(() => {
- this.history.push(
+ this._history.push(
...storedHistory.map((msg: AssistantMessage) => ({
role: msg.role,
content: msg.content,
@@ -539,7 +819,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
} else {
// Default welcome message
runInAction(() => {
- this.history.push({
+ this._history.push({
role: ASSISTANT_ROLE.ASSISTANT,
content: [
{
@@ -563,16 +843,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.filter(d => d);
return linkedDocs;
},
- linked => linked.forEach(doc => this.linked_docs_to_add.add(doc))
+ linked => linked.forEach(doc => this._linked_docs_to_add.add(doc))
);
// Observe changes to linked documents and handle document addition
- observe(this.linked_docs_to_add, change => {
+ observe(this._linked_docs_to_add, change => {
if (change.type === 'add') {
- if (PDFCast(change.newValue.data)) {
- this.addDocToVectorstore(change.newValue);
- } else if (CsvCast(change.newValue.data)) {
+ if (CsvCast(change.newValue.data)) {
this.addCSVForAnalysis(change.newValue);
+ } else {
+ this.addDocToVectorstore(change.newValue);
}
} else if (change.type === 'delete') {
// Handle document removal
@@ -609,7 +889,10 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
.map(d => DocCast(d?.annotationOn, d))
.filter(d => d)
- .filter(d => d.ai_doc_id)
+ .filter(d => {
+ console.log(d.ai_doc_id);
+ return d.ai_doc_id;
+ })
.map(d => StrCast(d.ai_doc_id));
}
@@ -640,18 +923,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
/**
* Getter that retrieves all linked CSV files for analysis.
*/
- @computed
- get linkedCSVs(): { filename: string; id: string; text: string }[] {
- return this.linked_csv_files;
+ @computed get linkedCSVs(): { filename: string; id: string; text: string }[] {
+ return this._linked_csv_files;
}
/**
* Getter that formats the entire chat history as a string for the agent's system message.
*/
- @computed
- get formattedHistory(): string {
+ @computed get formattedHistory(): string {
let history = '<chat_history>\n';
- for (const message of this.history) {
+ for (const message of this._history) {
history += `<${message.role}>${message.content.map(content => content.text).join(' ')}`;
if (message.loop_summary) {
history += `<loop_summary>${message.loop_summary}</loop_summary>`;
@@ -687,20 +968,21 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
handleFollowUpClick = (question: string) => {
- this.inputValue = question;
+ this._inputValue = question;
};
+ _dictation: DictationButton | null = null;
/**
* Renders the chat interface, including the message list, input field, and other UI elements.
*/
render() {
return (
<div className="chat-box">
- {this.isUploadingDocs && (
+ {this._isUploadingDocs && (
<div className="uploading-overlay">
<div className="progress-container">
<ProgressBar />
- <div className="step-name">{this.currentStep}</div>
+ <div className="step-name">{this._currentStep}</div>
</div>
</div>
)}
@@ -708,24 +990,29 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<h2>{this.userName()}&apos;s AI Assistant</h2>
</div>
<div className="chat-messages" ref={this.messagesRef}>
- {this.history.map((message, index) => (
- <MessageComponentBox key={index} message={message} index={index} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
+ {this._history.map((message, index) => (
+ <MessageComponentBox key={index} message={message} 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}
- />
+ {this._current_message && (
+ <MessageComponentBox key={this._history.length} message={this._current_message} onFollowUpClick={this.handleFollowUpClick} onCitationClick={this.handleCitationClick} updateMessageCitations={this.updateMessageCitations} />
)}
</div>
+
<form onSubmit={this.askGPT} className="chat-input">
- <input type="text" name="messageInput" autoComplete="off" placeholder="Type your message here..." value={this.inputValue} onChange={e => (this.inputValue = e.target.value)} />
- <button className="submit-button" type="submit" disabled={this.isLoading}>
- {this.isLoading ? (
+ <input
+ ref={r => {
+ this._textInputRef = r;
+ }}
+ type="text"
+ name="messageInput"
+ autoComplete="off"
+ placeholder="Type your message here..."
+ value={this._inputValue}
+ onChange={action(e => (this._inputValue = e.target.value))}
+ disabled={this._isLoading}
+ />
+ <button className="submit-button" onClick={() => this._dictation?.stopDictation()} type="submit" disabled={this._isLoading || !this._inputValue.trim()}>
+ {this._isLoading ? (
<div className="spinner"></div>
) : (
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
@@ -734,12 +1021,19 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</svg>
)}
</button>
+ <DictationButton
+ ref={r => {
+ this._dictation = r;
+ }}
+ setInput={this.setChatInput}
+ inputRef={this._textInputRef}
+ />
</form>
{/* Popup for citation */}
- {this.citationPopup.visible && (
+ {this._citationPopup.visible && (
<div className="citation-popup">
<p>
- <strong>Text from your document: </strong> {this.citationPopup.text}
+ <strong>Text from your document: </strong> {this._citationPopup.text}
</p>
</div>
)}
@@ -753,5 +1047,5 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
Docs.Prototypes.TemplateMap.set(DocumentType.CHAT, {
layout: { view: ChatBox, dataField: 'data' },
- options: { acl: '', chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
+ options: { acl: '', _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
});