diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/ChatBox/ChatBox.scss | 77 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/ChatBox.tsx | 351 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/MessageComponent.tsx | 53 | ||||
-rw-r--r-- | src/server/ApiManagers/AssistantManager.ts | 33 |
4 files changed, 216 insertions, 298 deletions
diff --git a/src/client/views/nodes/ChatBox/ChatBox.scss b/src/client/views/nodes/ChatBox/ChatBox.scss index a08b98f50..f1e3d3d67 100644 --- a/src/client/views/nodes/ChatBox/ChatBox.scss +++ b/src/client/views/nodes/ChatBox/ChatBox.scss @@ -9,12 +9,10 @@ $border-radius: 8px; .chatBox { display: flex; flex-direction: column; - width: 100%; /* Adjust the width as needed, could be in percentage */ - height: 100%; /* Adjust the height as needed, could be in percentage */ + width: 100%; + height: 100%; background-color: $background-color; font-family: 'Helvetica Neue', Arial, sans-serif; - //margin: 20px auto; - //overflow: hidden; .scroll-box { flex-grow: 1; @@ -24,6 +22,7 @@ $border-radius: 8px; padding: 10px; display: flex; flex-direction: column-reverse; + padding-bottom: 0; &::-webkit-scrollbar { width: 8px; @@ -47,28 +46,24 @@ $border-radius: 8px; border-radius: $border-radius; background-color: lighten($background-color, 5%); box-shadow: 0 2px 5px $shadow-color; - //display: flex; - align-items: center; - max-width: 70%; + align-items: flex-start; + max-width: 90%; + width: 100%; word-break: break-word; + .message-footer { - // Assuming this is the container for the toggle button - //max-width: 70%; + width: 100%; .toggle-logs-button { - margin-top: 10px; // Padding on sides to align with the text above + margin-top: 10px; width: 95%; - //display: block; // Ensures the button extends the full width of its container - text-align: center; // Centers the text inside the button - //padding: 8px 0; // Adequate padding for touch targets + text-align: center; background-color: $button-color; color: #fff; border: none; border-radius: $border-radius; cursor: pointer; - //transition: background-color 0.3s; - //margin-top: 10px; // Adds space above the button - box-shadow: 0 2px 4px $shadow-color; // Consistent shadow with other elements + box-shadow: 0 2px 4px $shadow-color; &:hover { background-color: $button-hover-color; } @@ -78,13 +73,9 @@ $border-radius: 8px; background-color: $input-background; color: $text-color; margin-top: 5px; - //padding: 10px; - //border-radius: $border-radius; - //box-shadow: inset 0 2px 4px $shadow-color; - //transition: opacity 1s ease-in-out; font-family: monospace; overflow-x: auto; - max-height: 150px; // Ensuring it does not grow too large + max-height: 150px; overflow-y: auto; } } @@ -118,18 +109,17 @@ $border-radius: 8px; } } } - padding-bottom: 0; } .chat-form { display: flex; - flex-grow: 1; - //height: 50px; + flex-grow: 0; bottom: 0; width: 100%; padding: 10px; background-color: $input-background; box-shadow: inset 0 -1px 2px $shadow-color; + margin-bottom: 0; input[type='text'] { flex-grow: 1; @@ -147,12 +137,12 @@ $border-radius: 8px; border-radius: $border-radius; cursor: pointer; transition: background-color 0.3s; + min-width: 80px; &:hover { background-color: $button-hover-color; } } - margin-bottom: 0; } } @@ -168,7 +158,7 @@ $border-radius: 8px; align-items: center; font-size: 1.5em; color: $text-color; - z-index: 10; // Ensure it's above all other content (may be better solution) + z-index: 10; &::before { content: 'Initializing...'; @@ -214,7 +204,6 @@ $border-radius: 8px; border: none; border-radius: $border-radius; cursor: pointer; - //margin: 5px; transition: background-color 0.3s; &:hover { @@ -223,3 +212,37 @@ $border-radius: 8px; } } } + +.follow-up-questions { + margin-top: 10px; + width: 100%; + + h4 { + margin-bottom: 5px; + font-size: 14px; + } + + .follow-up-button { + background-color: #f0f0f0; + border: 1px solid #ddd; + border-radius: 8px; + padding: 8px 10px; + margin: 4px 0; + cursor: pointer; + transition: background-color 0.3s; + display: block; + width: 100%; + text-align: left; + white-space: normal; + word-wrap: break-word; + font-size: 12px; + color: $text-color; + min-height: 40px; + height: auto; // Allow the button to expand as needed + line-height: 1.3; + + &:hover { + background-color: #e0e0e0; + } + } +} diff --git a/src/client/views/nodes/ChatBox/ChatBox.tsx b/src/client/views/nodes/ChatBox/ChatBox.tsx index 390f13ce7..b986c7393 100644 --- a/src/client/views/nodes/ChatBox/ChatBox.tsx +++ b/src/client/views/nodes/ChatBox/ChatBox.tsx @@ -21,6 +21,7 @@ import './ChatBox.scss'; import MessageComponent from './MessageComponent'; import { ANNOTATION_LINK_TYPE, ASSISTANT_ROLE, AssistantMessage, DOWNLOAD_TYPE } from './types'; import { Annotation } from 'mobx/dist/internal'; +import { FormEvent } from 'react'; @observer export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -33,7 +34,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable isInitializing: boolean = true; @observable expandedLogIndex: number | null = null; @observable linked_docs_to_add: Doc[] = []; - + @observable inputValue: string = ''; private openai: OpenAI; private assistantID: string = ''; private threadID: string = ''; @@ -161,239 +162,65 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @action - runAssistant = async (inputText: string) => { - // Ensure an assistant and thread are created - if (!this.assistantID || !this.threadID || !this.vectorStoreID) { - await this.createAssistant(); - console.log('Assistant and thread created:', this.assistantID, this.threadID); - } - - let currentText: string = ''; - let currentToolCallMessage: string = ''; - - // Send the user's input to the assistant - await this.openai.beta.threads.messages.create(this.threadID, { - role: 'user', - content: inputText, - }); - - // Listen to the streaming responses - const stream = this.openai.beta.threads.runs - .stream(this.threadID, { - assistant_id: this.assistantID, - }) - .on('runStepCreated', (runStep: RunStep) => { - runInAction(() => { - this.isLoading = true; - //intentionally don't merge run steps' messages and keep them as seperate messages on the interface - this.current_message = { role: ASSISTANT_ROLE.ASSISTANT, text: '', tool_logs: '' }; - }); - }) - .on('toolCallDelta', (toolCallDelta, snapshot) => { - if (toolCallDelta.type === 'code_interpreter') { - if (toolCallDelta.code_interpreter?.input) { - currentToolCallMessage += toolCallDelta.code_interpreter.input; - - if (this.current_message) { - this.current_message.tool_logs = currentToolCallMessage; - } - } - - if (toolCallDelta.code_interpreter?.outputs) { - currentToolCallMessage += '\n Code interpreter output:'; - toolCallDelta.code_interpreter.outputs.forEach(output => { - if (output.type === 'logs') { - runInAction(() => { - if (this.current_message) { - this.current_message.tool_logs += '\n|' + output.logs; - } - }); - } - }); - } - } - }) - .on('textDelta', (textDelta, snapshot) => { - currentText += textDelta.value; - runInAction(() => { - if (this.current_message) { - this.current_message.text = currentText; - } - }); - }) - .on('messageDone', async message => { - console.log(this.current_message); - const textItem = message.content.find(item => item.type === 'text'); - if (textItem && textItem.type === 'text') { - const { text } = textItem; - console.log(text.value); - try { - if (this.current_message) { - this.current_message.text = text.value; - } - } catch (e) { - console.error('Error parsing JSON response:', e); - } - console.log(this.current_message); - - const { annotations } = text; - console.log('Annotations: ' + annotations); - - await this.createLinks( - annotations.filter( - (annotation => { - const seenAnnotationTexts = new Set<string>(); - return annotation => { - if (seenAnnotationTexts.has(annotation.text)) { - return false; - } else { - seenAnnotationTexts.add(annotation.text); - return true; - } - }; - })() - ) - ); - } - runInAction(() => { - if (this.current_message) { - console.log(this.current_message); - this.history.push({ ...this.current_message }); - this.current_message = undefined; - } - }); - }) - .on('toolCallDone', async toolCall => { - runInAction(() => { - if (this.current_message?.tool_logs) { - this.history.push({ ...this.current_message }); - this.current_message = undefined; - } - }); - }) - .on('imageFileDone', (content: ImageFile, snapshot: Message) => { - console.log('Image file done:', content); - }); - }; - - createLinks = async (annotations: OpenAI.Beta.Threads.Messages.Annotation[]) => { - console.log(this.current_message); - let text = this.current_message?.text; - console.log(text); - await Promise.all( - annotations.map(async annotation => { - const subString = annotation.text; - const textToDisplay = `DASHLINK`; - let fileInfo = ''; - let formattedLink = ''; - const fileName = subString.split('/')[subString.split('/').length - 1]; - - if (annotation.type === 'file_path') { - const { file_path: filePath } = annotation; - if (filePath) { - fileInfo = filePath.file_id + '!!!' + fileName; - formattedLink = `[${textToDisplay}](${fileInfo}~~~${ANNOTATION_LINK_TYPE.DOWNLOAD_FILE})`; - } - } else { - const { file_citation: fileCitation } = annotation; - if (fileCitation) { - const citedFile = await this.openai.files.retrieve(fileCitation.file_id); - formattedLink = `[${textToDisplay}](${citedFile.filename}~~~${ANNOTATION_LINK_TYPE.DASH_DOC})`; - } - } - - console.log(formattedLink); - text = text?.split(subString).join(formattedLink); - console.log(text); - }) - ); - runInAction(() => { - if (this.current_message) this.current_message.text = text || ''; - }); - }; - - @action - goToLinkedDoc = async (link: string) => { - const linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.Document) - .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))) - .map(d => DocCast(d?.annotationOn, d)) - .filter(d => d); - - const linkedDoc = linkedDocs.find(doc => { - const docUrl = CsvCast(doc.data, PDFCast(doc.data)).url.pathname.replace('/files/pdfs/', '').replace('/files/csvs/', ''); - console.log('URL: ' + docUrl + ' Citation URL: ' + link); - return link === docUrl; - }); - - if (linkedDoc) { - await DocumentManager.Instance.showDocument(DocCast(linkedDoc), { willZoomCentered: true }, () => {}); - } - }; - - @action askGPT = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => { event.preventDefault(); + this.inputValue = ''; const textInput = event.currentTarget.elements.namedItem('messageInput') as HTMLInputElement; const trimmedText = textInput.value.trim(); - if (!this.assistantID || !this.threadID) { - try { - await this.createAssistant(); - } catch (err) { - console.error('Error:', err); - } - } - if (trimmedText) { try { textInput.value = ''; runInAction(() => { this.history.push({ role: ASSISTANT_ROLE.USER, text: trimmedText }); }); - await this.runAssistant(trimmedText); - this.dataDoc.data = this.history.toString(); + const { response } = await Networking.PostToServer('/askAgent', { input: trimmedText }); + runInAction(() => { + this.history.push({ role: ASSISTANT_ROLE.ASSISTANT, text: response }); + }); + this.dataDoc.data = JSON.stringify(this.history); } catch (err) { console.error('Error:', err); } } }; - @action - uploadLinks = async (linkedDocs: Doc[]) => { - if (this.isInitializing) { - console.log('Initialization in progress, upload aborted.'); - return; - } - const urls = linkedDocs.map(doc => CsvCast(doc.data, PDFCast(doc.data)).url.pathname); - const csvUrls = urls.filter(url => url.endsWith('.csv')); - console.log(this.assistantID, this.threadID, urls); - - const { openai_file_ids: openaiFileIds } = await Networking.PostToServer('/uploadPDFToVectorStore', { urls, threadID: this.threadID, assistantID: this.assistantID, vector_store_id: this.vectorStoreID }); - - linkedDocs.forEach((doc, i) => { - doc[this.Document[Id] + '_ai_field_id'] = openaiFileIds[i]; - console.log('AI Field ID: ' + openaiFileIds[i]); - }); - - if (csvUrls.length > 0) { - for (let i = 0; i < csvUrls.length; i++) { - this.linkedCsvIDs.push(openaiFileIds[urls.indexOf(csvUrls[i])]); - } - console.log('linked csvs:' + this.linkedCsvIDs); - await this.openai.beta.assistants.update(this.assistantID, { - tools: [{ type: 'file_search' }, { type: 'code_interpreter' }], - tool_resources: { - file_search: { - vector_store_ids: [this.vectorStoreID], - }, - code_interpreter: { - file_ids: this.linkedCsvIDs, - }, - }, - }); - } - }; + // @action + // uploadLinks = async (linkedDocs: Doc[]) => { + // if (this.isInitializing) { + // console.log('Initialization in progress, upload aborted.'); + // return; + // } + // const urls: string[] = linkedDocs.map(doc => CsvCast(doc.data, PDFCast(doc.data)).url.pathname); + // const csvUrls: string[] = urls.filter(url => url.endsWith('.csv')); + // console.log(this.assistantID, this.threadID, urls); + + // await Networking.PostToServer('/uploadPDFs', { file_path: urls[0] }); + + // // linkedDocs.forEach((doc, i) => { + // // doc[this.Document[Id] + '_ai_field_id'] = openaiFileIds[i]; + // // console.log('AI Field ID: ' + openaiFileIds[i]); + // // }); + + // // if (csvUrls.length > 0) { + // // for (let i = 0; i < csvUrls.length; i++) { + // // this.linkedCsvIDs.push(openaiFileIds[urls.indexOf(csvUrls[i])]); + // // } + // // console.log('linked csvs:' + this.linkedCsvIDs); + // // await this.openai.beta.assistants.update(this.assistantID, { + // // tools: [{ type: 'file_search' }, { type: 'code_interpreter' }], + // // tool_resources: { + // // file_search: { + // // vector_store_ids: [this.vectorStoreID], + // // }, + // // code_interpreter: { + // // file_ids: this.linkedCsvIDs, + // // }, + // // }, + // // }); + // // } + // }; downloadToComputer = (url: string, fileName: string) => { fetch(url, { method: 'get', mode: 'no-cors', referrerPolicy: 'no-referrer' }) @@ -555,53 +382,59 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); } + @action + handleFollowUpClick = (question: string) => { + console.log('Follow-up question clicked:', question); + this.inputValue = question; + }; render() { return ( - <MathJaxContext config={this.mathJaxConfig}> - <div className="chatBox"> - {this.isInitializing && <div className="initializing-overlay">Initializing...</div>} - {this.renderModal()} - <div - className="scroll-box chat-content" - ref={r => { - this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); - this._oldWheel = r; - r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); - }}> - <div className="messages"> - {this.history.map((message, index) => ( - <MessageComponent - key={index} - message={message} - toggleToolLogs={this.toggleToolLogs} - expandedLogIndex={this.expandedLogIndex} - index={index} - showModal={this.showModal} - goToLinkedDoc={this.goToLinkedDoc} - setCurrentFile={this.setCurrentFile} - /> - ))} - {!this.current_message ? null : ( - <MessageComponent - key={this.history.length} - message={this.current_message} - toggleToolLogs={this.toggleToolLogs} - expandedLogIndex={this.expandedLogIndex} - index={this.history.length} - showModal={this.showModal} - goToLinkedDoc={this.goToLinkedDoc} - setCurrentFile={this.setCurrentFile} - isCurrent - /> - )} - </div> + /** <MathJaxContext config={this.mathJaxConfig}> **/ + <div className="chatBox"> + {this.isInitializing && <div className="initializing-overlay">Initializing...</div>} + {this.renderModal()} + <div + className="scroll-box chat-content" + ref={r => { + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }}> + <div className="messages"> + {this.history.map((message, index) => ( + <MessageComponent + key={index} + message={message} + toggleToolLogs={this.toggleToolLogs} + expandedLogIndex={this.expandedLogIndex} + index={index} + showModal={this.showModal} + goToLinkedDoc={() => {}} + setCurrentFile={this.setCurrentFile} + onFollowUpClick={this.handleFollowUpClick} + /> + ))} + {!this.current_message ? null : ( + <MessageComponent + key={this.history.length} + message={this.current_message} + toggleToolLogs={this.toggleToolLogs} + expandedLogIndex={this.expandedLogIndex} + index={this.history.length} + showModal={this.showModal} + goToLinkedDoc={() => {}} + setCurrentFile={this.setCurrentFile} + onFollowUpClick={this.handleFollowUpClick} + /> + )} </div> - <form onSubmit={this.askGPT} className="chat-form"> - <input type="text" name="messageInput" autoComplete="off" placeholder="Type a message..." /> - <button type="submit">Send</button> - </form> </div> - </MathJaxContext> + <form onSubmit={this.askGPT} className="chat-form"> + <input type="text" name="messageInput" autoComplete="off" placeholder="Type a message..." value={this.inputValue} onChange={e => (this.inputValue = e.target.value)} /> + <button type="submit">Send</button> + </form> + </div> + /** </MathJaxContext> **/ ); } } diff --git a/src/client/views/nodes/ChatBox/MessageComponent.tsx b/src/client/views/nodes/ChatBox/MessageComponent.tsx index ef6ce83b5..15c0811fb 100644 --- a/src/client/views/nodes/ChatBox/MessageComponent.tsx +++ b/src/client/views/nodes/ChatBox/MessageComponent.tsx @@ -14,24 +14,29 @@ interface MessageComponentProps { showModal: () => void; goToLinkedDoc: (url: string) => void; setCurrentFile: (file: { url: string }) => void; + onFollowUpClick: (question: string) => void; // New prop isCurrent?: boolean; } -const MessageComponent: React.FC<MessageComponentProps> = function ({ message, toggleToolLogs, expandedLogIndex, goToLinkedDoc, index, showModal, setCurrentFile, isCurrent = false }) { - // const messageClass = `${message.role} ${isCurrent ? 'current-message' : ''}`; - +const MessageComponent: React.FC<MessageComponentProps> = function ({ + message, + toggleToolLogs, + expandedLogIndex, + goToLinkedDoc, + index, + showModal, + setCurrentFile, + onFollowUpClick, // New prop + isCurrent = false, +}) { const LinkRenderer = ({ href, children }: { href: string; children: React.ReactNode }) => { - // console.log(href + " " + children) const regex = /([a-zA-Z0-9_.!-]+)~~~(citation|file_path)/; const matches = href.match(regex); - // console.log(href) - // console.log(matches) const url = matches ? matches[1] : href; const linkType = matches ? matches[2] : null; if (linkType === 'citation') { children = <TbInfoCircleFilled />; } - // console.log(linkType) const style = { color: 'lightblue', verticalAlign: linkType === 'citation' ? 'super' : 'baseline', @@ -56,14 +61,38 @@ const MessageComponent: React.FC<MessageComponentProps> = function ({ message, t ); }; + const parseMessage = (text: string) => { + const answerMatch = text.match(/<answer>([\s\S]*?)<\/answer>/); + const followUpMatch = text.match(/<follow_up_question>([\s\S]*?)<\/follow_up_question>/); + + const answer = answerMatch ? answerMatch[1] : text; + const followUpQuestions = followUpMatch + ? followUpMatch[1] + .split('\n') + .filter(q => q.trim()) + .map(q => q.replace(/^\d+\.\s*/, '').trim()) + : []; + + return { answer, followUpQuestions }; + }; + + const { answer, followUpQuestions } = parseMessage(message.text); + console.log('Parsed answer:', answer); + console.log('Parsed follow-up questions:', followUpQuestions); return ( <div className={`message ${message.role}`}> - <MathJaxContext> - <MathJax dynamic hideUntilTypeset="every"> - <ReactMarkdown components={{ a: LinkRenderer }}>{message.text ? message.text : ''}</ReactMarkdown> - </MathJax> - </MathJaxContext> + <ReactMarkdown components={{ a: LinkRenderer }}>{answer}</ReactMarkdown> {message.image && <img src={message.image} alt="" />} + {followUpQuestions.length > 0 && ( + <div className="follow-up-questions"> + <h4>Follow-up Questions:</h4> + {followUpQuestions.map((question, idx) => ( + <button key={idx} className="follow-up-button" onClick={() => onFollowUpClick(question)}> + {question} + </button> + ))} + </div> + )} <div className="message-footer"> {message.tool_logs && ( <button className="toggle-logs-button" onClick={() => toggleToolLogs(index)}> diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts index f2ea83310..f0ca983d7 100644 --- a/src/server/ApiManagers/AssistantManager.ts +++ b/src/server/ApiManagers/AssistantManager.ts @@ -7,6 +7,7 @@ import * as uuid from 'uuid'; import { filesDirectory, publicDirectory } from '../SocketData'; import { Method } from '../RouteManager'; import ApiManager, { Registration } from './ApiManager'; +import axios from 'axios'; export enum Directory { parsed_files = 'parsed_files', @@ -130,5 +131,37 @@ export default class AssistantManager extends ApiManager { } }, }); + + register({ + method: Method.POST, + subscription: '/askAgent', + secureHandler: async ({ req, res }) => { + const { input } = req.body; + + try { + const response = await axios.post('http://localhost:8080/ask', { input }); + res.send({ response: response.data.response }); + } catch (error: any) { + console.error('Error communicating with chatbot:', error); + res.status(500).send({ error: 'Failed to communicate with the chatbot', details: error.message }); + } + }, + }); + // register({ + // method: Method.POST, + // subscription: '/uploadPDF', + // secureHandler: async ({ req, res }) => { + // const { file_path } = req.body; + // const fullPath = path.join(publicDirectory, file_path); + // const fileData = createReadStream(fullPath); + // try { + // const response = await axios.post('http://localhost:8080/uploadPDF', { fileData }); + // res.send({ response: response }); + // } catch (error: any) { + // console.error('Error communicating with chatbot:', error); + // res.status(500).send({ error: 'Failed to communicate with the chatbot', details: error.message }); + // } + // }, + // }); } } |