Updated the FE to be able to display warnings and images

This commit is contained in:
2025-09-24 11:49:57 -05:00
parent 1306ba9ed1
commit 0acfcc0d08
3 changed files with 549 additions and 426 deletions

View File

@@ -1,144 +1,195 @@
import { Box, Button, InputAdornment, TextField, Typography } from '@mui/material';
import React, {useRef, useContext, useEffect, useState } from 'react';
import { Conversation, ConversationPrompt, ConversationPromptType, 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';
import {
Box,
Button,
InputAdornment,
TextField,
Typography,
} from "@mui/material";
import React, { useRef, useContext, useEffect, useState } from "react";
import {
Conversation,
ConversationPrompt,
ConversationPromptType,
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= {
response: string
index: number
type RenderMessageProps = {
response: string;
index: number;
};
type AsyncChatProps = {
selectedConversation: number | undefined
conversationTitle: string
conversations: Conversation[]
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
setSelectedConversation: React.Dispatch<React.SetStateAction<number | undefined>>
selectedConversation: number | undefined;
conversationTitle: string;
conversations: Conversation[];
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>;
setSelectedConversation: React.Dispatch<
React.SetStateAction<number | undefined>
>;
drawerWidth: number;
}
};
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')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: 'hidden',
position: 'absolute',
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
whiteSpace: "nowrap",
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 [conversationDetails, setConversationDetails] = useState<
ConversationPrompt[]
>([]);
const [disableInput, setDisableInput] = useState<boolean>(false);
const [subscribe, unsubscribe, socket, sendMessage] = useContext(WebSocketContext)
const { account, setAccount } = useContext(AccountContext)
const messageRef = useRef('')
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 conversationRef = useRef(conversationDetails);
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}`
const channelName = `ACCOUNT_ID_${account?.email}`;
/* subscribe to channel and register callback */
subscribe(channelName, (message: string) => {
/* when a message is received just add it to the UI */
if (message === 'END_OF_THE_STREAM_ENDER_GAME_42'){
messageResponsePart.current = 0
if (message === "END_OF_THE_STREAM_ENDER_GAME_42") {
messageResponsePart.current = 0;
conversationRef.current.pop()
conversationRef.current.pop();
//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){
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){
if (!selectedConversation) {
//console.log("we have a new conversation")
setSelectedConversation(Number(message))
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 (messageResponsePart.current === 2){
messageRef.current += message
setStateMessage(messageRef.current)
}
} catch (e) {
// Not a JSON object, treat as raw string.
// contentToAdd is already `message`.
}
})
messageRef.current += contentToAdd;
setStateMessage(messageRef.current);
}
}
});
return () => {
/* unsubscribe from channel during cleanup */
unsubscribe(channelName)
}
}, [account, subscribe, unsubscribe])
unsubscribe(channelName);
};
}, [account, subscribe, unsubscribe]);
async function GetConversationDetails(){
if(selectedConversation){
try{
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 { data }: AxiosResponse<ConversationPromptType[]> =
await axiosInstance.get(
`conversation_details?conversation_id=${selectedConversation}`,
);
const tempConversations: ConversationPrompt[] = data.map((item) => new ConversationPrompt({
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{
created_timestamp: item.created_timestamp,
}),
);
conversationRef.current = tempConversations;
setConversationDetails(tempConversations);
} finally {
//setPromptProcessing(false)
}
}
}
useEffect(() => {
GetConversationDetails();
}, [selectedConversation])
}, [selectedConversation]);
// Function to render each message
const renderMessage = ({response, index}: RenderMessageProps) => (
const renderMessage = ({ response, index }: RenderMessageProps) => (
<div key={index}>
<p>{response}</p>
</div>
@@ -148,47 +199,60 @@ const AsyncChat = ({selectedConversation, conversationTitle, conversations, setC
prompt: string;
file: Blob | null;
fileType: string | null;
};
const handlePromptSubmit = async ({prompt, file, fileType}: PromptValues, {resetForm}: any): Promise<void> => {
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)
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)
console.log(
`Sending message. ${prompt} ${selectedConversation} ${fileType}`,
);
sendMessage(prompt, selectedConversation, file, fileType);
resetForm();
}catch(e){
console.log(`error ${e}`)
} catch (e) {
console.log(`error ${e}`);
// TODO: make this user friendly
}
}
};
return(
return (
<Box
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` }, mt: 5 }}
position='fixed'
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"
>
<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={{
<Box
sx={{
mb: 2,
display: "flex",
flexDirection: "column",
@@ -196,94 +260,99 @@ const AsyncChat = ({selectedConversation, conversationTitle, conversations, setC
overflow: "hidden",
overflowY: "scroll",
//justifyContent="flex-end" //# DO NOT USE THIS WITH 'scroll'
}}>
{selectedConversation ?
}}
>
{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}/>
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>
}
) : (
<Markdown className="text-light text-center">
Either select a previous conversation on start a new one.
</Markdown>
)}
<div ref={messageEndRef} />
</Box>
</div>
</div>
</div>
<div className='card-footer'>
<div className="card-footer">
<Formik
initialValues={{
prompt: '',
prompt: "",
file: null,
fileType: null,
}}
onSubmit={handlePromptSubmit}
validationSchema={validationSchema}
>
{(formik) =>
{(formik) => (
<Form>
<div className='row' style={{
position: 'sticky',
bottom: 0
}}>
<div className='col-12'>
<div
className="row"
style={{
position: "sticky",
bottom: 0,
}}
>
<div className="col-12">
<Field
name={"prompt"}
fullWidth
as={TextField}
label={'Prompt'}
errorstring={<ErrorMessage name={"prompt"}/>}
label={"Prompt"}
errorstring={<ErrorMessage name={"prompt"} />}
size={"small"}
role={undefined}
tabIndex={-1}
margin={"dense"}
variant={"outlined"}
InputProps={{
endAdornment: (
<InputAdornment position='end' >
<InputAdornment position="end">
<Button
component="label"
startIcon={<AttachFile/>}
startIcon={<AttachFile />}
role={undefined}
tabIndex={-1}
>
<VisuallyHiddenInput
type="file"
accept='.csv,.xlsx,.txt'
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)
formik.setFieldValue("file", file);
formik.setFieldValue("fileType", file.type);
} else {
formik.setFieldValue('file',null)
formik.setFieldValue('fileType',null)
formik.setFieldValue("file", null);
formik.setFieldValue("fileType", null);
}
}}
/>
</Button>
<Button
startIcon={<Send />}
type={'submit'}
disabled={!formik.isValid}>
</Button>
type={"submit"}
disabled={!formik.isValid}
></Button>
</InputAdornment>
)
),
}}
>
</Field>
></Field>
</div>
{/* <div className='col-1'>
<Button
@@ -295,17 +364,11 @@ const AsyncChat = ({selectedConversation, conversationTitle, conversations, setC
</div> */}
</div>
</Form>
}
)}
</Formik>
</div>
</Box>
);
};
)
}
export default AsyncChat
export default AsyncChat;

