Updated the FE to be able to display warnings and images
This commit is contained in:
@@ -1,291 +1,360 @@
|
|||||||
import { Box, Button, InputAdornment, TextField, Typography } from '@mui/material';
|
import {
|
||||||
import React, {useRef, useContext, useEffect, useState } from 'react';
|
Box,
|
||||||
import { Conversation, ConversationPrompt, ConversationPromptType, ConversationType } from '../../data';
|
Button,
|
||||||
import { axiosInstance } from '../../../axiosApi';
|
InputAdornment,
|
||||||
import { AxiosResponse } from 'axios';
|
TextField,
|
||||||
import ConversationDetailCard from '../ConversationDetailCard/ConversationDetailCard';
|
Typography,
|
||||||
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
} from "@mui/material";
|
||||||
import { WebSocketContext } from '../../contexts/WebSocketContext';
|
import React, { useRef, useContext, useEffect, useState } from "react";
|
||||||
import { AccountContext } from '../../contexts/AccountContext';
|
import {
|
||||||
import * as Yup from 'yup';
|
Conversation,
|
||||||
import { styled } from '@mui/material/styles';
|
ConversationPrompt,
|
||||||
import { AttachFile, Send } from '@mui/icons-material';
|
ConversationPromptType,
|
||||||
import Markdown from 'markdown-to-jsx';
|
ConversationType,
|
||||||
|
} from "../../data";
|
||||||
|
import { axiosInstance } from "../../../axiosApi";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import ConversationDetailCard from "../ConversationDetailCard/ConversationDetailCard";
|
||||||
|
import { ErrorMessage, Field, Form, Formik } from "formik";
|
||||||
|
import { WebSocketContext } from "../../contexts/WebSocketContext";
|
||||||
|
import { AccountContext } from "../../contexts/AccountContext";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import { AttachFile, Send } from "@mui/icons-material";
|
||||||
|
import Markdown from "markdown-to-jsx";
|
||||||
|
|
||||||
type RenderMessageProps= {
|
type RenderMessageProps = {
|
||||||
response: string
|
response: string;
|
||||||
index: number
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AsyncChatProps = {
|
type AsyncChatProps = {
|
||||||
selectedConversation: number | undefined
|
selectedConversation: number | undefined;
|
||||||
conversationTitle: string
|
conversationTitle: string;
|
||||||
conversations: Conversation[]
|
conversations: Conversation[];
|
||||||
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
|
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>;
|
||||||
setSelectedConversation: React.Dispatch<React.SetStateAction<number | undefined>>
|
setSelectedConversation: React.Dispatch<
|
||||||
drawerWidth: number;
|
React.SetStateAction<number | undefined>
|
||||||
}
|
>;
|
||||||
|
drawerWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
prompt: Yup.string().min(1, "Need to have at least one character").required("This is requried")
|
prompt: Yup.string()
|
||||||
}
|
.min(1, "Need to have at least one character")
|
||||||
)
|
.required("This is requried"),
|
||||||
|
});
|
||||||
|
|
||||||
const VisuallyHiddenInput = styled('input')({
|
const VisuallyHiddenInput = styled("input")({
|
||||||
clip: 'rect(0 0 0 0)',
|
clip: "rect(0 0 0 0)",
|
||||||
clipPath: 'inset(50%)',
|
clipPath: "inset(50%)",
|
||||||
height: 1,
|
height: 1,
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
width: 1,
|
width: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const AsyncChat = ({selectedConversation, conversationTitle, conversations, setConversations, setSelectedConversation, drawerWidth}: AsyncChatProps): JSX.Element => {
|
const AsyncChat = ({
|
||||||
|
selectedConversation,
|
||||||
|
conversationTitle,
|
||||||
|
conversations,
|
||||||
|
setConversations,
|
||||||
|
setSelectedConversation,
|
||||||
|
drawerWidth,
|
||||||
|
}: AsyncChatProps): JSX.Element => {
|
||||||
|
const messageEndRef = useRef(null);
|
||||||
|
|
||||||
|
const [conversationDetails, setConversationDetails] = useState<
|
||||||
const messageEndRef = useRef(null);
|
ConversationPrompt[]
|
||||||
|
>([]);
|
||||||
const [conversationDetails, setConversationDetails] = useState<ConversationPrompt[]>([])
|
const [disableInput, setDisableInput] = useState<boolean>(false);
|
||||||
const [disableInput, setDisableInput] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [subscribe, unsubscribe, socket, sendMessage] = useContext(WebSocketContext)
|
const [subscribe, unsubscribe, socket, sendMessage] =
|
||||||
const { account, setAccount } = useContext(AccountContext)
|
useContext(WebSocketContext);
|
||||||
const messageRef = useRef('')
|
const { account, setAccount } = useContext(AccountContext);
|
||||||
const messageResponsePart = useRef(0);
|
const messageRef = useRef("");
|
||||||
const conversationRef = useRef(conversationDetails)
|
const messageResponsePart = useRef(0);
|
||||||
const [stateMessage, setStateMessage] = useState<string>('')
|
const conversationRef = useRef(conversationDetails);
|
||||||
const selectedConversationRef = useRef<undefined | number>(undefined)
|
const [stateMessage, setStateMessage] = useState<string>("");
|
||||||
|
const selectedConversationRef = useRef<undefined | number>(undefined);
|
||||||
useEffect(() => {
|
|
||||||
/* register a consistent channel name for identifing this chat messages */
|
|
||||||
const channelName = `ACCOUNT_ID_${account?.email}`
|
|
||||||
|
|
||||||
/* subscribe to channel and register callback */
|
useEffect(() => {
|
||||||
subscribe(channelName, (message: string) => {
|
/* register a consistent channel name for identifing this chat messages */
|
||||||
/* when a message is received just add it to the UI */
|
const channelName = `ACCOUNT_ID_${account?.email}`;
|
||||||
|
|
||||||
if (message === 'END_OF_THE_STREAM_ENDER_GAME_42'){
|
/* subscribe to channel and register callback */
|
||||||
messageResponsePart.current = 0
|
subscribe(channelName, (message: string) => {
|
||||||
|
/* when a message is received just add it to the UI */
|
||||||
conversationRef.current.pop()
|
|
||||||
|
if (message === "END_OF_THE_STREAM_ENDER_GAME_42") {
|
||||||
//handleAssistantPrompt({prompt: messageRef.current})
|
messageResponsePart.current = 0;
|
||||||
setConversationDetails([...conversationRef.current, new ConversationPrompt({message: `${messageRef.current}`, user_created:false})])
|
|
||||||
messageRef.current = ''
|
conversationRef.current.pop();
|
||||||
setStateMessage('')
|
|
||||||
|
//handleAssistantPrompt({prompt: messageRef.current})
|
||||||
|
setConversationDetails([
|
||||||
|
...conversationRef.current,
|
||||||
|
new ConversationPrompt({
|
||||||
|
message: `${messageRef.current}`,
|
||||||
|
user_created: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
messageRef.current = "";
|
||||||
|
setStateMessage("");
|
||||||
|
} else if (message === "START_OF_THE_STREAM_ENDER_GAME_42") {
|
||||||
|
messageResponsePart.current = 2;
|
||||||
|
} else if (message === "CONVERSATION_ID") {
|
||||||
|
messageResponsePart.current = 1;
|
||||||
|
} else {
|
||||||
|
if (messageResponsePart.current === 1) {
|
||||||
|
// this has to do with the conversation id
|
||||||
|
if (!selectedConversation) {
|
||||||
|
//console.log("we have a new conversation")
|
||||||
|
setSelectedConversation(Number(message));
|
||||||
|
}
|
||||||
|
} else if (messageResponsePart.current === 2) {
|
||||||
|
let contentToAdd = message;
|
||||||
|
try {
|
||||||
|
const parsedMessage = JSON.parse(message);
|
||||||
|
if (
|
||||||
|
parsedMessage &&
|
||||||
|
typeof parsedMessage === "object" &&
|
||||||
|
parsedMessage.type
|
||||||
|
) {
|
||||||
|
switch (parsedMessage.type) {
|
||||||
|
case "text":
|
||||||
|
contentToAdd = parsedMessage.content;
|
||||||
|
break;
|
||||||
|
case "plot":
|
||||||
|
contentToAdd = `<plot format="${parsedMessage.format}" image="${parsedMessage.image}"></plot>`;
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
contentToAdd = `<error content="${parsedMessage.content}"></error>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// contentToAdd is already `message`
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (message === 'START_OF_THE_STREAM_ENDER_GAME_42'){
|
} catch (e) {
|
||||||
messageResponsePart.current = 2
|
// Not a JSON object, treat as raw string.
|
||||||
|
// contentToAdd is already `message`.
|
||||||
|
}
|
||||||
|
|
||||||
}else if (message === 'CONVERSATION_ID'){
|
messageRef.current += contentToAdd;
|
||||||
messageResponsePart.current = 1
|
setStateMessage(messageRef.current);
|
||||||
}else{
|
|
||||||
if (messageResponsePart.current === 1){
|
|
||||||
// this has to do with the conversation id
|
|
||||||
if(!selectedConversation){
|
|
||||||
//console.log("we have a new conversation")
|
|
||||||
setSelectedConversation(Number(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (messageResponsePart.current === 2){
|
|
||||||
messageRef.current += message
|
|
||||||
setStateMessage(messageRef.current)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
/* unsubscribe from channel during cleanup */
|
|
||||||
unsubscribe(channelName)
|
|
||||||
}
|
}
|
||||||
}, [account, subscribe, unsubscribe])
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function GetConversationDetails(){
|
|
||||||
if(selectedConversation){
|
|
||||||
|
|
||||||
try{
|
|
||||||
//console.log('GetConversationDetails')
|
|
||||||
//setPromptProcessing(true)
|
|
||||||
selectedConversationRef.current = selectedConversation;
|
|
||||||
const {data, }: AxiosResponse<ConversationPromptType[]> = await axiosInstance.get(`conversation_details?conversation_id=${selectedConversation}`)
|
|
||||||
|
|
||||||
const tempConversations: ConversationPrompt[] = data.map((item) => new ConversationPrompt({
|
|
||||||
message: item.message,
|
|
||||||
user_created: item.user_created,
|
|
||||||
created_timestamp: item.created_timestamp
|
|
||||||
}))
|
|
||||||
conversationRef.current = tempConversations
|
|
||||||
setConversationDetails(tempConversations)
|
|
||||||
|
|
||||||
}finally{
|
|
||||||
//setPromptProcessing(false)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
GetConversationDetails();
|
|
||||||
}, [selectedConversation])
|
|
||||||
|
|
||||||
|
|
||||||
// Function to render each message
|
|
||||||
|
|
||||||
const renderMessage = ({response, index}: RenderMessageProps) => (
|
|
||||||
<div key={index}>
|
|
||||||
<p>{response}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type PromptValues = {
|
|
||||||
prompt: string;
|
|
||||||
file: Blob | null;
|
|
||||||
fileType: string | null;
|
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
/* unsubscribe from channel during cleanup */
|
||||||
|
unsubscribe(channelName);
|
||||||
};
|
};
|
||||||
|
}, [account, subscribe, unsubscribe]);
|
||||||
|
|
||||||
const handlePromptSubmit = async ({prompt, file, fileType}: PromptValues, {resetForm}: any): Promise<void> => {
|
async function GetConversationDetails() {
|
||||||
|
if (selectedConversation) {
|
||||||
// send the prompt to be saved
|
try {
|
||||||
console.log(fileType)
|
//console.log('GetConversationDetails')
|
||||||
try{
|
//setPromptProcessing(true)
|
||||||
const tempConversations: ConversationPrompt[] = [...conversationDetails, new ConversationPrompt({message: prompt, user_created:true}), new ConversationPrompt({message: '', user_created:false})]
|
selectedConversationRef.current = selectedConversation;
|
||||||
conversationRef.current = tempConversations
|
const { data }: AxiosResponse<ConversationPromptType[]> =
|
||||||
setConversationDetails(tempConversations)
|
await axiosInstance.get(
|
||||||
// TODO: add the file here
|
`conversation_details?conversation_id=${selectedConversation}`,
|
||||||
console.log(`Sending message. ${prompt} ${selectedConversation} ${fileType}`)
|
);
|
||||||
sendMessage(prompt, selectedConversation, file, fileType)
|
|
||||||
resetForm();
|
const tempConversations: ConversationPrompt[] = data.map(
|
||||||
|
(item) =>
|
||||||
|
new ConversationPrompt({
|
||||||
}catch(e){
|
message: item.message,
|
||||||
console.log(`error ${e}`)
|
user_created: item.user_created,
|
||||||
// TODO: make this user friendly
|
created_timestamp: item.created_timestamp,
|
||||||
}
|
}),
|
||||||
|
);
|
||||||
|
conversationRef.current = tempConversations;
|
||||||
|
setConversationDetails(tempConversations);
|
||||||
|
} finally {
|
||||||
|
//setPromptProcessing(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
GetConversationDetails();
|
||||||
|
}, [selectedConversation]);
|
||||||
|
|
||||||
return(
|
// Function to render each message
|
||||||
<Box
|
|
||||||
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` },
|
const renderMessage = ({ response, index }: RenderMessageProps) => (
|
||||||
ml: { sm: `${drawerWidth}px` }, mt: 5 }}
|
<div key={index}>
|
||||||
position='fixed'
|
<p>{response}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type PromptValues = {
|
||||||
|
prompt: string;
|
||||||
|
file: Blob | null;
|
||||||
|
fileType: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePromptSubmit = async (
|
||||||
|
{ prompt, file, fileType }: PromptValues,
|
||||||
|
{ resetForm }: any,
|
||||||
|
): Promise<void> => {
|
||||||
|
// send the prompt to be saved
|
||||||
|
console.log(fileType);
|
||||||
|
try {
|
||||||
|
const tempConversations: ConversationPrompt[] = [
|
||||||
|
...conversationDetails,
|
||||||
|
new ConversationPrompt({ message: prompt, user_created: true }),
|
||||||
|
new ConversationPrompt({ message: "", user_created: false }),
|
||||||
|
];
|
||||||
|
conversationRef.current = tempConversations;
|
||||||
|
setConversationDetails(tempConversations);
|
||||||
|
// TODO: add the file here
|
||||||
|
console.log(
|
||||||
|
`Sending message. ${prompt} ${selectedConversation} ${fileType}`,
|
||||||
|
);
|
||||||
|
sendMessage(prompt, selectedConversation, file, fileType);
|
||||||
|
resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`error ${e}`);
|
||||||
|
// TODO: make this user friendly
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
p: 3,
|
||||||
|
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||||
|
ml: { sm: `${drawerWidth}px` },
|
||||||
|
mt: 5,
|
||||||
|
}}
|
||||||
|
position="fixed"
|
||||||
|
>
|
||||||
|
<div className="card" style={{ height: "auto" }}>
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="bg-gradient-dark shadow-dark border-radius-lg py-3 pe-1">
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
style={{ flexGrow: 1, marginLeft: "1rem" }}
|
||||||
|
className="text-white font-weight-bold"
|
||||||
|
>
|
||||||
|
{conversationTitle}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body bg-gradient-dark border-radius-lg py-3 pe-1">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: 700,
|
||||||
|
overflow: "hidden",
|
||||||
|
overflowY: "scroll",
|
||||||
|
//justifyContent="flex-end" //# DO NOT USE THIS WITH 'scroll'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedConversation ? (
|
||||||
|
conversationDetails.map((convo_detail) =>
|
||||||
|
convo_detail.message.length > 0 ? (
|
||||||
|
<ConversationDetailCard
|
||||||
|
message={convo_detail.message}
|
||||||
|
user_created={convo_detail.user_created}
|
||||||
|
key={convo_detail.id}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ConversationDetailCard
|
||||||
|
message={stateMessage}
|
||||||
|
user_created={convo_detail.user_created}
|
||||||
|
key={convo_detail.id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Markdown className="text-light text-center">
|
||||||
|
Either select a previous conversation on start a new one.
|
||||||
|
</Markdown>
|
||||||
|
)}
|
||||||
|
<div ref={messageEndRef} />
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-footer">
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
prompt: "",
|
||||||
|
file: null,
|
||||||
|
fileType: null,
|
||||||
|
}}
|
||||||
|
onSubmit={handlePromptSubmit}
|
||||||
|
validationSchema={validationSchema}
|
||||||
>
|
>
|
||||||
<div className="card" style= {{height: 'auto'}}>
|
{(formik) => (
|
||||||
<div className='card-header'>
|
<Form>
|
||||||
|
<div
|
||||||
<div className='bg-gradient-dark shadow-dark border-radius-lg py-3 pe-1'>
|
className="row"
|
||||||
<Typography variant="h6" style={{ flexGrow: 1, marginLeft: '1rem' }} className='text-white font-weight-bold'>
|
style={{
|
||||||
{conversationTitle}
|
position: "sticky",
|
||||||
</Typography>
|
bottom: 0,
|
||||||
</div>
|
}}
|
||||||
</div>
|
>
|
||||||
<div className="card-body bg-gradient-dark border-radius-lg py-3 pe-1">
|
<div className="col-12">
|
||||||
<Box sx={{
|
<Field
|
||||||
mb: 2,
|
name={"prompt"}
|
||||||
display: "flex",
|
fullWidth
|
||||||
flexDirection: "column",
|
as={TextField}
|
||||||
height: 700,
|
label={"Prompt"}
|
||||||
overflow: "hidden",
|
errorstring={<ErrorMessage name={"prompt"} />}
|
||||||
overflowY: "scroll",
|
size={"small"}
|
||||||
//justifyContent="flex-end" //# DO NOT USE THIS WITH 'scroll'
|
role={undefined}
|
||||||
}}>
|
tabIndex={-1}
|
||||||
{selectedConversation ?
|
margin={"dense"}
|
||||||
conversationDetails.map((convo_detail) =>
|
variant={"outlined"}
|
||||||
convo_detail.message.length >0 ?
|
InputProps={{
|
||||||
<ConversationDetailCard message={convo_detail.message} user_created={convo_detail.user_created} key={convo_detail.id}/> : <ConversationDetailCard message={stateMessage} user_created={convo_detail.user_created} key={convo_detail.id}/>
|
endAdornment: (
|
||||||
)
|
<InputAdornment position="end">
|
||||||
:
|
<Button
|
||||||
<Markdown className='text-light text-center'>Either select a previous conversation on start a new one.</Markdown>
|
component="label"
|
||||||
}
|
startIcon={<AttachFile />}
|
||||||
<div ref={messageEndRef} />
|
role={undefined}
|
||||||
|
tabIndex={-1}
|
||||||
</Box>
|
>
|
||||||
|
<VisuallyHiddenInput
|
||||||
|
type="file"
|
||||||
</div>
|
accept=".csv,.xlsx,.txt"
|
||||||
|
onChange={(event) => {
|
||||||
</div>
|
const file = event.target.files?.[0];
|
||||||
|
//console.log(file)
|
||||||
<div className='card-footer'>
|
if (file) {
|
||||||
<Formik
|
formik.setFieldValue("file", file);
|
||||||
initialValues={{
|
formik.setFieldValue("fileType", file.type);
|
||||||
prompt: '',
|
} else {
|
||||||
file: null,
|
formik.setFieldValue("file", null);
|
||||||
fileType: null,
|
formik.setFieldValue("fileType", null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<Send />}
|
||||||
|
type={"submit"}
|
||||||
|
disabled={!formik.isValid}
|
||||||
|
></Button>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
onSubmit={handlePromptSubmit}
|
></Field>
|
||||||
validationSchema={validationSchema}
|
</div>
|
||||||
>
|
{/* <div className='col-1'>
|
||||||
{(formik) =>
|
|
||||||
<Form>
|
|
||||||
<div className='row' style={{
|
|
||||||
position: 'sticky',
|
|
||||||
bottom: 0
|
|
||||||
}}>
|
|
||||||
<div className='col-12'>
|
|
||||||
<Field
|
|
||||||
name={"prompt"}
|
|
||||||
fullWidth
|
|
||||||
as={TextField}
|
|
||||||
label={'Prompt'}
|
|
||||||
errorstring={<ErrorMessage name={"prompt"}/>}
|
|
||||||
size={"small"}
|
|
||||||
role={undefined}
|
|
||||||
tabIndex={-1}
|
|
||||||
|
|
||||||
|
|
||||||
margin={"dense"}
|
|
||||||
variant={"outlined"}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position='end' >
|
|
||||||
|
|
||||||
<Button
|
|
||||||
component="label"
|
|
||||||
startIcon={<AttachFile/>}
|
|
||||||
role={undefined}
|
|
||||||
tabIndex={-1}
|
|
||||||
|
|
||||||
>
|
|
||||||
<VisuallyHiddenInput
|
|
||||||
type="file"
|
|
||||||
accept='.csv,.xlsx,.txt'
|
|
||||||
|
|
||||||
onChange={(event) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
//console.log(file)
|
|
||||||
if (file) {
|
|
||||||
formik.setFieldValue('file',file)
|
|
||||||
formik.setFieldValue('fileType',file.type)
|
|
||||||
} else {
|
|
||||||
formik.setFieldValue('file',null)
|
|
||||||
formik.setFieldValue('fileType',null)
|
|
||||||
}
|
|
||||||
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<Send />}
|
|
||||||
type={'submit'}
|
|
||||||
disabled={!formik.isValid}>
|
|
||||||
|
|
||||||
</Button>
|
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
{/* <div className='col-1'>
|
|
||||||
<Button
|
<Button
|
||||||
type={'submit'}
|
type={'submit'}
|
||||||
// disabled={selectedConversation === undefined ? true : false || !formik.isValid}
|
// disabled={selectedConversation === undefined ? true : false || !formik.isValid}
|
||||||
@@ -293,19 +362,13 @@ const AsyncChat = ({selectedConversation, conversationTitle, conversations, setC
|
|||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
</Formik>
|
export default AsyncChat;
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AsyncChat
|
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { useContext, useEffect, useRef, useState } from "react"
|
import React from "react";
|
||||||
import { WebSocketContext } from "../../contexts/WebSocketContext"
|
import Markdown from "markdown-to-jsx";
|
||||||
import { AccountContext } from "../../contexts/AccountContext"
|
import { Card, CardContent, CircularProgress } from "@mui/material";
|
||||||
import Markdown from "markdown-to-jsx"
|
import styled from "styled-components";
|
||||||
import { Card, CardContent, CircularProgress } from "@mui/material"
|
|
||||||
import MDTypography from "../../ui-kit/components/MDTypography"
|
|
||||||
import styled from 'styled-components';
|
|
||||||
// import { CodeBlock } from "react-code-blocks"
|
|
||||||
// import CustomCodeBlock from "../CustomPreBlock/CustomPreBlock"
|
|
||||||
// import CustomPreBlock from "../CustomPreBlock/CustomPreBlock"
|
|
||||||
|
|
||||||
const StyleDiv = styled.div`
|
const StyleDiv = styled.div`
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
@@ -26,44 +20,112 @@ const StyleDiv = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type ConversationDetailCardProps = {
|
type ConversationDetailCardProps = {
|
||||||
message: string
|
message: string;
|
||||||
user_created: boolean
|
user_created: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
}
|
// Custom component for rendering plots
|
||||||
|
const MyPlot = ({ format, image }: { format: string; image: string }) => {
|
||||||
|
const imageSrc = `data:image/${format};base64,${image}`;
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
style={{ maxWidth: "100%", height: "auto" }}
|
||||||
|
alt="plot"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ConversationDetailCard = ({message, user_created}: ConversationDetailCardProps): JSX.Element => {
|
// Custom component for rendering errors
|
||||||
const type = user_created ? 'info' : 'dark'
|
const MyError = ({ content }: { content: string }) => {
|
||||||
if(user_created){
|
return (
|
||||||
|
<span style={{ color: "red", fontWeight: "bold" }}>Error: {content}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
const ConversationDetailCard = ({
|
||||||
const text_align = user_created ? 'right' : 'left'
|
message,
|
||||||
if (message.length === 0){
|
user_created,
|
||||||
return(
|
}: ConversationDetailCardProps): JSX.Element => {
|
||||||
<Card sx={{margin: '0.25rem'}}>
|
const text_align = user_created ? "right" : "left";
|
||||||
<CardContent className='card-body-small text-dark ' style={{textAlign: `right`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}>
|
if (message.length === 0) {
|
||||||
<CircularProgress color="inherit"/>
|
return (
|
||||||
</CardContent>
|
<Card sx={{ margin: "0.25rem" }}>
|
||||||
</Card>
|
<CardContent
|
||||||
)
|
className="card-body-small text-dark "
|
||||||
}else{
|
style={{
|
||||||
const newMessage: string = message.replace("```", "\n```\n");
|
textAlign: `right`,
|
||||||
|
marginRight: "1rem",
|
||||||
return (
|
marginLeft: "1rem",
|
||||||
|
marginTop: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let contentToAdd = message;
|
||||||
|
console.log(contentToAdd);
|
||||||
|
try {
|
||||||
|
const parsedMessage = JSON.parse(message);
|
||||||
|
if (
|
||||||
|
parsedMessage &&
|
||||||
|
typeof parsedMessage === "object" &&
|
||||||
|
parsedMessage.type
|
||||||
|
) {
|
||||||
|
switch (parsedMessage.type) {
|
||||||
|
case "text":
|
||||||
|
contentToAdd = parsedMessage.content;
|
||||||
|
|
||||||
|
break;
|
||||||
<Card sx={{margin: '0.25rem'}}>
|
case "plot":
|
||||||
<CardContent sx={{textAlign: `${text_align}`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}>
|
contentToAdd = `<plot format="${parsedMessage.format}" image="${parsedMessage.image}"></plot>`;
|
||||||
<Markdown
|
break;
|
||||||
className='display-linebreak'
|
case "error":
|
||||||
style={{whiteSpace: "pre-line"}}
|
contentToAdd = `<error content="${parsedMessage.content}"></error>`;
|
||||||
color='#F000000'
|
break;
|
||||||
>{newMessage}</Markdown>
|
default:
|
||||||
</CardContent>
|
// contentToAdd is already `message`
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
|
console.log(contentToAdd);
|
||||||
|
|
||||||
export default ConversationDetailCard;
|
return (
|
||||||
|
<Card sx={{ margin: "0.25rem" }}>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
textAlign: `${text_align}`,
|
||||||
|
marginRight: "1rem",
|
||||||
|
marginLeft: "1rem",
|
||||||
|
marginTop: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Markdown
|
||||||
|
className="display-linebreak"
|
||||||
|
style={{ whiteSpace: "pre-line" }}
|
||||||
|
options={{
|
||||||
|
overrides: {
|
||||||
|
plot: {
|
||||||
|
component: MyPlot,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
component: MyError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contentToAdd}
|
||||||
|
</Markdown>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConversationDetailCard;
|
||||||
|
|||||||
@@ -1,113 +1,111 @@
|
|||||||
import { AccountContext } from "./AccountContext"
|
import { AccountContext } from "./AccountContext";
|
||||||
import { AuthContext } from "./AuthContext";
|
import { AuthContext } from "./AuthContext";
|
||||||
|
|
||||||
const { useEffect, createContext, useRef, useState, useContext } = require("react")
|
const {
|
||||||
|
useEffect,
|
||||||
|
createContext,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
} = require("react");
|
||||||
|
|
||||||
|
const WebSocketContext = createContext();
|
||||||
const WebSocketContext = createContext()
|
|
||||||
|
|
||||||
function WebSocketProvider({ children }) {
|
function WebSocketProvider({ children }) {
|
||||||
const { authenticated, loading } = useContext(AuthContext);
|
const { authenticated, loading } = useContext(AuthContext);
|
||||||
const ws = useRef(null)
|
const ws = useRef(null);
|
||||||
const [socket, setSocket] = useState(null)
|
const [socket, setSocket] = useState(null);
|
||||||
const channels = useRef({}) // maps each channel to the callback
|
const channels = useRef({}); // maps each channel to the callback
|
||||||
const { account, setAccount } = useContext(AccountContext)
|
const { account, setAccount } = useContext(AccountContext);
|
||||||
const [currentChannel, setCurrentChannel] = useState('')
|
const [currentChannel, setCurrentChannel] = useState("");
|
||||||
/* called from a component that registers a callback for a channel */
|
/* called from a component that registers a callback for a channel */
|
||||||
const subscribe = (channel, callback) => {
|
const subscribe = (channel, callback) => {
|
||||||
//console.log(`Subbing to ${channel}`)
|
//console.log(`Subbing to ${channel}`)
|
||||||
setCurrentChannel(channel)
|
setCurrentChannel(channel);
|
||||||
channels.current[channel] = callback
|
channels.current[channel] = callback;
|
||||||
}
|
};
|
||||||
/* remove callback */
|
/* remove callback */
|
||||||
const unsubscribe = (channel) => {
|
const unsubscribe = (channel) => {
|
||||||
delete channels.current[channel]
|
delete channels.current[channel];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = (message, conversation_id, file, fileType, modelName) => {
|
||||||
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64File = reader.result?.toString().split(",")[1];
|
||||||
|
if (base64File) {
|
||||||
|
const data = {
|
||||||
|
message: message,
|
||||||
|
conversation_id: conversation_id,
|
||||||
|
email: account?.email,
|
||||||
|
file: base64File,
|
||||||
|
fileType: fileType,
|
||||||
|
modelName: modelName,
|
||||||
|
};
|
||||||
|
socket.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
const data = {
|
||||||
|
message: message,
|
||||||
|
conversation_id: conversation_id,
|
||||||
|
email: account?.email,
|
||||||
|
file: null,
|
||||||
|
fileType: null,
|
||||||
|
modelName: modelName,
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
//socket.send(`${conversation_id} | ${message}`)
|
||||||
|
} else {
|
||||||
|
console.log("Error sending message. WebSocket is not open");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
/* WS initialization and cleanup */
|
||||||
|
if (account) {
|
||||||
|
ws.current = new WebSocket(`ws://localhost:8011/ws/chat_again/`);
|
||||||
|
//ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/chat_again/')
|
||||||
|
//ws.current = process.env.REACT_APP_BACKEND_WS_API_BASE_URL;
|
||||||
|
|
||||||
const sendMessage = (message, conversation_id, file, fileType, modelName) => {
|
ws.current.onopen = () => {
|
||||||
if (socket && socket.readyState === WebSocket.OPEN){
|
setSocket(ws.current);
|
||||||
|
};
|
||||||
if (file){
|
ws.current.onclose = () => {};
|
||||||
|
ws.current.onmessage = (message) => {
|
||||||
|
const data = message.data;
|
||||||
|
// lookup for an existing chat in which this message belongs
|
||||||
|
// if no chat is subscribed send message to generic channel
|
||||||
|
const chatChannel = Object.entries(channels.current)[0][0];
|
||||||
|
|
||||||
|
if (channels.current[chatChannel]) {
|
||||||
const reader = new FileReader();
|
/* in chat component the subscribed channel is `MESSAGE_CREATE_${id}` */
|
||||||
reader.onload = () => {
|
channels.current[chatChannel](data);
|
||||||
const base64File = reader.result?.toString().split(',')[1];
|
} else {
|
||||||
if (base64File){
|
/* in notifications wrapper the subscribed channel is `MESSAGE_CREATE` */
|
||||||
const data = {
|
console.log("Error");
|
||||||
message: message,
|
// channels.current[type]?.(data)
|
||||||
conversation_id: conversation_id,
|
|
||||||
email: account?.email,
|
|
||||||
file: base64File,
|
|
||||||
fileType: fileType,
|
|
||||||
modelName: modelName,
|
|
||||||
}
|
|
||||||
socket.send(JSON.stringify(data))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}else{
|
|
||||||
const data = {
|
|
||||||
message: message,
|
|
||||||
conversation_id: conversation_id,
|
|
||||||
email: account?.email,
|
|
||||||
file: null,
|
|
||||||
fileType: null,
|
|
||||||
modelName: modelName,
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.send(JSON.stringify(data))
|
|
||||||
}
|
|
||||||
//socket.send(`${conversation_id} | ${message}`)
|
|
||||||
|
|
||||||
}else{
|
|
||||||
console.log('Error sending message. WebSocket is not open')
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
ws.current.close();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
}, [account]);
|
||||||
/* WS initialization and cleanup */
|
|
||||||
if (account){
|
|
||||||
|
|
||||||
|
|
||||||
//ws.current = new WebSocket(`ws://127.0.0.1:8011/ws/chat_again/`);
|
|
||||||
ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/chat_again/')
|
|
||||||
//ws.current = process.env.REACT_APP_BACKEND_WS_API_BASE_URL;
|
|
||||||
|
|
||||||
ws.current.onopen = () => { setSocket(ws.current); }
|
/* WS provider dom */
|
||||||
ws.current.onclose = () => { }
|
/* subscribe and unsubscribe are the only required prop for the context */
|
||||||
ws.current.onmessage = (message) => {
|
return (
|
||||||
const data = message.data
|
<WebSocketContext.Provider
|
||||||
// lookup for an existing chat in which this message belongs
|
value={[subscribe, unsubscribe, socket, sendMessage]}
|
||||||
// if no chat is subscribed send message to generic channel
|
>
|
||||||
const chatChannel = Object.entries(channels.current)[0][0]
|
{children}
|
||||||
|
</WebSocketContext.Provider>
|
||||||
if (channels.current[chatChannel]) {
|
);
|
||||||
/* in chat component the subscribed channel is `MESSAGE_CREATE_${id}` */
|
|
||||||
channels.current[chatChannel](data)
|
|
||||||
} else {
|
|
||||||
/* in notifications wrapper the subscribed channel is `MESSAGE_CREATE` */
|
|
||||||
console.log('Error')
|
|
||||||
// channels.current[type]?.(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => { ws.current.close() }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}, [account])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* WS provider dom */
|
|
||||||
/* subscribe and unsubscribe are the only required prop for the context */
|
|
||||||
return (
|
|
||||||
<WebSocketContext.Provider value={[subscribe, unsubscribe, socket, sendMessage]}>
|
|
||||||
{children}
|
|
||||||
</WebSocketContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { WebSocketContext, WebSocketProvider }
|
export { WebSocketContext, WebSocketProvider };
|
||||||
|
|||||||
Reference in New Issue
Block a user