diff options
Diffstat (limited to 'src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx')
-rw-r--r-- | src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx | 262 |
1 files changed, 204 insertions, 58 deletions
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 89070ee5a..f13116fdd 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -16,17 +16,24 @@ import { v4 as uuidv4 } from 'uuid'; import { ClientUtils, OmitKeys } from '../../../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../../../fields/Doc'; import { DocData, DocViews } from '../../../../../fields/DocSymbols'; +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 { 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 { supportedDocumentTypes } from '../tools/CreateDocumentTool'; import { ASSISTANT_ROLE, AssistantMessage, CHUNK_TYPE, Citation, ProcessingInfo, SimplifiedChunk, TEXT_TYPE } from '../types/types'; @@ -34,6 +41,7 @@ import { Vectorstore } from '../vectorstore/Vectorstore'; import './ChatBox.scss'; import MessageComponentBox from './MessageComponent'; import { ProgressBar } from './ProgressBar'; +import { OpenWhere } from '../../OpenWhere'; dotenv.config(); @@ -97,7 +105,7 @@ 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.createDocInDash, 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 @@ -133,9 +141,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { console.error('Error uploading document:', error); this._currentStep = 'Error during upload'; } finally { - this._isUploadingDocs = false; - this._uploadProgress = 0; - this._currentStep = ''; + runInAction(() => { + this._isUploadingDocs = false; + this._uploadProgress = 0; + this._currentStep = ''; + }); } }; @@ -399,6 +409,23 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }); + @action + createImageInDash = async (result: any, 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); + 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. @@ -425,6 +452,31 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { case supportedDocumentTypes.notetaking: return Docs.Create.NoteTakingDocument([], options); case supportedDocumentTypes.web: return Docs.Create.WebDocument(data as string, { ...options, data_useCors: true }); case supportedDocumentTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options); + case supportedDocumentTypes.pdf: return Docs.Create.PdfDocument(data as string, options); + case supportedDocumentTypes.video: return Docs.Create.VideoDocument(data as string, options); + case supportedDocumentTypes.mermaid: 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 supportedDocumentTypes.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 supportedDocumentTypes.collection: { const arr = this.createCollectionWithChildren(data as parsedDoc[], true).filter(d=>d).map(d => d!); const collOpts = { ...options, _layout_fitWidth: true, _width:300, _height: 300, _freeform_backgroundGrid: true }; @@ -440,14 +492,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } })(); } - // case supportedDocumentTypes.diagram: return Docs.Create.DiagramDocument(options); - // case supportedDocumentTypes.audio: return Docs.Create.AudioDocument(data as string, options); // case supportedDocumentTypes.map: return Docs.Create.MapDocument([], options); // case supportedDocumentTypes.button: return Docs.Create.ButtonDocument(options); - // case supportedDocumentTypes.script: return Docs.Create.ScriptingDocument(null, options); - // case supportedDocumentTypes.chat: return Docs.Create.ChatDocument(options); // case supportedDocumentTypes.trail: return Docs.Create.PresDocument(options); - // case supportedDocumentTypes.trail: return Docs.Create.FreeformDocument([], options); } // prettier-ignore })(); @@ -552,68 +599,164 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { * @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: - 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. @@ -702,10 +845,10 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // Observe changes to linked documents and handle document addition 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 @@ -742,7 +885,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)); } |