)
}
interface ChatMessageProps {
chatMessage: SingleChatMessage;
isMobileWidth: boolean;
customClassName?: string;
borderLeftColor?: string;
}
interface TrainOfThoughtProps {
message: string;
primary: boolean;
}
function chooseIconFromHeader(header: string, iconColor: string) {
const compareHeader = header.toLowerCase();
const classNames = `inline mt-1 mr-2 ${iconColor}`;
if (compareHeader.includes("understanding")) {
return
}
if (compareHeader.includes("generating")) {
return ;
}
if (compareHeader.includes("data sources")) {
return ;
}
if (compareHeader.includes("notes")) {
return ;
}
if (compareHeader.includes("read")) {
return ;
}
if (compareHeader.includes("search")) {
return ;
}
if (compareHeader.includes("summary") || compareHeader.includes("summarize")) {
return ;
}
return ;
}
export function TrainOfThought(props: TrainOfThoughtProps) {
// The train of thought comes in as a markdown-formatted string. It starts with a heading delimited by two asterisks at the start and end and a colon, followed by the message. Example: **header**: status. This function will parse the message and render it as a div.
let extractedHeader = props.message.match(/\*\*(.*)\*\*/);
let header = extractedHeader ? extractedHeader[1] : "";
const iconColor = props.primary ? 'text-orange-400' : 'text-gray-500';
const icon = chooseIconFromHeader(header, iconColor);
let markdownRendered = DomPurify.sanitize(md.render(props.message));
return (
{icon}
)
}
export default function ChatMessage(props: ChatMessageProps) {
const [copySuccess, setCopySuccess] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const [markdownRendered, setMarkdownRendered] = useState('');
const messageRef = useRef(null);
useEffect(() => {
let message = props.chatMessage.message;
// Replace LaTeX delimiters with placeholders
message = message.replace(/\\\(/g, 'LEFTPAREN').replace(/\\\)/g, 'RIGHTPAREN')
.replace(/\\\[/g, 'LEFTBRACKET').replace(/\\\]/g, 'RIGHTBRACKET');
if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image") {
message = ``;
} else if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image2") {
message = ``;
} else if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image-v3") {
message = ``;
}
if (props.chatMessage.intent && props.chatMessage.intent.type.includes("text-to-image") && props.chatMessage.intent["inferred-queries"]?.length > 0) {
message += `\n\n**Inferred Query**\n\n${props.chatMessage.intent["inferred-queries"][0]}`;
}
let markdownRendered = md.render(message);
// Replace placeholders with LaTeX delimiters
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
// Sanitize and set the rendered markdown
setMarkdownRendered(DomPurify.sanitize(markdownRendered));
}, [props.chatMessage.message]);
useEffect(() => {
if (copySuccess) {
setTimeout(() => {
setCopySuccess(false);
}, 2000);
}
}, [copySuccess]);
useEffect(() => {
if (messageRef.current) {
const preElements = messageRef.current.querySelectorAll('pre > .hljs');
preElements.forEach((preElement) => {
const copyButton = document.createElement('button');
const copyImage = document.createElement('img');
copyImage.src = '/copy-button.svg';
copyImage.alt = 'Copy';
copyImage.width = 24;
copyImage.height = 24;
copyButton.appendChild(copyImage);
copyButton.className = `hljs ${styles.codeCopyButton}`
copyButton.addEventListener('click', () => {
let textContent = preElement.textContent || '';
// Strip any leading $ characters
textContent = textContent.replace(/^\$+/, '');
// Remove 'Copy' if it's at the start of the string
textContent = textContent.replace(/^Copy/, '');
textContent = textContent.trim();
navigator.clipboard.writeText(textContent);
});
preElement.prepend(copyButton);
});
}
}, [markdownRendered]);
if (!props.chatMessage.message) {
return null;
}
function formatDate(timestamp: string) {
// Format date in HH:MM, DD MMM YYYY format
let date = new Date(timestamp + "Z");
let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase();
let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit'}).replaceAll('-', ' ');
return `${time_string} on ${date_string}`;
}
function renderTimeStamp(timestamp: string) {
if (!timestamp.endsWith('Z')) {
timestamp = timestamp + 'Z';
}
const messageDateTime = new Date(timestamp);
const currentDateTime = new Date();
const timeDiff = currentDateTime.getTime() - messageDateTime.getTime();
if (timeDiff < 60e3) {
return "Just now";
}
if (timeDiff < 3600e3) {
// Using Math.round for closer to actual time representation
return `${Math.round(timeDiff / 60e3)}m ago`;
}
if (timeDiff < 86400e3) {
return `${Math.round(timeDiff / 3600e3)}h ago`;
}
return `${Math.round(timeDiff / 86400e3)}d ago`;
}
function constructClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageContainer];
classes.push(styles[chatMessage.by]);
if (props.customClassName) {
classes.push(styles[`${chatMessage.by}${props.customClassName}`])
}
return classes.join(' ');
}
function chatMessageWrapperClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageWrapper];
classes.push(styles[chatMessage.by]);
if (chatMessage.by === "khoj") {
const dynamicBorderColor = `border-l-${props.borderLeftColor}`;
classes.push(`border-l-4 border-opacity-50 border-l-orange-400 ${dynamicBorderColor}`);
}
return classes.join(' ');
}
const allReferences = constructAllReferences(props.chatMessage.context, props.chatMessage.onlineContext);
return (