backstory/frontend/src/components/ui/TransientChat.tsx
James Ketrenos 179bef1564 Anthropic backend working
Add regenerate skill assessment
2025-07-11 13:24:47 -07:00

264 lines
7.7 KiB
TypeScript

import React, { forwardRef, useState, useEffect, useRef, JSX, useImperativeHandle } from 'react';
import { Box } from '@mui/material';
import { useAuth } from 'hooks/AuthContext';
import {
ChatMessage,
ChatSession,
ChatMessageUser,
ChatMessageError,
ChatMessageStreaming,
ChatMessageStatus,
ChatMessageMetaData,
Candidate,
ChatQuery,
} from 'types/types';
import { ConversationHandle } from 'components/Conversation';
import { Message } from 'components/Message';
import { useAppState } from 'hooks/GlobalContext';
import PropagateLoader from 'react-spinners/PropagateLoader';
import { Scrollable } from 'components/Scrollable';
const emptyMetadata: ChatMessageMetaData = {
model: 'qwen2.5',
temperature: 0,
maxTokens: 0,
topP: 0,
frequencyPenalty: 0,
presencePenalty: 0,
stopSequences: [],
usage: {
evalCount: 0,
evalDuration: 0,
promptEvalCount: 0,
promptEvalDuration: 0,
},
};
const defaultMessage: ChatMessage = {
status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
role: 'user',
metadata: emptyMetadata,
};
interface TransientChatProps {
id: string;
}
const TransientChat = forwardRef<ConversationHandle, TransientChatProps>(
(props, ref): JSX.Element => {
const { id } = props;
const { apiClient, user } = useAuth();
const [processingMessage, setProcessingMessage] = useState<
ChatMessageStatus | ChatMessageError | null
>(null);
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
const { setSnack } = useAppState();
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [streaming, setStreaming] = useState<boolean>(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
submitQuery: (query: ChatQuery): void => {
sendMessage(query.prompt);
},
fetchHistory: (): void => {
console.log('fetchHistory called, but not implemented');
},
}));
const chatHandlers = {
onMessage: (msg: ChatMessage): void => {
setMessages(prev => {
const filtered = prev.filter(m => m.id !== msg.id);
return [...filtered, msg] as ChatMessage[];
});
setStreamingMessage(null);
setProcessingMessage(null);
},
onError: (error: string | ChatMessageError): void => {
console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === 'object' && error !== null && 'content' in error) {
setProcessingMessage(error);
} else {
setProcessingMessage({
...defaultMessage,
status: 'error',
content: error,
});
}
setStreaming(false);
},
onStreaming: (chunk: ChatMessageStreaming): void => {
// console.log("onStreaming:", chunk);
setStreamingMessage({
...chunk,
role: 'assistant',
metadata: emptyMetadata,
});
},
onStatus: (status: ChatMessageStatus): void => {
setProcessingMessage(status);
},
onComplete: (): void => {
console.log('onComplete');
setStreamingMessage(null);
setProcessingMessage(null);
setStreaming(false);
},
};
// Send message
const sendMessage = async (message: string): Promise<void> => {
if (!message.trim() || !chatSession?.id || streaming) return;
const messageContent = message;
setStreaming(true);
const chatMessage: ChatMessageUser = {
sessionId: chatSession.id,
role: 'user',
content: messageContent,
status: 'done',
type: 'text',
timestamp: new Date(),
};
setProcessingMessage({
...defaultMessage,
status: 'status',
activity: 'info',
content: `Establishing connection with chat session.`,
});
setMessages(prev => {
const filtered = prev.filter(m => m.id !== chatMessage.id);
return [...filtered, chatMessage] as ChatMessage[];
});
try {
apiClient.sendMessageStream(chatMessage, chatHandlers);
} catch (error) {
console.error('Failed to send message:', error);
setStreaming(false);
}
};
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Load sessions when username changes
useEffect(() => {
if (!user) return;
try {
apiClient
.getOrCreateChatSession(user as Candidate, `Transient chat - ${id}`, 'candidate_chat')
.then(session => {
setChatSession(session);
});
} catch (error) {
setSnack('Unable to load chat session', 'error');
}
}, [user, apiClient, setSnack]);
// Load messages when session changes
useEffect(() => {
const loadMessages = async (): Promise<void> => {
if (!chatSession?.id) return;
try {
const result = await apiClient.getChatMessages(chatSession.id);
const chatMessages: ChatMessage[] = result.data;
setMessages(chatMessages);
setProcessingMessage(null);
setStreamingMessage(null);
console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages);
} catch (error) {
console.error('Failed to load messages:', error);
}
};
if (chatSession?.id) {
loadMessages();
}
}, [chatSession, apiClient]);
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%' /* Restrict to main-container's height */,
width: '100%',
minHeight: 0 /* Prevent flex overflow */,
maxHeight: 'min-content',
'& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */,
},
position: 'relative',
}}
>
{/* Chat Interface */}
{/* Scrollable Messages Area */}
{chatSession && (
<Scrollable
sx={{
position: 'relative',
maxHeight: '100%',
width: '100%',
display: 'flex',
flexGrow: 1,
flex: 1 /* Take remaining space in some-container */,
overflowY: 'auto' /* Scroll if content overflows */,
pt: 2,
pl: 1,
pr: 1,
pb: 2,
}}
>
{messages.map((message: ChatMessage) => (
<Message key={message.id} {...{ chatSession, message }} />
))}
{processingMessage !== null && (
<Message {...{ chatSession, message: processingMessage }} />
)}
{streamingMessage !== null && (
<Message {...{ chatSession, message: streamingMessage }} />
)}
{streaming && (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
m: 1,
}}
>
<PropagateLoader
size="10px"
loading={streaming}
aria-label="Loading Spinner"
data-testid="loader"
/>
</Box>
)}
<div ref={messagesEndRef} />
</Scrollable>
)}
</Box>
);
}
);
TransientChat.displayName = 'TransientChat';
export { TransientChat };