Compare commits

..

3 Commits

Author SHA1 Message Date
2657334e0c fixing the rebase 2025-09-24 12:02:59 -05:00
b120f85ccd update button based on state 2025-09-24 11:58:38 -05:00
0acfcc0d08 Updated the FE to be able to display warnings and images 2025-09-24 11:57:40 -05:00
4 changed files with 801 additions and 650 deletions

View File

@@ -1,291 +1,361 @@
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<
ConversationPrompt[]
>([]);
const [disableInput, setDisableInput] = useState<boolean>(false);
const messageEndRef = useRef(null); const [subscribe, unsubscribe, socket, sendMessage] =
useContext(WebSocketContext);
const { account, setAccount } = useContext(AccountContext);
const messageRef = useRef("");
const messageResponsePart = useRef(0);
const conversationRef = useRef(conversationDetails);
const [stateMessage, setStateMessage] = useState<string>("");
const selectedConversationRef = useRef<undefined | number>(undefined);
const [conversationDetails, setConversationDetails] = useState<ConversationPrompt[]>([]) useEffect(() => {
const [disableInput, setDisableInput] = useState<boolean>(false); /* register a consistent channel name for identifing this chat messages */
const channelName = `ACCOUNT_ID_${account?.email}`;
const [subscribe, unsubscribe, socket, sendMessage] = useContext(WebSocketContext) /* subscribe to channel and register callback */
const { account, setAccount } = useContext(AccountContext) subscribe(channelName, (message: string) => {
const messageRef = useRef('') /* when a message is received just add it to the UI */
const messageResponsePart = useRef(0);
const conversationRef = useRef(conversationDetails)
const [stateMessage, setStateMessage] = useState<string>('')
const selectedConversationRef = useRef<undefined | number>(undefined)
useEffect(() => { if (message === "END_OF_THE_STREAM_ENDER_GAME_42") {
/* register a consistent channel name for identifing this chat messages */ messageResponsePart.current = 0;
const channelName = `ACCOUNT_ID_${account?.email}`
/* subscribe to channel and register callback */ conversationRef.current.pop();
subscribe(channelName, (message: string) => {
/* when a message is received just add it to the UI */
if (message === 'END_OF_THE_STREAM_ENDER_GAME_42'){ //handleAssistantPrompt({prompt: messageRef.current})
messageResponsePart.current = 0 setConversationDetails([
...conversationRef.current,
conversationRef.current.pop() new ConversationPrompt({
message: `${messageRef.current}`,
//handleAssistantPrompt({prompt: messageRef.current}) user_created: false,
setConversationDetails([...conversationRef.current, new ConversationPrompt({message: `${messageRef.current}`, user_created:false})]) }),
messageRef.current = '' ]);
setStateMessage('') 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) {
try {
//console.log('GetConversationDetails')
//setPromptProcessing(true)
selectedConversationRef.current = selectedConversation;
const { data }: AxiosResponse<ConversationPromptType[]> =
await axiosInstance.get(
`conversation_details?conversation_id=${selectedConversation}`,
);
// send the prompt to be saved const tempConversations: ConversationPrompt[] = data.map(
console.log(fileType) (item) =>
try{ new ConversationPrompt({
const tempConversations: ConversationPrompt[] = [...conversationDetails, new ConversationPrompt({message: prompt, user_created:true}), new ConversationPrompt({message: '', user_created:false})] message: item.message,
conversationRef.current = tempConversations user_created: item.user_created,
setConversationDetails(tempConversations) created_timestamp: item.created_timestamp,
// TODO: add the file here }),
console.log(`Sending message. ${prompt} ${selectedConversation} ${fileType}`) );
sendMessage(prompt, selectedConversation, file, fileType) conversationRef.current = tempConversations;
resetForm(); setConversationDetails(tempConversations);
} finally {
//setPromptProcessing(false)
}catch(e){ }
console.log(`error ${e}`)
// TODO: make this user friendly
}
} }
}
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"
} color={formik.values.file ? "success" : "inherit"}
<div ref={messageEndRef} /> startIcon={<AttachFile />}
role={undefined}
</Box> tabIndex={-1}
>
<VisuallyHiddenInput
</div> type="file"
accept=".csv,.xlsx,.txt"
</div> onChange={(event) => {
const file = event.target.files?.[0];
<div className='card-footer'> //console.log(file)
<Formik if (file) {
initialValues={{ formik.setFieldValue("file", file);
prompt: '', formik.setFieldValue("fileType", file.type);
file: null, } else {
fileType: null, formik.setFieldValue("file", 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 +363,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

View File

@@ -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",
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;
return ( 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;
}
}
} catch {}
console.log(contentToAdd);
<Card sx={{margin: '0.25rem'}}> return (
<CardContent sx={{textAlign: `${text_align}`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}> <Card sx={{ margin: "0.25rem" }}>
<Markdown <CardContent
className='display-linebreak' sx={{
style={{whiteSpace: "pre-line"}} textAlign: `${text_align}`,
color='#F000000' marginRight: "1rem",
>{newMessage}</Markdown> marginLeft: "1rem",
</CardContent> marginTop: "1rem",
</Card> 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; export default ConversationDetailCard;

View File

@@ -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);
};
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 (file){ if (channels.current[chatChannel]) {
/* in chat component the subscribed channel is `MESSAGE_CREATE_${id}` */
channels.current[chatChannel](data);
const reader = new FileReader(); } else {
reader.onload = () => { /* in notifications wrapper the subscribed channel is `MESSAGE_CREATE` */
const base64File = reader.result?.toString().split(',')[1]; console.log("Error");
if (base64File){ // channels.current[type]?.(data)
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')
} }
};
return () => {
ws.current.close();
};
} }
useEffect(() => { }, [account]);
/* WS initialization and cleanup */
if (account){
/* WS provider dom */
//ws.current = new WebSocket(`ws://127.0.0.1:8011/ws/chat_again/`); /* subscribe and unsubscribe are the only required prop for the context */
ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/chat_again/') return (
//ws.current = process.env.REACT_APP_BACKEND_WS_API_BASE_URL; <WebSocketContext.Provider
value={[subscribe, unsubscribe, socket, sendMessage]}
ws.current.onopen = () => { setSocket(ws.current); } >
ws.current.onclose = () => { } {children}
ws.current.onmessage = (message) => { </WebSocketContext.Provider>
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]) {
/* 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 };

View File

@@ -1,64 +1,83 @@
import { Card, CardContent, Divider, InputAdornment, MenuItem, Select, } from "@mui/material" import {
Card,
CardContent,
Divider,
InputAdornment,
MenuItem,
Select,
} from "@mui/material";
import DashboardLayout from "../../ui-kit/examples/LayoutContainers/DashboardLayout" import DashboardLayout from "../../ui-kit/examples/LayoutContainers/DashboardLayout";
import MDBox from "../../ui-kit/components/MDBox" import MDBox from "../../ui-kit/components/MDBox";
import { useMaterialUIController } from "../../ui-kit/context" import { useMaterialUIController } from "../../ui-kit/context";
import { useContext, useEffect, useRef, useState } from "react" import { useContext, useEffect, useRef, useState } from "react";
// Images // Images
import MDTypography from "../../ui-kit/components/MDTypography" import MDTypography from "../../ui-kit/components/MDTypography";
import { CardFooter } from "react-bootstrap" import { CardFooter } from "react-bootstrap";
import MDInput from "../../ui-kit/components/MDInput" import MDInput from "../../ui-kit/components/MDInput";
import { ErrorMessage, Field, Form, Formik } from "formik" import { ErrorMessage, Field, Form, Formik } from "formik";
import MDButton from "../../ui-kit/components/MDButton" import MDButton from "../../ui-kit/components/MDButton";
import { AttachFile, Send } from "@mui/icons-material" import { AttachFile, Send } from "@mui/icons-material";
import Header2 from "../../components/Header2/Header2" import Header2 from "../../components/Header2/Header2";
import DashboardWrapperLayout from "../../components/DashboardWrapperLayout/DashboardWrapperLayout" import DashboardWrapperLayout from "../../components/DashboardWrapperLayout/DashboardWrapperLayout";
import * as Yup from 'yup'; import * as Yup from "yup";
import { Announcement, AnnouncementType, Conversation, ConversationPrompt, ConversationPromptType, ConversationType } from "../../data" import {
import { axiosInstance } from "../../../axiosApi" Announcement,
import { AxiosResponse } from "axios" AnnouncementType,
import { ConversationContext } from "../../contexts/ConversationContext" Conversation,
import Markdown from "markdown-to-jsx" ConversationPrompt,
import ConversationDetailCard from "../../components/ConversationDetailCard/ConversationDetailCard" ConversationPromptType,
import { WebSocketContext } from "../../contexts/WebSocketContext" ConversationType,
import { AccountContext } from "../../contexts/AccountContext" } from "../../data";
import { styled } from '@mui/material/styles'; import { axiosInstance } from "../../../axiosApi";
import Footer from "../../components/Footer/Footer" import { AxiosResponse } from "axios";
import { MessageContext } from "../../contexts/MessageContext" import { ConversationContext } from "../../contexts/ConversationContext";
import CustomSelect, { CustomSelectItem } from "../../components/CustomSelect/CustomSelect" import Markdown from "markdown-to-jsx";
import ConversationDetailCard from "../../components/ConversationDetailCard/ConversationDetailCard";
import { WebSocketContext } from "../../contexts/WebSocketContext";
import { AccountContext } from "../../contexts/AccountContext";
import { styled } from "@mui/material/styles";
import Footer from "../../components/Footer/Footer";
import { MessageContext } from "../../contexts/MessageContext";
import CustomSelect, {
CustomSelectItem,
} from "../../components/CustomSelect/CustomSelect";
const MODELS = ["Turbo","RAG"] const MODELS = ["Turbo", "RAG"];
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,
}); });
@@ -67,22 +86,21 @@ type PromptValues = {
file: Blob | null; file: Blob | null;
fileType: string | null; fileType: string | null;
modelName: string; modelName: string;
}; };
const models: CustomSelectItem[] = [ const models: CustomSelectItem[] = [
{ {
label: "General", label: "General",
value: "GENERAL" value: "GENERAL",
}, },
{ {
label: "Code", label: "Code",
value: "CODE" value: "CODE",
}, },
{ {
label: "Reasoning", label: "Reasoning",
value: "REASONING" value: "REASONING",
} },
]; ];
const AlwaysScrollToBottom = (): JSX.Element => { const AlwaysScrollToBottom = (): JSX.Element => {
@@ -91,191 +109,200 @@ const AlwaysScrollToBottom = (): JSX.Element => {
return <div ref={elementRef} />; return <div ref={elementRef} />;
}; };
const AsyncDashboardInner =({}): JSX.Element => { const AsyncDashboardInner = ({}): JSX.Element => {
const [controller, dispatch] = useMaterialUIController(); const [controller, dispatch] = useMaterialUIController();
const { const { darkMode } = controller;
darkMode, const buttonColor = darkMode ? "dark" : "light";
} = controller; const [announcements, setAnnouncement] = useState<Announcement[]>([]);
const buttonColor = darkMode? 'dark' : 'light'; const [subscribe, unsubscribe, socket, sendMessage] =
const [announcements, setAnnouncement] = useState<Announcement[]>([]); useContext(WebSocketContext);
const [subscribe, unsubscribe, socket, sendMessage] = useContext(WebSocketContext) const { account } = useContext(AccountContext);
const { account } = useContext(AccountContext)
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const {conversations, selectedConversation, setSelectedConversation} = useContext(ConversationContext); const { conversations, selectedConversation, setSelectedConversation } =
useContext(ConversationContext);
const {
conversationDetails,
setConversationDetails,
stateMessage,
isGeneratingMessage,
} = useContext(MessageContext);
const {conversationDetails, setConversationDetails, stateMessage, isGeneratingMessage} = useContext(MessageContext); const conversationRef = useRef(conversationDetails);
const conversationRef = useRef(conversationDetails) async function GetAnnouncements() {
const response: AxiosResponse<AnnouncementType[]> =
await axiosInstance.get("announcment/get/");
setAnnouncement(
response.data.map(
async function GetAnnouncements(){ (status, message) =>
const response: AxiosResponse<AnnouncementType[]> = await axiosInstance.get('announcment/get/') new Announcement({
setAnnouncement(response.data.map((status, message) => new Announcement({
status: status, status: status,
message: message message: message,
}))) }),
),
} );
const conversationTitle = conversations.find(item => item.id === selectedConversation)?.title ?? 'New Conversation';
const colorName = darkMode ? 'light' : 'dark';
const handlePromptSubmit = async ({prompt, file, fileType, modelName}: PromptValues, {resetForm}: any): Promise<void> => {
// send the prompt to be saved
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
sendMessage(prompt, selectedConversation, file, fileType, modelName)
resetForm();
}catch(e){
console.log(`error ${e}`)
// TODO: make this user friendly
}
} }
const conversationTitle =
conversations.find((item) => item.id === selectedConversation)?.title ??
"New Conversation";
const colorName = darkMode ? "light" : "dark";
const handlePromptSubmit = async (
{ prompt, file, fileType, modelName }: PromptValues,
{ resetForm }: any,
): Promise<void> => {
// send the prompt to be saved
try {
const tempConversations: ConversationPrompt[] = [
...conversationDetails,
new ConversationPrompt({ message: prompt, user_created: true }),
new ConversationPrompt({ message: "", user_created: false }),
];
return( conversationRef.current = tempConversations;
<DashboardLayout>
<Header2 />
<MDBox sx={{mt:5}}>
</MDBox>1 setConversationDetails(tempConversations);
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}> // TODO: add the file here
<Card>
<CardContent>
<MDTypography variant="h3" >
{conversationTitle}
</MDTypography> sendMessage(prompt, selectedConversation, file, fileType, modelName);
</CardContent> resetForm();
<Divider /> } catch (e) {
<CardContent> console.log(`error ${e}`);
<> // TODO: make this user friendly
{conversationDetails.length > 0 ? }
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-center' color='inherit'>Either select a previous conversation on start a new one.</Markdown>
}
<AlwaysScrollToBottom/>
</>
</CardContent>
<Divider />
<CardFooter>
<Formik
initialValues={{
prompt: '',
file: null,
fileType: null,
modelName: '',
}}
validationSchema={validationSchema}
onSubmit={handlePromptSubmit}
>
{(formik)=>
<Form>
return (
<DashboardLayout>
<Header2 />
<MDBox sx={{ mt: 5 }}></MDBox>1
<MDBox
sx={{
margin: "0 auto",
width: "80%",
height: "80%",
minHeight: "80%",
maxHeight: "80%",
align: "center",
}}
>
<Card>
<CardContent>
<MDTypography variant="h3">{conversationTitle}</MDTypography>
</CardContent>
<Divider />
<CardContent>
<>
{conversationDetails.length > 0 ? (
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-center" color="inherit">
Either select a previous conversation on start a new one.
</Markdown>
)}
<AlwaysScrollToBottom />
</>
</CardContent>
<Divider />
<CardFooter>
<Formik
initialValues={{
prompt: "",
file: null,
fileType: null,
modelName: "",
}}
validationSchema={validationSchema}
onSubmit={handlePromptSubmit}
>
{(formik) => (
<Form>
<Field
name={"prompt"}
fullWidth
as={MDInput}
label={"Prompt"}
errorstring={<ErrorMessage name={"prompt"} />}
size={"large"}
role={undefined}
tabIndex={-1}
variant={"outlined"}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<MDButton
component="label"
startIcon={<AttachFile />}
role={undefined}
tabIndex={-1}
color={formik.values.file ? "dark" : "light"}
>
<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);
}
}}
/>
</MDButton>
<MDButton
type={"submit"}
startIcon={<Send />}
disabled={!formik.isValid}
color={buttonColor}
>
<></>
</MDButton>
</InputAdornment>
),
}}
></Field>
</Form>
)}
</Formik>
<Field {/* <MDInput
name={"prompt"}
fullWidth
as={MDInput}
label={'Prompt'}
errorstring={<ErrorMessage name={'prompt'}/>}
size={'large'}
role={undefined}
tabIndex={-1}
variant={"outlined"}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<MDButton
component="label"
startIcon={<AttachFile/>}
role={undefined}
tabIndex={-1}
color={buttonColor}
>
<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)
}
}}
/>
</MDButton>
<MDButton
type={'submit'}
startIcon={<Send />}
disabled={!formik.isValid}
color={buttonColor}
>
<></>
</MDButton>
</InputAdornment>
)
}}
>
</Field>
</Form>
}
</Formik>
{/* <MDInput
label="Prompt" label="Prompt"
/> */} /> */}
</CardFooter> </CardFooter>
</Card> </Card>
<Footer /> <Footer />
</MDBox> </MDBox>
</DashboardLayout>
</DashboardLayout> );
) };
}
const AsyncDashboard2 = ({}): JSX.Element => { const AsyncDashboard2 = ({}): JSX.Element => {
return( return (
<DashboardWrapperLayout>
<AsyncDashboardInner />
</DashboardWrapperLayout>
);
};
export default AsyncDashboard2;
<DashboardWrapperLayout>
<AsyncDashboardInner />
</DashboardWrapperLayout>
)
}
export default AsyncDashboard2