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.tsx1052
1 files changed, 792 insertions, 260 deletions
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 15b148372..c09df166d 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -16,12 +16,14 @@ 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 { Id } from '../../../../../fields/FieldSymbols';
import { RichTextField } from '../../../../../fields/RichTextField';
import { ScriptField } from '../../../../../fields/ScriptField';
-import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast } from '../../../../../fields/Types';
+import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast, VideoCast, AudioCast } from '../../../../../fields/Types';
import { DocUtils } from '../../../../documents/DocUtils';
import { CollectionViewType, DocumentType } from '../../../../documents/DocumentTypes';
import { Docs, DocumentOptions } from '../../../../documents/Documents';
+import { DocServer } from '../../../../DocServer';
import { DocumentManager } from '../../../../util/DocumentManager';
import { ImageUtils } from '../../../../util/Import & Export/ImageUtils';
import { LinkManager } from '../../../../util/LinkManager';
@@ -35,18 +37,30 @@ import { PDFBox } from '../../PDFBox';
import { ScriptingBox } from '../../ScriptingBox';
import { VideoBox } from '../../VideoBox';
import { Agent } from '../agentsystem/Agent';
-import { supportedDocTypes } from '../tools/CreateDocumentTool';
+import { supportedDocTypes } from '../types/tool_types';
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';
+import { AgentDocumentManager } from '../utils/AgentDocumentManager';
+import { AiOutlineSend } from 'react-icons/ai';
+import { SnappingManager } from '../../../../util/SnappingManager';
+import { Button, Size, Type } from '@dash/components';
+import { MdLink, MdViewModule } from 'react-icons/md';
+import { Tooltip } from '@mui/material';
dotenv.config();
-export type parsedDocData = { doc_type: string; data: unknown };
+export type parsedDocData = {
+ doc_type: string;
+ data: unknown;
+ _disable_resource_loading?: boolean;
+ _sandbox_iframe?: boolean;
+ _iframe_sandbox?: string;
+ data_useCors?: boolean;
+};
export type parsedDoc = DocumentOptions & parsedDocData;
/**
* ChatBox is the main class responsible for managing the interaction between the user and the assistant,
@@ -67,14 +81,18 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@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 _isFontSizeModalOpen: boolean = false;
+ @observable private _fontSize: 'small' | 'normal' | 'large' | 'xlarge' = 'normal';
+ @observable private _toolReloadModal: { visible: boolean; toolName: string } = { visible: false, toolName: '' };
// Private properties for managing OpenAI API, vector store, agent, and UI elements
- private openai: OpenAI;
- private vectorstore_id: string;
- private vectorstore: Vectorstore;
- private agent: Agent;
- private messagesRef: React.RefObject<HTMLDivElement>;
+ private openai!: OpenAI; // Using definite assignment assertion
+ private vectorstore_id: string | undefined;
+ private vectorstore: Vectorstore | undefined;
+ private agent: Agent | undefined;
+ private messagesRef: React.RefObject<HTMLDivElement> = React.createRef();
private _textInputRef: HTMLInputElement | undefined | null;
+ private docManager: AgentDocumentManager | undefined;
/**
* Static method that returns the layout string for the field.
@@ -88,6 +106,12 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._inputValue = input;
});
+ @action
+ toggleCanvasMode = () => {
+ const newMode = !this.docManager?.getCanvasMode();
+ this.docManager?.setCanvasMode(newMode);
+ };
+
/**
* 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.
@@ -95,33 +119,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
constructor(props: FieldViewProps) {
super(props);
- makeObservable(this); // Enable MobX observables
-
- // Initialize OpenAI, vectorstore, and agent
- this.openai = this.initializeOpenAI();
- if (StrCast(this.dataDoc.vectorstore_id) == '') {
- this.vectorstore_id = uuidv4();
- this.dataDoc.vectorstore_id = this.vectorstore_id;
- } else {
- 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.createImageInDash, this.createDocInDash, this.createCSVInDash);
- this.messagesRef = React.createRef<HTMLDivElement>();
-
- // Reaction to update dataDoc when chat history changes
- reaction(
- () =>
- this._history.map((msg: AssistantMessage) => ({
- role: msg.role,
- content: msg.content,
- follow_up_questions: msg.follow_up_questions,
- citations: msg.citations,
- })),
- serializableHistory => {
- this.dataDoc.data = JSON.stringify(serializableHistory);
- }
- );
+ makeObservable(this);
}
/**
@@ -131,22 +129,54 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
addDocToVectorstore = async (newLinkedDoc: Doc) => {
- this._uploadProgress = 0;
- this._currentStep = 'Initializing...';
- this._isUploadingDocs = true;
-
try {
- // Add the document to the vectorstore
+ const isAudioOrVideo = VideoCast(newLinkedDoc.data)?.url?.pathname || AudioCast(newLinkedDoc.data)?.url?.pathname;
+
+ // Set UI state to show the processing overlay
+ runInAction(() => {
+ this._isUploadingDocs = true;
+ this._uploadProgress = 0;
+ this._currentStep = isAudioOrVideo ? 'Preparing media file...' : 'Processing document...';
+ });
+
+ if (!this.docManager || !this.vectorstore) throw new Error('Document manager or vectorstore is not initialized');
+ // Process the document first to ensure it has a valid ID
+ await this.docManager.processDocument(newLinkedDoc);
+
+ // Add the document to the vectorstore which will also register chunks
await this.vectorstore.addAIDoc(newLinkedDoc, this.updateProgress);
- } catch (error) {
- console.error('Error uploading document:', error);
- this._currentStep = 'Error during upload';
- } finally {
+
+ // Give a slight delay to show the completion message
+ if (this._uploadProgress === 100) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ // Reset UI state
runInAction(() => {
this._isUploadingDocs = false;
this._uploadProgress = 0;
this._currentStep = '';
});
+
+ return true;
+ } catch (err) {
+ console.error('Error adding document to vectorstore:', err);
+
+ // Show error in UI
+ runInAction(() => {
+ this._currentStep = `Error: ${err instanceof Error ? err.message : 'Failed to process document'}`;
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // Reset UI state
+ runInAction(() => {
+ this._isUploadingDocs = false;
+ this._uploadProgress = 0;
+ this._currentStep = '';
+ });
+
+ return false;
}
};
@@ -157,10 +187,18 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
updateProgress = (progress: number, step: string) => {
- this._uploadProgress = progress;
+ // Ensure progress is within expected bounds
+ const validProgress = Math.min(Math.max(0, progress), 100);
+ this._uploadProgress = validProgress;
this._currentStep = step;
+
+ // Force UI update
+ if (process.env.NODE_ENV !== 'production') {
+ console.log(`Progress: ${validProgress}%, Step: ${step}`);
+ }
};
+ //TODO: Update for new chunk_simpl on agentDocument
/**
* Adds a CSV file for analysis by sending it to OpenAI and generating a summary.
* @param newLinkedDoc The linked document representing the CSV file.
@@ -229,14 +267,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
apiKey: process.env.OPENAI_KEY,
dangerouslyAllowBrowser: true,
};
- return new OpenAI(configuration);
+ this.openai = new OpenAI(configuration);
}
/**
* Adds a scroll event listener to detect user scrolling and handle passive wheel events.
*/
addScrollListener = () => {
- if (this.messagesRef.current) {
+ if (this.messagesRef?.current) {
this.messagesRef.current.addEventListener('wheel', this.onPassiveWheel, { passive: false });
}
};
@@ -245,7 +283,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* Removes the scroll event listener from the chat messages container.
*/
removeScrollListener = () => {
- if (this.messagesRef.current) {
+ if (this.messagesRef?.current) {
this.messagesRef.current.removeEventListener('wheel', this.onPassiveWheel);
}
};
@@ -276,15 +314,19 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
askGPT = async (event: React.FormEvent): Promise<void> => {
event.preventDefault();
+ if (!this._textInputRef) {
+ console.log('ERROR: text input ref is undefined');
+ return;
+ }
this._inputValue = '';
// Extract the user's message
- const textInput = (event.currentTarget as HTMLFormElement).elements.namedItem('messageInput') as HTMLInputElement;
- const trimmedText = textInput.value.trim();
+ const textInput = this._textInputRef?.value ?? '';
+ const trimmedText = textInput.trim();
if (trimmedText) {
+ this._textInputRef.value = ''; // Clear the input field
try {
- textInput.value = '';
// Add the user's message to the history
this._history.push({
role: ASSISTANT_ROLE.USER,
@@ -323,6 +365,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
});
};
+ if (!this.agent) throw new Error('Agent is not initialized');
// Send the user's question to the assistant and get the final message
const finalMessage = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate);
@@ -367,27 +410,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
/**
- * Adds a linked document from a URL for future reference and analysis.
- * @param url The URL of the document to add.
- * @param id The unique identifier for the document.
- */
- @action
- addLinkedUrlDoc = async (url: string, id: string) => {
- const doc = Docs.Create.WebDocument(url, { data_useCors: true });
-
- const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
- LinkManager.Instance.addLink(linkDoc);
-
- const chunkToAdd = {
- chunkId: id,
- chunkType: CHUNK_TYPE.URL,
- url: url,
- };
-
- doc.chunk_simpl = JSON.stringify({ chunks: [chunkToAdd] });
- };
-
- /**
* Getter to retrieve the current user's name from the client utils.
*/
@computed
@@ -408,7 +430,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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));
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => this.addCSVForAnalysis(doc, id));
}
});
@@ -440,21 +462,34 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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;
+ public whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
+ const options = OmitKeys(doc, ['doc_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.note: 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.web: {
+ // Create web document with enhanced safety options
+ const webOptions = {
+ ...options,
+ data_useCors: true
+ };
+
+ // If iframe_sandbox was passed from AgentDocumentManager, add it to the options
+ if ('_iframe_sandbox' in options) {
+ webOptions._iframe_sandbox = options._iframe_sandbox;
+ }
+
+ return Docs.Create.WebDocument(data as string, webOptions);
+ }
+ case supportedDocTypes.dataviz: case supportedDocTypes.table: return Docs.Create.DataVizDocument('/Users/ajshul/Dash-Web/src/server/public/files/csv/0d237e7c-98c9-44d0-aa61-5285fdbcf96c-random_sample.csv.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.
@@ -510,28 +545,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
/**
- * 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.
@@ -604,83 +617,139 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
handleCitationClick = async (citation: Citation) => {
- const currentLinkedDocs: Doc[] = this.linkedDocs;
- const chunkId = citation.chunk_id;
+ try {
+ // Extract values from MobX proxy object if needed
+ const chunkId = typeof citation.chunk_id === 'object' ? (citation.chunk_id as any).toString() : citation.chunk_id;
+
+ // For debugging
+ console.log('Citation clicked:', {
+ chunkId,
+ citation: JSON.stringify(citation, null, 2),
+ });
- 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 (!this.docManager) throw new Error('Document manager is not initialized');
- if (foundChunk) {
- // Handle media chunks specifically
+ // Get the simplified chunk using the document manager
+ const { foundChunk, doc, dataDoc } = this.docManager.getSimplifiedChunkById(chunkId);
+ console.log('doc: ', doc);
+ console.log('dataDoc: ', dataDoc);
+ if (!foundChunk || !doc) {
+ if (doc) {
+ console.warn(`Chunk not found in document, ${doc.id}, for chunk ID: ${chunkId}`);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ } else {
+ console.warn(`Chunk not found for chunk ID: ${chunkId}`);
+ }
+ return;
+ }
- if (doc.ai_type == 'video' || doc.ai_type == 'audio') {
- const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []);
+ console.log(`Found chunk in document:`, foundChunk);
- 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);
- }
+ // Handle different chunk types
+ if (foundChunk.chunkType === CHUNK_TYPE.AUDIO || foundChunk.chunkType === CHUNK_TYPE.VIDEO) {
+ const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []);
+ if (directMatchSegmentStart) {
+ await this.goToMediaTimestamp(doc, directMatchSegmentStart, foundChunk.chunkType);
+ } else {
+ console.error('No direct matching segment found for the citation.');
+ }
+ } else if (foundChunk.chunkType === CHUNK_TYPE.TABLE || foundChunk.chunkType === CHUNK_TYPE.IMAGE) {
+ console.log('here: ', foundChunk);
+ this.handleOtherChunkTypes(foundChunk as SimplifiedChunk, citation, doc);
+ } else {
+ if (doc.type === 'web') {
+ DocumentManager.Instance.showDocument(doc, { openLocation: OpenWhere.addRight }, () => {});
+ return;
}
+ this.handleOtherChunkTypes(foundChunk, citation, doc, dataDoc);
+ // Show the chunk text in citation popup
+ const chunkText = citation.direct_text || 'Text content not available';
+ this.showCitationPopup(chunkText);
+
+ // Also navigate to the document
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
}
+ } catch (error) {
+ console.error('Error handling citation click:', error);
}
};
+ /**
+ * Finds a matching segment in a document based on text content.
+ * @param doc The document to search in
+ * @param citationText The text to find in the document
+ * @param indexesOfSegments Optional indexes of segments to search in
+ * @returns The starting timestamp of the matching segment, or -1 if not found
+ */
getDirectMatchingSegmentStart = (doc: Doc, citationText: string, indexesOfSegments: string[]): number => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- 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,
- }));
-
- if (!Array.isArray(originalSegments) || originalSegments.length === 0 || !Array.isArray(indexesOfSegments)) {
- return 0;
+ if (!doc || !citationText || !this.docManager) return -1;
+
+ // Get original segments using document manager
+ const original_segments = this.docManager.getOriginalSegments(doc);
+
+ if (!original_segments || !Array.isArray(original_segments) || original_segments.length === 0) {
+ return -1;
}
- // 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 };
- });
+ let segments = original_segments;
- console.log('Constructed itemsToSearch:', itemsToSearch);
+ // If specific indexes are provided, filter segments by those indexes
+ if (indexesOfSegments && indexesOfSegments.length > 0) {
+ segments = original_segments.filter(segment => indexesOfSegments.includes(segment.index));
+ }
- // Helper function to calculate word overlap score
+ // If no segments match the indexes, use all segments
+ if (segments.length === 0) {
+ segments = original_segments;
+ }
+
+ // First try to find an exact match
+ const exactMatch = segments.find(segment => segment.text && segment.text.includes(citationText));
+
+ if (exactMatch) {
+ return exactMatch.start;
+ }
+
+ // If no exact match, find segment with best word overlap
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
+ if (!text1 || !text2) return 0;
+
+ const words1 = text1.toLowerCase().split(/\s+/);
+ const words2 = text2.toLowerCase().split(/\s+/);
+ const wordSet1 = new Set(words1);
+
+ let overlap = 0;
+ for (const word of words2) {
+ if (wordSet1.has(word)) {
+ overlap++;
+ }
+ }
+
+ // Return percentage of overlap relative to the shorter text
+ return overlap / Math.min(words1.length, words2.length);
};
- // 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;
+ // Find segment with highest word overlap
+ let bestMatch = null;
+ let highestOverlap = 0;
+
+ for (const segment of segments) {
+ if (!segment.text) continue;
+
+ const overlap = calculateWordOverlap(segment.text, citationText);
+ if (overlap > highestOverlap) {
+ highestOverlap = overlap;
+ bestMatch = segment;
}
- });
+ }
- console.log('Best match found with score:', bestScore, '| Start time:', bestMatchStart);
+ // Only return matches with significant overlap (more than 30%)
+ if (bestMatch && highestOverlap > 0.3) {
+ return bestMatch.start;
+ }
- // Return the start time of the best match
- return bestMatchStart;
+ // If no good match found, return the start of the first segment as fallback
+ return segments.length > 0 ? segments[0].start : -1;
};
/**
@@ -714,7 +783,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @param citation The citation object.
* @param doc The document containing the chunk.
*/
- handleOtherChunkTypes = (foundChunk: SimplifiedChunk, citation: Citation, doc: Doc) => {
+ handleOtherChunkTypes = (foundChunk: SimplifiedChunk, citation: Citation, doc: Doc, dataDoc?: Doc) => {
switch (foundChunk.chunkType) {
case CHUNK_TYPE.IMAGE:
case CHUNK_TYPE.TABLE:
@@ -729,6 +798,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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);
@@ -736,31 +806,182 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const annotationKey = '$' + Doc.LayoutDataKey(doc) + '_annotations';
- const existingDoc = DocListCast(doc[annotationKey]).find(d => d.citation_id === citation.citation_id);
+ const existingDoc = DocListCast(doc[DocData][annotationKey]).find(d => d.citation_id === citation.citation_id);
+ if (existingDoc) {
+ existingDoc.x = x1;
+ existingDoc.y = y1;
+ existingDoc._width = x2 - x1;
+ existingDoc._height = y2 - y1;
+ }
const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc);
+ //doc.layout_scroll = y1;
+ doc._layout_curPage = foundChunk.startPage + 1;
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);
+ {
+ this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
+ this.startCitationPopupTimer();
- 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 ?? '');
- });
+ // Check if the document is a PDF (has a PDF viewer component)
+ const isPDF = PDFCast(dataDoc!.data) !== null || dataDoc!.type === DocumentType.PDF;
+
+ // First ensure document is fully visible before trying to access its views
+ this.ensureDocumentIsVisible(dataDoc!, isPDF, citation, foundChunk, doc);
+ }
break;
case CHUNK_TYPE.CSV:
case CHUNK_TYPE.URL:
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true });
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ console.log(`Showing web document in viewer with URL: ${foundChunk.url}`);
+ });
break;
default:
console.error('Unhandled chunk type:', foundChunk.chunkType);
break;
}
};
+
+ /**
+ * Ensures a document is fully visible and rendered before performing actions on it
+ * @param doc The document to ensure is visible
+ * @param isPDF Whether this is a PDF document
+ * @param citation The citation information
+ * @param foundChunk The chunk information
+ * @param doc The document to ensure is visible
+ */
+ ensureDocumentIsVisible = (doc: Doc, isPDF: boolean, citation: Citation, foundChunk: SimplifiedChunk, layoutDoc?: Doc) => {
+ try {
+ // First, check if the document already has views and is rendered
+ const hasViews = doc[DocViews] && doc[DocViews].size > 0;
+
+ console.log(`Document ${doc.id}: Current state - hasViews: ${hasViews}, isPDF: ${isPDF}`);
+
+ if (hasViews) {
+ // Document is already rendered, proceed with accessing its view
+ this.processPDFDocumentView(doc, isPDF, citation, foundChunk);
+ return;
+ } else if (layoutDoc) {
+ this.processPDFDocumentView(layoutDoc, isPDF, citation, foundChunk);
+ return;
+ }
+
+ // If document is not rendered yet, show it and wait for it to be ready
+ console.log(`Document ${doc.id} needs to be shown first`);
+
+ // Force document to be rendered by using willZoomCentered: true
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ // Wait a bit for the document to be fully rendered (longer than our previous attempts)
+ setTimeout(() => {
+ // Now manually check if document view exists and is valid
+ this.verifyAndProcessDocumentView(doc, isPDF, citation, foundChunk, 1);
+ }, 800); // Increased initial delay
+ });
+ } catch (error) {
+ console.error('Error ensuring document visibility:', error);
+ // Show the document anyway as a fallback
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true });
+ }
+ };
+
+ /**
+ * Verifies document view exists and processes it, with retries if needed
+ */
+ verifyAndProcessDocumentView = (doc: Doc, isPDF: boolean, citation: Citation, foundChunk: SimplifiedChunk, attempt: number) => {
+ // Diagnostic info
+ console.log(`Verify attempt ${attempt}: Document views for ${doc.id}:`, doc[DocViews] ? `Found ${doc[DocViews].size} views` : 'No views');
+
+ // Double-check document exists in current document system
+ const docExists = DocServer.GetCachedRefField(doc[Id]) !== undefined;
+ if (!docExists) {
+ console.warn(`Document ${doc.id} no longer exists in document system`);
+ return;
+ }
+
+ try {
+ if (!doc[DocViews] || doc[DocViews].size === 0) {
+ if (attempt >= 5) {
+ console.error(`Maximum verification attempts (${attempt}) reached for document ${doc.id}`);
+
+ // Last resort: force re-creation of the document view
+ if (isPDF) {
+ console.log('Forcing document recreation as last resort');
+ DocumentManager.Instance.showDocument(doc, {
+ willZoomCentered: true,
+ });
+ }
+ return;
+ }
+
+ // Let's try explicitly requesting the document be shown again
+ if (attempt > 2) {
+ console.log(`Attempt ${attempt}: Re-requesting document be shown`);
+ DocumentManager.Instance.showDocument(doc, {
+ willZoomCentered: true,
+ openLocation: attempt % 2 === 0 ? OpenWhere.addRight : undefined,
+ });
+ }
+
+ // Use exponential backoff for retries
+ const nextDelay = Math.min(2000, 500 * Math.pow(1.5, attempt));
+ console.log(`Scheduling retry ${attempt + 1} in ${nextDelay}ms`);
+
+ setTimeout(() => {
+ this.verifyAndProcessDocumentView(doc, isPDF, citation, foundChunk, attempt + 1);
+ }, nextDelay);
+ return;
+ }
+
+ this.processPDFDocumentView(doc, isPDF, citation, foundChunk);
+ } catch (error) {
+ console.error(`Error on verification attempt ${attempt}:`, error);
+ if (attempt < 5) {
+ setTimeout(
+ () => {
+ this.verifyAndProcessDocumentView(doc, isPDF, citation, foundChunk, attempt + 1);
+ },
+ 500 * Math.pow(1.5, attempt)
+ );
+ }
+ }
+ };
+
+ /**
+ * Processes a PDF document view once we're sure it exists
+ */
+ processPDFDocumentView = (doc: Doc, isPDF: boolean, citation: Citation, foundChunk: SimplifiedChunk) => {
+ try {
+ const views = Array.from(doc[DocViews] || []);
+ if (!views.length) {
+ console.warn('No document views found in document that should have views');
+ return;
+ }
+
+ const firstView = views[0] as DocumentView;
+ if (!firstView) {
+ console.warn('First view is invalid');
+ return;
+ }
+
+ console.log(`Successfully found document view for ${doc.id}:`, firstView.ComponentView ? `Component: ${firstView.ComponentView.constructor.name}` : 'No component view');
+
+ if (!firstView.ComponentView) {
+ console.warn('Component view not available');
+ return;
+ }
+
+ // For PDF documents, perform fuzzy search
+ if (isPDF && firstView.ComponentView && citation.direct_text) {
+ const pdfComponent = firstView.ComponentView as PDFBox;
+ this.ensureFuzzySearchAndExecute(pdfComponent, citation.direct_text.trim(), foundChunk.startPage);
+ }
+ } catch (error) {
+ console.error('Error processing PDF document view:', error);
+ }
+ };
+
/**
* Creates an annotation highlight on a PDF document for image citations.
* @param x1 X-coordinate of the top-left corner of the highlight.
@@ -780,7 +1001,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
_height: y2 - y1,
backgroundColor: 'rgba(255, 255, 0, 0.5)',
});
- highlight_doc.$citation_id = citation.citation_id;
+ highlight_doc[DocData].citation_id = citation.citation_id;
+ highlight_doc.freeform_scale = 1;
Doc.AddDocToList(pdfDoc[DocData], annotationKey, highlight_doc);
highlight_doc.annotationOn = pdfDoc;
Doc.SetContainer(highlight_doc, pdfDoc);
@@ -800,6 +1022,65 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* Initializes scroll listeners, sets up document reactions, and loads chat history from dataDoc if available.
*/
componentDidMount() {
+ // At mount time, find the DocumentView whose .Document is the collection container.
+ const parentView = this.DocumentView?.()._props.containerViewPath?.().lastElement();
+ if (!parentView) {
+ console.warn('GPT ChatBox not inside a DocumentView – cannot perform operations on Documents.');
+ return;
+ }
+
+ this.docManager = new AgentDocumentManager(this, parentView);
+
+ // Initialize OpenAI client
+ this.initializeOpenAI();
+
+ // Create a unique vectorstore ID for this ChatBox
+ this.vectorstore_id = uuidv4();
+
+ // Initialize vectorstore with the document manager
+ this.vectorstore = new Vectorstore(this.vectorstore_id, this.docManager);
+
+ /*
+ reaction(
+ () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }),
+ ({ selDoc, visible }) => {
+ const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs;
+ if (hasChildDocs) {
+ this._textToDocMap.clear();
+ this.setCollectionContext(selDoc.Document);
+ this.onGptResponse = (sortResult: string, questionType: GPTDocCommand) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType);
+ this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
+ this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
+ Doc.getDescription(doc).then(text => this._textToDocMap.set(text.replace(/\n/g, ' ').trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+ )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+ }
+ },
+ { fireImmediately: true }
+ );
+ }*/
+
+ // Initialize font size from saved preference
+ this.initFontSize();
+ // Create an agent with the vectorstore
+ this.agent = new Agent(this.vectorstore, this.retrieveFormattedHistory.bind(this), this.retrieveCSVData.bind(this), this.createImageInDash.bind(this), this.createCSVInDash.bind(this), this.docManager);
+
+ // Set up the tool created callback
+ this.agent.setToolCreatedCallback(this.handleToolCreated);
+
+ // Reaction to update dataDoc when chat history changes
+ reaction(
+ () =>
+ this._history.map((msg: AssistantMessage) => ({
+ role: msg.role,
+ content: msg.content,
+ follow_up_questions: msg.follow_up_questions,
+ citations: msg.citations,
+ })),
+ serializableHistory => {
+ this.dataDoc.data = JSON.stringify(serializableHistory);
+ }
+ );
+
this._props.setContentViewBox?.(this);
if (this.dataDoc.data) {
try {
@@ -860,6 +1141,19 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
});
this.addScrollListener();
+
+ // Initialize the document manager by finding existing documents
+ this.docManager.initializeFindDocsFreeform();
+
+ // If there are stored doc IDs in our list of docs to add, process them
+ if (this._linked_docs_to_add.size > 0) {
+ this._linked_docs_to_add.forEach(async doc => {
+ await this.docManager!.processDocument(doc);
+ });
+ }
+
+ // Add event listeners
+ this.addScrollListener();
}
/**
@@ -871,58 +1165,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
/**
- * Getter that retrieves all linked documents for the current document.
- */
- @computed
- get linkedDocs() {
- return LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d)
- .map(d => d!);
- }
-
- /**
- * Getter that retrieves document IDs of linked documents that have AI-related content.
- */
- @computed
- get docIds() {
- return LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d)
- .map(d => d!)
- .filter(d => {
- console.log(d.ai_doc_id);
- return d.ai_doc_id;
- })
- .map(d => StrCast(d.ai_doc_id));
- }
-
- /**
- * Getter that retrieves summaries of all linked documents.
- */
- @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?.summary)
- .map((doc, index) => {
- if (PDFCast(doc?.data)) {
- return `<summary file_name="${PDFCast(doc!.data)!.url.pathname}" applicable_tools=["rag"]>${doc!.summary}</summary>`;
- } else if (CsvCast(doc?.data)) {
- return `<summary file_name="${CsvCast(doc!.data)!.url.pathname}" applicable_tools=["dataAnalysis"]>${doc!.summary}</summary>`;
- } else {
- return `${index + 1}) ${doc?.summary}`;
- }
- })
- .join('\n') + '\n'
- );
- }
-
- /**
* Getter that retrieves all linked CSV files for analysis.
*/
@computed get linkedCSVs(): { filename: string; id: string; text: string }[] {
@@ -947,22 +1189,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Other helper methods for retrieving document data and processing
- retrieveSummaries = () => {
- return this.summaries;
- };
-
retrieveCSVData = () => {
return this.linkedCSVs;
};
- retrieveFormattedHistory = () => {
+ retrieveFormattedHistory = (): string => {
return this.formattedHistory;
};
- retrieveDocIds = () => {
- return this.docIds;
- };
-
/**
* Handles follow-up questions when the user clicks on them.
* Automatically sets the input value to the clicked follow-up question.
@@ -973,25 +1207,280 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._inputValue = question;
};
+ /**
+ * Handles tool creation notification and shows the reload modal
+ * @param toolName The name of the tool that was created
+ */
+ @action
+ handleToolCreated = (toolName: string) => {
+ this._toolReloadModal = {
+ visible: true,
+ toolName: toolName,
+ };
+ };
+
+ /**
+ * Closes the tool reload modal
+ */
+ @action
+ closeToolReloadModal = () => {
+ this._toolReloadModal = {
+ visible: false,
+ toolName: '',
+ };
+ };
+
+ /**
+ * Handles the reload confirmation and triggers page reload
+ */
+ @action
+ handleReloadConfirmation = async () => {
+ // Close the modal first
+ this.closeToolReloadModal();
+
+ try {
+ // Perform the deferred tool save operation
+ const saveSuccess = await this.agent?.performDeferredToolSave();
+
+ if (saveSuccess) {
+ console.log('Tool saved successfully, proceeding with reload...');
+ } else {
+ console.warn('Tool save failed, but proceeding with reload anyway...');
+ }
+ } catch (error) {
+ console.error('Error during deferred tool save:', error);
+ }
+
+ // Trigger page reload to rebuild webpack and load the new tool
+ setTimeout(() => {
+ window.location.reload();
+ }, 100);
+ };
+
_dictation: DictationButton | null = null;
- setInputRef = (r: HTMLInputElement) => (this._textInputRef = r);
- setDictationRef = (r: DictationButton) => (this._dictation = r);
+
/**
- * Renders the chat interface, including the message list, input field, and other UI elements.
+ * Toggles the font size modal visibility
+ */
+ @action
+ toggleFontSizeModal = () => {
+ this._isFontSizeModalOpen = !this._isFontSizeModalOpen;
+ };
+
+ /**
+ * Changes the font size and applies it to the chat interface
+ * @param size The new font size to apply
+ */
+ @action
+ changeFontSize = (size: 'small' | 'normal' | 'large' | 'xlarge') => {
+ this._fontSize = size;
+ this._isFontSizeModalOpen = false;
+
+ // Save preference to localStorage if needed
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('chatbox-font-size', size);
+ }
+ };
+
+ /**
+ * Initializes font size from saved preference
+ */
+ initFontSize = () => {
+ if (typeof window !== 'undefined') {
+ const savedSize = localStorage.getItem('chatbox-font-size');
+ if (savedSize && ['small', 'normal', 'large', 'xlarge'].includes(savedSize)) {
+ this._fontSize = savedSize as 'small' | 'normal' | 'large' | 'xlarge';
+ }
+ }
+ };
+
+ /**
+ * Renders a font size icon SVG
+ */
+ renderFontSizeIcon = () => (
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <polyline points="4 7 4 4 20 4 20 7"></polyline>
+ <line x1="9" y1="20" x2="15" y2="20"></line>
+ <line x1="12" y1="4" x2="12" y2="20"></line>
+ </svg>
+ );
+
+ /**
+ * Shows the citation popup with the given text.
+ * @param text The text to display in the popup.
+ */
+ @action
+ showCitationPopup = (text: string) => {
+ this._citationPopup = {
+ text: text || 'No text available',
+ visible: true,
+ };
+ this.startCitationPopupTimer();
+ };
+
+ /**
+ * Closes the citation popup.
+ */
+ @action
+ closeCitationPopup = () => {
+ this._citationPopup.visible = false;
+ };
+
+ /**
+ * Starts the auto-close timer for the citation popup.
+ */
+ startCitationPopupTimer = () => {
+ // Auto-close the popup after 5 seconds
+ setTimeout(() => this.closeCitationPopup(), 5000);
+ };
+
+ /**
+ * Retry PDF search with exponential backoff
+ */
+ retryPdfSearch = (doc: Doc, citation: Citation, foundChunk: SimplifiedChunk, isPDF: boolean, attempt: number) => {
+ if (attempt > 5) {
+ console.error('Maximum retry attempts reached for PDF search');
+ return;
+ }
+
+ const delay = Math.min(2000, 500 * Math.pow(1.5, attempt)); // Exponential backoff with max delay of 2 seconds
+
+ setTimeout(() => {
+ try {
+ if (!doc[DocViews] || doc[DocViews].size === 0) {
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ return;
+ }
+
+ const views = Array.from(doc[DocViews]);
+ if (!views.length) {
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ return;
+ }
+
+ const firstView = views[0] as DocumentView;
+ if (!firstView || !firstView.ComponentView) {
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ return;
+ }
+
+ const pdfComponent = firstView.ComponentView as PDFBox;
+ if (isPDF && pdfComponent && citation.direct_text) {
+ console.log(`PDF component found on attempt ${attempt}, executing search...`);
+ this.ensureFuzzySearchAndExecute(pdfComponent, citation.direct_text.trim(), foundChunk.startPage);
+ }
+ } catch (error) {
+ console.error(`Error on retry attempt ${attempt}:`, error);
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ }
+ }, delay);
+ };
+
+ /**
+ * Ensures fuzzy search is enabled in PDFBox and performs a search
+ * @param pdfComponent The PDFBox component
+ * @param searchText The text to search for
+ * @param startPage Optional page to navigate to before searching
+ */
+ private ensureFuzzySearchAndExecute = (pdfComponent: PDFBox, searchText: string, startPage?: number) => {
+ if (!pdfComponent) {
+ console.warn('PDF component is undefined, cannot perform search');
+ return;
+ }
+
+ if (!searchText?.trim()) {
+ console.warn('Search text is empty, skipping search');
+ return;
+ }
+
+ try {
+ // Check if the component has required methods
+ if (typeof pdfComponent.gotoPage !== 'function' || typeof pdfComponent.toggleFuzzySearch !== 'function' || typeof pdfComponent.search !== 'function') {
+ console.warn('PDF component missing required methods');
+ return;
+ }
+
+ // Navigate to the page if specified
+ if (typeof startPage === 'number') {
+ pdfComponent.gotoPage(startPage + 1);
+ }
+
+ // Always try to enable fuzzy search
+ try {
+ // PDFBox.tsx toggles fuzzy search state internally
+ // We'll call it once to make sure it's enabled
+ pdfComponent.toggleFuzzySearch();
+ } catch (toggleError) {
+ console.warn('Error toggling fuzzy search:', toggleError);
+ }
+
+ // Add a sufficient delay to ensure PDF is fully loaded before searching
+ setTimeout(() => {
+ try {
+ console.log('Performing fuzzy search for text:', searchText);
+ pdfComponent.search(searchText);
+ } catch (searchError) {
+ console.error('Error performing search:', searchError);
+ }
+ }, 1000); // Increased delay for better reliability
+ } catch (error) {
+ console.error('Error in fuzzy search setup:', error);
+ }
+ };
+
+ /**
+ * Main render method for the ChatBox
*/
render() {
+ const fontSizeClass = `font-size-${this._fontSize}`;
+
return (
- <div className="chat-box">
+ <div className={`chat-box ${fontSizeClass}`}>
{this._isUploadingDocs && (
<div className="uploading-overlay">
<div className="progress-container">
- <ProgressBar />
- <div className="step-name">{this._currentStep}</div>
+ <div className="progress-bar-wrapper">
+ <div className="progress-bar" style={{ width: `${this._uploadProgress}%` }} />
+ </div>
+ <div className="progress-details">
+ <div className="progress-percentage">{Math.round(this._uploadProgress)}%</div>
+ <div className="step-name">{this._currentStep}</div>
+ </div>
</div>
</div>
)}
<div className="chat-header">
<h2>{this.userName()}&apos;s AI Assistant</h2>
+ <div className="header-controls">
+ <Tooltip title={<div className="dash-tooltip">{this.docManager?.getCanvasMode() ? 'Click to limit scope to linked documents' : 'Click to expand scope to all documents on canvas'}</div>} placement="bottom">
+ <div className={`canvas-mode-toggle ${this.docManager?.getCanvasMode() ? 'canvas-active' : ''}`} onClick={this.toggleCanvasMode}>
+ {this.docManager?.getCanvasMode() ? <MdViewModule /> : <MdLink />}
+ </div>
+ </Tooltip>
+ <div className="font-size-control" onClick={this.toggleFontSizeModal}>
+ {this.renderFontSizeIcon()}
+ </div>
+ </div>
+ {this._isFontSizeModalOpen && (
+ <div className="font-size-modal">
+ <div className={`font-size-option ${this._fontSize === 'small' ? 'active' : ''}`} onClick={() => this.changeFontSize('small')}>
+ <span className="option-label">Small</span>
+ <span className="size-preview small">Aa</span>
+ </div>
+ <div className={`font-size-option ${this._fontSize === 'normal' ? 'active' : ''}`} onClick={() => this.changeFontSize('normal')}>
+ <span className="option-label">Normal</span>
+ <span className="size-preview normal">Aa</span>
+ </div>
+ <div className={`font-size-option ${this._fontSize === 'large' ? 'active' : ''}`} onClick={() => this.changeFontSize('large')}>
+ <span className="option-label">Large</span>
+ <span className="size-preview large">Aa</span>
+ </div>
+ <div className={`font-size-option ${this._fontSize === 'xlarge' ? 'active' : ''}`} onClick={() => this.changeFontSize('xlarge')}>
+ <span className="option-label">Extra Large</span>
+ <span className="size-preview xlarge">Aa</span>
+ </div>
+ </div>
+ )}
</div>
<div className="chat-messages" ref={this.messagesRef}>
{this._history.map((message, index) => (
@@ -1003,34 +1492,77 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</div>
<form onSubmit={this.askGPT} className="chat-input">
- <input
- ref={this.setInputRef}
- 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}
+ <div className="input-container">
+ <input
+ ref={r => {
+ this._textInputRef = r;
+ }}
+ type="text"
+ name="messageInput"
+ autoComplete="off"
+ placeholder="Type your message here..."
+ value={this._inputValue}
+ onChange={e => this.setChatInput(e.target.value)}
+ disabled={this._isLoading}
+ />
+ </div>
+ <Button
+ // className="submit-button"
+ onClick={this.askGPT}
+ type={Type.PRIM}
+ tooltip="Send to AI"
+ color={SnappingManager.userVariantColor}
+ inactive={this._isLoading || !this._inputValue.trim()}
+ icon={<AiOutlineSend />}
+ size={Size.LARGE}
+ />
+ <DictationButton
+ ref={r => {
+ this._dictation = r;
+ }}
+ setInput={this.setChatInput}
+ inputRef={this._textInputRef}
/>
- <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">
- <line x1="22" y1="2" x2="11" y2="13"></line>
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
- </svg>
- )}
- </button>
- <DictationButton ref={this.setDictationRef} setInput={this.setChatInput} inputRef={this._textInputRef} />
</form>
{/* Popup for citation */}
{this._citationPopup.visible && (
<div className="citation-popup">
- <p>
- <strong>Text from your document: </strong> {this._citationPopup.text}
- </p>
+ <div className="citation-popup-header">
+ <strong>Text from your document</strong>
+ <button className="citation-close-button" onClick={this.closeCitationPopup}>
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
+ <line x1="18" y1="6" x2="6" y2="18"></line>
+ <line x1="6" y1="6" x2="18" y2="18"></line>
+ </svg>
+ </button>
+ </div>
+ <div className="citation-content">{this._citationPopup.text}</div>
+ </div>
+ )}
+
+ {/* Tool Reload Modal */}
+ {this._toolReloadModal.visible && (
+ <div className="tool-reload-modal-overlay">
+ <div className="tool-reload-modal">
+ <div className="tool-reload-modal-header">
+ <h3>Tool Created Successfully!</h3>
+ </div>
+ <div className="tool-reload-modal-content">
+ <p>
+ The tool <strong>{this._toolReloadModal.toolName}</strong> has been created and saved successfully.
+ </p>
+ <p>To make the tool available for future use, the page needs to be reloaded to rebuild the application bundle.</p>
+ <p>Click &quot;Reload Page&quot; to complete the tool installation.</p>
+ </div>
+ <div className="tool-reload-modal-actions">
+ <button className="reload-button primary" onClick={this.handleReloadConfirmation}>
+ Reload Page
+ </button>
+ <button className="close-button secondary" onClick={this.closeToolReloadModal}>
+ Later
+ </button>
+ </div>
+ </div>
</div>
)}
</div>
@@ -1043,5 +1575,5 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
Docs.Prototypes.TemplateMap.set(DocumentType.CHAT, {
layout: { view: ChatBox, dataField: 'data' },
- options: { acl: '', _layout_nativeDimEditable: true, _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '', _forceActive: true },
});