309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
import { useState, useRef } from 'react';
|
|
import Divider from '@mui/material/Divider';
|
|
import Accordion from '@mui/material/Accordion';
|
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
|
import Card from '@mui/material/Card';
|
|
import Table from '@mui/material/Table';
|
|
import TableBody from '@mui/material/TableBody';
|
|
import TableCell from '@mui/material/TableCell';
|
|
import TableContainer from '@mui/material/TableContainer';
|
|
import TableHead from '@mui/material/TableHead';
|
|
import TableRow from '@mui/material/TableRow';
|
|
import Box from '@mui/material/Box';
|
|
import Button from '@mui/material/Button';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import CardContent from '@mui/material/CardContent';
|
|
import CardActions from '@mui/material/CardActions';
|
|
import Collapse from '@mui/material/Collapse';
|
|
import Typography from '@mui/material/Typography';
|
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
import { ExpandMore } from './ExpandMore';
|
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
import CheckIcon from '@mui/icons-material/Check';
|
|
|
|
import { ChatBubble } from './ChatBubble';
|
|
import { StyledMarkdown } from './StyledMarkdown';
|
|
import { Tooltip } from '@mui/material';
|
|
|
|
import { VectorVisualizer } from './VectorVisualizer';
|
|
import { SeverityType } from './Snack';
|
|
|
|
type MessageRoles = 'info' | 'user' | 'assistant' | 'system';
|
|
|
|
type MessageData = {
|
|
role: MessageRoles,
|
|
content: string,
|
|
user?: string,
|
|
type?: string,
|
|
id?: string,
|
|
isProcessing?: boolean,
|
|
metadata?: MessageMetaProps
|
|
};
|
|
|
|
interface MessageMetaProps {
|
|
query?: {
|
|
query_embedding: number[];
|
|
vector_embedding: number[];
|
|
},
|
|
rag: any,
|
|
tools: any[],
|
|
eval_count: number,
|
|
eval_duration: number,
|
|
prompt_eval_count: number,
|
|
prompt_eval_duration: number,
|
|
sessionId?: string,
|
|
connectionBase: string,
|
|
setSnack: (message: string, severity: SeverityType) => void,
|
|
}
|
|
|
|
type MessageList = MessageData[];
|
|
|
|
interface MessageProps {
|
|
message?: MessageData,
|
|
isFullWidth?: boolean,
|
|
submitQuery?: (text: string) => void,
|
|
sessionId?: string,
|
|
connectionBase: string,
|
|
setSnack: (message: string, severity: SeverityType) => void,
|
|
};
|
|
|
|
interface ChatQueryInterface {
|
|
text: string,
|
|
submitQuery?: (text: string) => void
|
|
}
|
|
|
|
|
|
const MessageMeta = ({ ...props }: MessageMetaProps) => {
|
|
return (<>
|
|
<Box sx={{ fontSize: "0.8rem", mb: 1 }}>
|
|
Below is the LLM performance of this query. Note that if tools are called, the
|
|
entire context is processed for each separate tool request by the LLM. This
|
|
can dramatically increase the total time for a response.
|
|
</Box>
|
|
<TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
|
|
<Table aria-label="prompt stats" size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell></TableCell>
|
|
<TableCell align="right" >Tokens</TableCell>
|
|
<TableCell align="right">Time (s)</TableCell>
|
|
<TableCell align="right">TPS</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
<TableRow key="prompt" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
|
<TableCell component="th" scope="row">Prompt</TableCell>
|
|
<TableCell align="right">{props.prompt_eval_count}</TableCell>
|
|
<TableCell align="right">{Math.round(props.prompt_eval_duration / 10 ** 7) / 100}</TableCell>
|
|
<TableCell align="right">{Math.round(props.prompt_eval_count * 10 ** 9 / props.prompt_eval_duration)}</TableCell>
|
|
</TableRow>
|
|
<TableRow key="response" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
|
<TableCell component="th" scope="row">Response</TableCell>
|
|
<TableCell align="right">{props.eval_count}</TableCell>
|
|
<TableCell align="right">{Math.round(props.eval_duration / 10 ** 7) / 100}</TableCell>
|
|
<TableCell align="right">{Math.round(props.eval_count * 10 ** 9 / props.eval_duration)}</TableCell>
|
|
</TableRow>
|
|
<TableRow key="total" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
|
<TableCell component="th" scope="row">Total</TableCell>
|
|
<TableCell align="right">{props.prompt_eval_count + props.eval_count}</TableCell>
|
|
<TableCell align="right">{Math.round((props.prompt_eval_duration + props.eval_duration) / 10 ** 7) / 100}</TableCell>
|
|
<TableCell align="right">{Math.round((props.prompt_eval_count + props.eval_count) * 10 ** 9 / (props.prompt_eval_duration + props.eval_duration))}</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
{
|
|
props.tools !== undefined && props.tools.length !== 0 &&
|
|
<Accordion sx={{ boxSizing: "border-box" }}>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Box sx={{ fontSize: "0.8rem" }}>
|
|
Tools queried
|
|
</Box>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
{props.tools.map((tool: any, index: number) => <Box key={index}>
|
|
{index !== 0 && <Divider />}
|
|
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 0.5 }}>
|
|
<div style={{ display: "flex", paddingRight: "1rem", whiteSpace: "nowrap" }}>
|
|
{tool.tool}
|
|
</div>
|
|
<div style={{
|
|
display: "flex",
|
|
padding: "3px",
|
|
whiteSpace: "pre-wrap",
|
|
flexGrow: 1,
|
|
border: "1px solid #E0E0E0",
|
|
wordBreak: "break-all",
|
|
maxHeight: "5rem",
|
|
overflow: "auto"
|
|
}}>
|
|
{JSON.stringify(tool.result, null, 2)}
|
|
</div>
|
|
</Box>
|
|
</Box>)}
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
}
|
|
{
|
|
props?.rag?.name !== undefined && <>
|
|
<Accordion>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Box sx={{ fontSize: "0.8rem" }}>
|
|
Top RAG {props.rag.ids.length} matches from '{props.rag.name}' collection against embedding vector of {props.rag.query_embedding.length} dimensions
|
|
</Box>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
{props.rag.ids.map((id: number, index: number) => <Box key={index}>
|
|
{index !== 0 && <Divider />}
|
|
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "row", mb: 0.5, mt: 0.5 }}>
|
|
<div style={{ display: "flex", flexDirection: "column", paddingRight: "1rem", minWidth: "10rem" }}>
|
|
<div style={{ whiteSpace: "nowrap" }}>Doc ID: {props.rag.ids[index].slice(-10)}</div>
|
|
<div style={{ whiteSpace: "nowrap" }}>Similarity: {Math.round(props.rag.distances[index] * 100) / 100}</div>
|
|
<div style={{ whiteSpace: "nowrap" }}>Type: {props.rag.metadatas[index].doc_type}</div>
|
|
<div style={{ whiteSpace: "nowrap" }}>Chunk Len: {props.rag.documents[index].length}</div>
|
|
</div>
|
|
<div style={{ display: "flex", padding: "3px", flexGrow: 1, border: "1px solid #E0E0E0", maxHeight: "5rem", overflow: "auto" }}>{props.rag.documents[index]}</div>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
<Accordion>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Box sx={{ fontSize: "0.8rem" }}>
|
|
UMAP Vector Visualization of RAG
|
|
</Box>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<VectorVisualizer inline {...props} rag={props?.rag} />
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
</>
|
|
}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const ChatQuery = ({ text, submitQuery }: ChatQueryInterface) => {
|
|
if (submitQuery === undefined) {
|
|
return (<Box>{text}</Box>);
|
|
}
|
|
return (
|
|
<Button variant="outlined" sx={{
|
|
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
|
|
borderColor: theme => theme.palette.custom.highlight,
|
|
m: 1
|
|
}}
|
|
size="small" onClick={(e: any) => { console.log(text); submitQuery(text); }}>
|
|
{text}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase }: MessageProps) => {
|
|
const [expanded, setExpanded] = useState<boolean>(false);
|
|
const [copied, setCopied] = useState(false);
|
|
const textFieldRef = useRef(null);
|
|
|
|
const handleCopy = () => {
|
|
if (message === undefined || message.content === undefined) {
|
|
return;
|
|
}
|
|
|
|
navigator.clipboard.writeText(message.content.trim()).then(() => {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
|
|
});
|
|
};
|
|
|
|
const handleExpandClick = () => {
|
|
setExpanded(!expanded);
|
|
};
|
|
|
|
if (message === undefined) {
|
|
return (<></>);
|
|
}
|
|
|
|
if (message.content === undefined) {
|
|
console.info("Message content is undefined");
|
|
return (<></>);
|
|
}
|
|
|
|
const formattedContent = message.content.trim();
|
|
|
|
return (
|
|
<ChatBubble className="Message" isFullWidth={isFullWidth} role={message.role} sx={{ pb: message.metadata ? 0 : "8px", m: 0, mb: 1, mt: 1, overflowX: "auto" }}>
|
|
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto" }}>
|
|
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
|
<IconButton
|
|
onClick={handleCopy}
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 0,
|
|
width: 24,
|
|
height: 24,
|
|
bgcolor: 'background.paper',
|
|
'&:hover': { bgcolor: 'action.hover' },
|
|
}}
|
|
size="small"
|
|
color={copied ? "success" : "default"}
|
|
>
|
|
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
|
|
</IconButton>
|
|
</Tooltip>
|
|
|
|
{message.role !== 'user' ?
|
|
<StyledMarkdown
|
|
className="MessageContent"
|
|
sx={{ display: "flex", color: 'text.secondary' }}
|
|
{...{ content: formattedContent, submitQuery }} />
|
|
:
|
|
<Typography
|
|
className="MessageContent"
|
|
ref={textFieldRef}
|
|
variant="body2"
|
|
sx={{ display: "flex", color: 'text.secondary' }}>
|
|
{message.content}
|
|
</Typography>
|
|
}
|
|
</CardContent>
|
|
{message.metadata && <>
|
|
<CardActions disableSpacing sx={{ justifySelf: "flex-end" }}>
|
|
<Button variant="text" onClick={handleExpandClick} sx={{ color: "darkgrey", p: 1, flexGrow: 0 }}>LLM information for this query</Button>
|
|
<ExpandMore
|
|
expand={expanded}
|
|
onClick={handleExpandClick}
|
|
aria-expanded={expanded}
|
|
aria-label="show more"
|
|
>
|
|
<ExpandMoreIcon />
|
|
</ExpandMore>
|
|
</CardActions>
|
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
|
<CardContent>
|
|
<MessageMeta {...{ ...message.metadata, sessionId, connectionBase, setSnack }} />
|
|
</CardContent>
|
|
</Collapse>
|
|
</>}
|
|
</ChatBubble>
|
|
);
|
|
};
|
|
|
|
export type {
|
|
MessageProps,
|
|
MessageList,
|
|
ChatQueryInterface,
|
|
MessageMetaProps,
|
|
MessageData,
|
|
MessageRoles
|
|
};
|
|
|
|
export {
|
|
Message,
|
|
ChatQuery,
|
|
MessageMeta
|
|
};
|
|
|