Single chat session per candidate

This commit is contained in:
James Ketr 2025-06-02 19:45:04 -07:00
parent f286868cbe
commit 0994c95f91
4 changed files with 232 additions and 601 deletions

View File

@ -16,6 +16,7 @@ interface CandidateInfoProps {
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "small" | "normal" | null
}; };
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => { const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
@ -23,7 +24,8 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
const { const {
sx, sx,
action = '', action = '',
elevation = 1 elevation = 1,
variant = "normal"
} = props; } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@ -107,6 +109,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
{candidate.description} {candidate.description}
</Typography> </Typography>
{variant !== "small" && <>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
{candidate.location && {candidate.location &&
@ -119,11 +122,12 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
<strong>Email:</strong> {candidate.email} <strong>Email:</strong> {candidate.email}
</Typography> </Typography>
} }
{ candidate.phone && <Typography variant="body2"> {candidate.phone && <Typography variant="body2">
<strong>Phone:</strong> {candidate.phone} <strong>Phone:</strong> {candidate.phone}
</Typography> } </Typography>
}
</>}
</Grid> </Grid>
</Grid> </Grid>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -2,64 +2,35 @@ import React, { forwardRef, useState, useEffect, useRef } from 'react';
import { import {
Box, Box,
Paper, Paper,
Typography,
TextField,
Button, Button,
List,
ListItem,
ListItemText,
Chip,
IconButton,
CircularProgress,
Divider, Divider,
Card,
CardContent,
Avatar,
useTheme, useTheme,
useMediaQuery, useMediaQuery,
Fab,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Backdrop
} from '@mui/material'; } from '@mui/material';
import { import {
ChevronLeft, Send as SendIcon
ChevronRight,
Chat as ChatIcon,
Edit as EditIcon,
Delete as DeleteIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { ChatMessageBase, ChatMessage, ChatSession, ChatStatusType, ChatMessageType, ChatMessageUser } from 'types/types'; import { ChatMessageBase, ChatMessage, ChatSession, ChatMessageUser } from 'types/types';
import { ConversationHandle } from 'components/Conversation'; import { ConversationHandle } from 'components/Conversation';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { Message } from 'components/Message'; import { Message } from 'components/Message';
import { DeleteConfirmation } from 'components/DeleteConfirmation'; import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { CandidateSessionsResponse } from 'services/api-client';
import { CandidateInfo } from 'components/CandidateInfo'; import { CandidateInfo } from 'components/CandidateInfo';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useSelectedCandidate } from 'hooks/GlobalContext';
import PropagateLoader from 'react-spinners/PropagateLoader'; import PropagateLoader from 'react-spinners/PropagateLoader';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
const DRAWER_WIDTH = 300;
const FAB_WIDTH = 48;
const FAB_HEIGHT = 64;
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
type: "preparing", status: "done", sender: "system", sessionId: "", timestamp: new Date(), content: "" type: "preparing", status: "done", sender: "system", sessionId: "", timestamp: new Date(), content: ""
}; };
type HandlePosition = 'center' | 'top' | 'bottom';
const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => { const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { selectedCandidate } = useSelectedCandidate()
const navigate = useNavigate(); const navigate = useNavigate();
const { selectedCandidate } = useSelectedCandidate()
const theme = useTheme(); const theme = useTheme();
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null); const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null); const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
@ -69,84 +40,12 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
submitQuery, submitQuery,
} = props; } = props;
const [sessions, setSessions] = useState<CandidateSessionsResponse | null>(null);
const [chatSession, setChatSession] = useState<ChatSession | null>(null); const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [streaming, setStreaming] = useState<boolean>(false); const [streaming, setStreaming] = useState<boolean>(false);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
// Drawer state - defaults to open on md+ screens
const [drawerOpen, setDrawerOpen] = useState(() => isMdUp);
// Handle position state for mobile scroll behavior
const [handlePosition, setHandlePosition] = useState<HandlePosition>('center');
const lastScrollY = useRef(0);
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
// Edit session state
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingSession, setEditingSession] = useState<ChatSession | null>(null);
const [editSessionTitle, setEditSessionTitle] = useState('');
// Delete confirmation state
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<ChatSession | null>(null);
// Update drawer state when screen size changes
useEffect(() => {
if (isMdUp && !drawerOpen) {
setDrawerOpen(true);
}
}, [isMdUp]);
// Scroll event handler for mobile handle positioning
useEffect(() => {
if (isMdUp) return; // Only for mobile
const handleScroll = () => {
const currentScrollY = window.scrollY;
const scrollDirection = currentScrollY > lastScrollY.current ? 'down' : 'up';
// Clear existing timeout
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
// Update handle position based on scroll direction
if (scrollDirection === 'down' && currentScrollY > 50) {
setHandlePosition('top');
} else if (scrollDirection === 'up' && currentScrollY > 50) {
setHandlePosition('bottom');
}
lastScrollY.current = currentScrollY;
// Reset to center after scrolling stops (with debounce)
scrollTimeout.current = setTimeout(() => {
if (currentScrollY <= 50) {
setHandlePosition('center');
}
}, 150);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
};
}, [isMdUp]);
// Close drawer when clicking outside on mobile only
const handleBackdropClick = () => {
if (!isMdUp) {
setDrawerOpen(false);
}
};
// Load sessions for the selectedCandidate // Load sessions for the selectedCandidate
const loadSessions = async () => { const loadSessions = async () => {
if (!selectedCandidate) return; if (!selectedCandidate) return;
@ -154,9 +53,20 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
try { try {
setLoading(true); setLoading(true);
const result = await apiClient.getCandidateChatSessions(selectedCandidate.username); const result = await apiClient.getCandidateChatSessions(selectedCandidate.username);
setSessions(result); let session = null;
if (result.sessions.data.length === 0) {
session = await apiClient.createCandidateChatSession(
selectedCandidate.username,
'candidate_chat',
`Backstory chat about ${selectedCandidate.fullName}`
);
} else {
session = result.sessions.data[0];
}
setChatSession(session);
setLoading(false);
} catch (error) { } catch (error) {
console.error('Failed to load sessions:', error); setSnack('Unable to load chat session', 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -178,76 +88,15 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
} }
}; };
// Create new session const onDelete = async (session: ChatSession) => {
const createNewSession = async () => { if (!session.id) {
if (!selectedCandidate) { return } return;
try {
setLoading(true);
const newSession = await apiClient.createCandidateChatSession(
selectedCandidate.username,
'candidate_chat',
`Backstory chat about ${selectedCandidate.fullName}`
);
setChatSession(newSession);
setMessages([]);
setProcessingMessage(null);
setStreamingMessage(null);
await loadSessions(); // Refresh sessions list
} catch (error) {
console.error('Failed to create session:', error);
} finally {
setLoading(false);
} }
};
// Edit session
const handleEditSession = (session: ChatSession, event: React.MouseEvent) => {
event.stopPropagation(); // Prevent session selection
setEditingSession(session);
setEditSessionTitle(session.title || '');
setEditDialogOpen(true);
};
const handleSaveEdit = async () => {
if (!editingSession?.id || !editSessionTitle.trim()) return;
try { try {
// Assuming there's an API method to update session title await apiClient.resetChatSession(session.id);
await apiClient.updateChatSession(editingSession.id, { title: editSessionTitle.trim() });
await loadSessions(); // Refresh sessions list
setEditDialogOpen(false);
setEditingSession(null);
setEditSessionTitle('');
setSnack('Session title updated successfully', 'success');
} catch (error) {
console.error('Failed to update session:', error);
setSnack('Failed to update session title', 'error');
}
};
// Delete session
const handleDeleteSession = (session: ChatSession, event: React.MouseEvent) => {
event.stopPropagation(); // Prevent session selection
setSessionToDelete(session);
setDeleteDialogOpen(true);
};
const handleConfirmDelete = async () => {
if (!sessionToDelete?.id) return;
try {
await apiClient.deleteChatSession(sessionToDelete.id);
await loadSessions(); // Refresh sessions list
// If we're deleting the currently selected session, clear it // If we're deleting the currently selected session, clear it
if (chatSession?.id === sessionToDelete.id) {
setChatSession(null);
setMessages([]); setMessages([]);
} setSnack('Session reset succeeded', 'success');
setDeleteDialogOpen(false);
setSessionToDelete(null);
setSnack('Session deleted successfully', 'success');
} catch (error) { } catch (error) {
console.error('Failed to delete session:', error); console.error('Failed to delete session:', error);
setSnack('Failed to delete session', 'error'); setSnack('Failed to delete session', 'error');
@ -276,7 +125,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
}); });
try { try {
await apiClient.sendMessageStream(chatMessage, { apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => { onMessage: (msg: ChatMessage) => {
console.log(`onMessage: ${msg.type} ${msg.content}`, msg); console.log(`onMessage: ${msg.type} ${msg.content}`, msg);
if (msg.type === "response") { if (msg.type === "response") {
@ -339,264 +188,32 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
if (!selectedCandidate) { if (!selectedCandidate) {
navigate('/find-a-candidate'); navigate('/find-a-candidate');
return (<></>);
} }
// Get handle positioning styles based on current position const welcomeMessage: ChatMessage = {
const getHandleStyles = () => { sessionId: chatSession?.id || '',
const baseStyles = { type: "info",
position: 'absolute' as const, status: "done",
left: DRAWER_WIDTH, sender: "system",
zIndex: theme.zIndex.drawer + 1, timestamp: new Date(),
content: `Welcome to the Backstory Chat about ${selectedCandidate.fullName}. Ask any questions you have about ${selectedCandidate.firstName}.`
}; };
switch (handlePosition) {
case 'top':
return {
...baseStyles,
position: 'fixed' as const,
top: 20,
transform: 'translateY(0)',
};
case 'bottom':
return {
...baseStyles,
position: 'fixed' as const,
bottom: 20,
transform: 'translateY(0)',
};
case 'center':
default:
return {
...baseStyles,
top: '50%',
transform: 'translateY(-50%)',
};
}
};
const drawerContent = (
<Box sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden' // Prevent overall drawer from scrolling
}}>
{/* Fixed Header Section */}
<Box sx={{ p: 2, flexShrink: 0 }}>
<Typography variant="h6" gutterBottom>
Chat Sessions
{sessions && (
<Chip
size="small"
label={`${sessions.sessions.total} total`}
sx={{ ml: 1 }}
/>
)}
</Typography>
<Button
variant="outlined"
onClick={createNewSession}
disabled={loading || !selectedCandidate}
sx={{ mb: 2, width: '100%' }}
>
New Session
</Button>
</Box>
{/* Scrollable Sessions List */}
<Box sx={{
flexGrow: 1,
overflow: 'auto',
px: 2,
pb: 2,
// Custom scrollbar styling
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.grey[100],
borderRadius: '4px',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.grey[400],
borderRadius: '4px',
'&:hover': {
backgroundColor: theme.palette.grey[600],
},
},
}}>
{sessions ? (
<List sx={{ padding: 0 }}>
{sessions.sessions.data.map((session: any) => (
<ListItem
key={session.id}
onClick={() => {
setChatSession(session);
// Auto-close drawer on mobile only when session is selected
if (!isMdUp) {
setDrawerOpen(false);
}
}}
sx={{
mb: 1,
borderRadius: 1,
border: '1px solid',
borderColor: chatSession?.id === session.id ? 'orange' : 'divider',
cursor: 'pointer',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'action.hover'
}
}}
secondaryAction={
<Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton
edge="end"
size="small"
onClick={(e) => handleEditSession(session, e)}
sx={{
'&:hover': {
backgroundColor: 'action.hover',
color: 'primary.main'
}
}}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
edge="end"
size="small"
onClick={(e) => handleDeleteSession(session, e)}
sx={{
'&:hover': {
backgroundColor: 'error.light',
color: 'error.main'
}
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
}
>
<ListItemText
primary={session.title}
secondary={`${new Date(session.lastActivity).toLocaleDateString()}${session.context.type}`}
sx={{ pr: 2 }}
/>
</ListItem>
))}
</List>
) : (
<Typography color="text.secondary" align="center" sx={{ mt: 4 }}>
Enter a username and click "Load Sessions"
</Typography>
)}
</Box>
</Box>
);
return ( return (
<Box ref={ref} sx={{ <Box ref={ref} sx={{
width: "100%", width: "100%",
height: "100vh", // Fixed viewport height
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
overflow: 'hidden' // Prevent page-level scrolling gap: 1,
}}> }}>
{selectedCandidate && (
<CandidateInfo <CandidateInfo
action={`Chat with Backstory about ${selectedCandidate.firstName}`} action={`Chat with Backstory about ${selectedCandidate.firstName}`}
elevation={4} elevation={4}
candidate={selectedCandidate} candidate={selectedCandidate}
variant="small"
sx={{ flexShrink: 0 }} // Prevent header from shrinking sx={{ flexShrink: 0 }} // Prevent header from shrinking
/> />
)}
<Box sx={{
display: "flex",
mt: 1,
gap: 1,
flexGrow: 1,
position: 'relative',
overflow: 'hidden',
minHeight: 0 // Important for flex child to shrink
}}>
{/* Mobile Backdrop */}
{!isMdUp && drawerOpen && (
<Backdrop
open={drawerOpen}
onClick={handleBackdropClick}
sx={{
position: 'absolute',
zIndex: theme.zIndex.drawer - 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
}}
/>
)}
{/* Drawer Container - Different behavior for mobile vs desktop */}
<Box
sx={{
position: isMdUp ? 'relative' : 'absolute',
left: 0,
top: 0,
width: isMdUp ? DRAWER_WIDTH : DRAWER_WIDTH + FAB_WIDTH,
height: '100%',
transform: isMdUp ? 'none' : (drawerOpen ? 'translateX(0)' : `translateX(-${DRAWER_WIDTH}px)`),
transition: isMdUp ? 'none' : theme.transitions.create('transform', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.standard,
}),
zIndex: theme.zIndex.drawer,
}}
>
{/* Drawer Content */}
<Paper
sx={{
position: 'absolute',
left: 0,
top: 0,
width: DRAWER_WIDTH,
height: '100%',
borderRight: '1px solid',
borderColor: 'divider',
borderRadius: 0,
overflow: 'hidden',
display: isMdUp ? 'block' : (drawerOpen ? 'block' : 'none'),
}}
>
{drawerContent}
</Paper>
{/* Integrated Fab Handle - Mobile Only */}
{!isMdUp && (
<Box sx={getHandleStyles()}>
<Fab
size="small"
color="primary"
onClick={() => setDrawerOpen(!drawerOpen)}
sx={{
borderRadius: '0 50% 50% 0',
width: FAB_WIDTH,
height: FAB_HEIGHT,
minHeight: FAB_HEIGHT,
boxShadow: 2,
transition: theme.transitions.create(['box-shadow', 'background-color'], {
duration: theme.transitions.duration.short,
}),
'&:hover': {
boxShadow: 4,
}
}}
>
{drawerOpen ? <ChevronLeft /> : <ChatIcon />}
</Fab>
</Box>
)}
</Box>
{/* Chat Interface */} {/* Chat Interface */}
<Paper <Paper
@ -604,22 +221,18 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
marginLeft: isMdUp ? 0 : 0, width: '100%',
width: isMdUp ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%', minHeight: 'max-content'
overflow: 'hidden',
minHeight: 0, // Important for flex child
}} }}
> >
{chatSession?.id ? (
<>
{/* Scrollable Messages Area */} {/* Scrollable Messages Area */}
{chatSession && <>
<Box sx={{ <Box sx={{
flexGrow: 1, flexGrow: 1,
overflow: 'auto',
p: 2, p: 2,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: 0, // Important for flex child minHeight: 'max-content', // Important for flex child
// Custom scrollbar styling // Custom scrollbar styling
'&::-webkit-scrollbar': { '&::-webkit-scrollbar': {
width: '8px', width: '8px',
@ -636,6 +249,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
}, },
}, },
}}> }}>
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, setSnack, submitQuery }} />}
{messages.map((message: ChatMessageBase) => ( {messages.map((message: ChatMessageBase) => (
<Message key={message.id} {...{ chatSession, message, setSnack, submitQuery }} /> <Message key={message.id} {...{ chatSession, message, setSnack, submitQuery }} />
))} ))}
@ -663,88 +277,34 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
)} )}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</Box> </Box>
</>}
<Divider /> </Paper>
{/* Fixed Message Input */} {/* Fixed Message Input */}
<Box sx={{ p: 2, display: 'flex', gap: 1, flexShrink: 0 }}> <Box sx={{ display: 'flex', flexShrink: 0, gap: 1 }}>
<DeleteConfirmation
onDelete={() => { chatSession && onDelete(chatSession); }}
disabled={!chatSession}
action="reset"
label="chat session"
title="Reset Chat Session"
message={`Are you sure you want to reset the session? This action cannot be undone.`}
/>
<BackstoryTextField <BackstoryTextField
placeholder="Type your message about the candidate..." placeholder="Type your message about the candidate..."
ref={backstoryTextRef} ref={backstoryTextRef}
onEnter={sendMessage} onEnter={sendMessage}
disabled={streaming} disabled={streaming || loading}
/> />
<Button <Button
variant="contained" variant="contained"
onClick={() => { sendMessage((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }} onClick={() => { sendMessage((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }}
disabled={streaming} disabled={streaming || loading}
sx={{ minWidth: 'auto', px: 2 }} sx={{ minWidth: 'auto', px: 2 }}
> >
<SendIcon />
</Button> </Button>
</Box> </Box>
</>
) : (
<Box
sx={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 2
}}
>
<Typography variant="h6" color="text.secondary">
Select a session to start chatting
</Typography>
<Typography variant="body2" color="text.secondary" align="center">
Create a new session or choose from existing ones to begin discussing the candidate
</Typography>
</Box>
)}
</Paper>
</Box>
{/* Edit Session Dialog */}
<Dialog open={editDialogOpen} onClose={() => setEditDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Edit Session Title</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Session Title"
fullWidth
variant="outlined"
value={editSessionTitle}
onChange={(e) => setEditSessionTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSaveEdit();
}
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSaveEdit} variant="contained" disabled={!editSessionTitle.trim()}>
Save
</Button>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<DeleteConfirmation
open={deleteDialogOpen}
action="delete"
label="chat session"
hideButton={true}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={handleConfirmDelete}
title="Delete Chat Session"
message={`Are you sure you want to delete the session "${sessionToDelete?.title}"? This action cannot be undone.`}
/>
</Box> </Box>
); );
}); });