View File

@@ -1,13 +1,7 @@
import { useContext, useEffect, useRef, useState } from "react"
import { WebSocketContext } from "../../contexts/WebSocketContext"
import { AccountContext } from "../../contexts/AccountContext"
import Markdown from "markdown-to-jsx"
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"
import React from "react";
import Markdown from "markdown-to-jsx";
import { Card, CardContent, CircularProgress } from "@mui/material";
import styled from "styled-components";
const StyleDiv = styled.div`
background-color: #f0f0f0;
@@ -26,44 +20,112 @@ const StyleDiv = styled.div`
`;
type ConversationDetailCardProps = {
message: string
user_created: boolean
message: string;
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 => {
const type = user_created ? 'info' : 'dark'
if(user_created){
// Custom component for rendering errors
const MyError = ({ content }: { content: string }) => {
return (
<span style={{ color: "red", fontWeight: "bold" }}>Error: {content}</span>
);
};
}
const text_align = user_created ? 'right' : 'left'
if (message.length === 0){
return(
<Card sx={{margin: '0.25rem'}}>
<CardContent className='card-body-small text-dark ' style={{textAlign: `right`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}>
<CircularProgress color="inherit"/>
const ConversationDetailCard = ({
message,
user_created,
}: ConversationDetailCardProps): JSX.Element => {
const text_align = user_created ? "right" : "left";
if (message.length === 0) {
return (
<Card sx={{ margin: "0.25rem" }}>
<CardContent
className="card-body-small text-dark "
style={{
textAlign: `right`,
marginRight: "1rem",
marginLeft: "1rem",
marginTop: "1rem",
marginBottom: "1rem",
}}
>
<CircularProgress color="inherit" />
</CardContent>
</Card>
)
}else{
const newMessage: string = message.replace("```", "\n```\n");
);
} 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;
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);
return (
<Card sx={{margin: '0.25rem'}}>
<CardContent sx={{textAlign: `${text_align}`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}>
<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"}}
color='#F000000'
>{newMessage}</Markdown>
className="display-linebreak"
style={{ whiteSpace: "pre-line" }}
options={{
overrides: {
plot: {
component: MyPlot,
},
error: {
component: MyError,
},
},
}}
>
{contentToAdd}
</Markdown>
</CardContent>
</Card>
)
);
}
}
};
export default ConversationDetailCard;

View File

@@ -1,39 +1,41 @@
import { AccountContext } from "./AccountContext"
import { AccountContext } from "./AccountContext";
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 }) {
const { authenticated, loading } = useContext(AuthContext);
const ws = useRef(null)
const [socket, setSocket] = useState(null)
const channels = useRef({}) // maps each channel to the callback
const { account, setAccount } = useContext(AccountContext)
const [currentChannel, setCurrentChannel] = useState('')
const ws = useRef(null);
const [socket, setSocket] = useState(null);
const channels = useRef({}); // maps each channel to the callback
const { account, setAccount } = useContext(AccountContext);
const [currentChannel, setCurrentChannel] = useState("");
/* called from a component that registers a callback for a channel */
const subscribe = (channel, callback) => {
//console.log(`Subbing to ${channel}`)
setCurrentChannel(channel)
channels.current[channel] = callback
}
setCurrentChannel(channel);
channels.current[channel] = callback;
};
/* remove callback */
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){
if (socket && socket.readyState === WebSocket.OPEN) {
if (file) {
const reader = new FileReader();
reader.onload = () => {
const base64File = reader.result?.toString().split(',')[1];
if (base64File){
const base64File = reader.result?.toString().split(",")[1];
if (base64File) {
const data = {
message: message,
conversation_id: conversation_id,
@@ -41,13 +43,12 @@ function WebSocketProvider({ children }) {
file: base64File,
fileType: fileType,
modelName: modelName,
};
socket.send(JSON.stringify(data));
}
socket.send(JSON.stringify(data))
}
}
reader.readAsDataURL(file)
}else{
};
reader.readAsDataURL(file);
} else {
const data = {
message: message,
conversation_id: conversation_id,
@@ -55,59 +56,56 @@ function WebSocketProvider({ children }) {
file: null,
fileType: null,
modelName: modelName,
}
};
socket.send(JSON.stringify(data))
socket.send(JSON.stringify(data));
}
//socket.send(`${conversation_id} | ${message}`)
}else{
console.log('Error sending message. WebSocket is not open')
}
} else {
console.log("Error sending message. WebSocket is not open");
}
};
useEffect(() => {
/* 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/')
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;
ws.current.onopen = () => { setSocket(ws.current); }
ws.current.onclose = () => { }
ws.current.onopen = () => {
setSocket(ws.current);
};
ws.current.onclose = () => {};
ws.current.onmessage = (message) => {
const data = message.data
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]
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)
channels.current[chatChannel](data);
} else {
/* in notifications wrapper the subscribed channel is `MESSAGE_CREATE` */
console.log('Error')
console.log("Error");
// channels.current[type]?.(data)
}
};
return () => {
ws.current.close();
};
}
return () => { ws.current.close() }
}
}, [account])
}, [account]);
/* WS provider dom */
/* subscribe and unsubscribe are the only required prop for the context */
return (
<WebSocketContext.Provider value={[subscribe, unsubscribe, socket, sendMessage]}>
<WebSocketContext.Provider
value={[subscribe, unsubscribe, socket, sendMessage]}
>
{children}
</WebSocketContext.Provider>
)
);
}
export { WebSocketContext, WebSocketProvider }
export { WebSocketContext, WebSocketProvider };