View File

@ -932,6 +932,15 @@ class ApiClient {
return handleApiResponse<{ success: boolean; message: string }>(response); return handleApiResponse<{ success: boolean; message: string }>(response);
} }
async resetChatSession(id: string): Promise<{ success: boolean; message: string }> {
const response = await fetch(`${this.baseUrl}/chat/sessions/${id}/reset`, {
method: 'PATCH',
headers: this.defaultHeaders
});
return handleApiResponse<{ success: boolean; message: string }>(response);
}
/** /**
* Send message with streaming response support and date conversion * Send message with streaming response support and date conversion
*/ */

View File

@ -3065,6 +3065,64 @@ async def delete_chat_session(
content=create_error_response("DELETE_ERROR", str(e)) content=create_error_response("DELETE_ERROR", str(e))
) )
@api_router.patch("/chat/sessions/{session_id}/reset")
async def reset_chat_session(
session_id: str = Path(...),
current_user = Depends(get_current_user),
database: RedisDatabase = Depends(get_database)
):
"""Delete a chat session and all its messages"""
try:
# Get the session to verify it exists and check ownership
session_data = await database.get_chat_session(session_id)
if not session_data:
return JSONResponse(
status_code=404,
content=create_error_response("NOT_FOUND", "Chat session not found")
)
session = ChatSession.model_validate(session_data)
# Check authorization - user can only delete their own sessions
if session.user_id != current_user.id:
return JSONResponse(
status_code=403,
content=create_error_response("FORBIDDEN", "Cannot reset another user's chat session")
)
# Delete all messages associated with this session
try:
chat_messages = await database.get_chat_messages(session_id)
message_count = len(chat_messages)
# Delete each message
for message_data in chat_messages:
message_id = message_data.get("id")
if message_id:
await database.delete_chat_message(session_id, message_id)
logger.info(f"🗑️ Deleted {message_count} messages from session {session_id}")
except Exception as e:
logger.warning(f"⚠️ Error deleting messages for session {session_id}: {e}")
# Continue with session deletion even if message deletion fails
logger.info(f"🗑️ Chat session {session_id} reset by user {current_user.id}")
return create_success_response({
"success": True,
"message": "Chat session reset successfully",
"sessionId": session_id
})
except Exception as e:
logger.error(f"❌ Reset chat session error: {e}")
return JSONResponse(
status_code=500,
content=create_error_response("RESET_ERROR", str(e))
)
@api_router.get("/candidates/{username}/chat-sessions") @api_router.get("/candidates/{username}/chat-sessions")
async def get_candidate_chat_sessions( async def get_candidate_chat_sessions(
username: str = Path(...), username: str = Path(...),