Added mockup chat page
This commit is contained in:
parent
b823c1e839
commit
27d9ab467a
486
frontend/src/documents/CandidateChatSystem.tsx
Normal file
486
frontend/src/documents/CandidateChatSystem.tsx
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
CircularProgress,
|
||||||
|
Divider,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Avatar,
|
||||||
|
Grid
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
// Mock API client (replace with your actual implementation)
|
||||||
|
const mockApiClient = {
|
||||||
|
async createCandidateChatSession(username: string, chatType: any, aiParameters: any, title: any) {
|
||||||
|
return {
|
||||||
|
id: `session-${Date.now()}`,
|
||||||
|
title: title || `Chat about ${username}`,
|
||||||
|
userId: 'current-user-id',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastActivity: new Date().toISOString(),
|
||||||
|
context: {
|
||||||
|
type: chatType,
|
||||||
|
relatedEntityId: `candidate-${username}`,
|
||||||
|
relatedEntityType: 'candidate',
|
||||||
|
aiParameters,
|
||||||
|
additionalContext: {
|
||||||
|
candidateInfo: {
|
||||||
|
id: `candidate-${username}`,
|
||||||
|
name: `${username} Candidate`,
|
||||||
|
email: `${username}@example.com`,
|
||||||
|
username: username,
|
||||||
|
skills: ['JavaScript', 'React', 'Python'],
|
||||||
|
experience: 3,
|
||||||
|
location: 'San Francisco'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async getCandidateChatSessions(username: string) {
|
||||||
|
return {
|
||||||
|
candidate: {
|
||||||
|
id: `candidate-${username}`,
|
||||||
|
username: username,
|
||||||
|
fullName: `${username} Candidate`,
|
||||||
|
email: `${username}@example.com`
|
||||||
|
},
|
||||||
|
sessions: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'session-1',
|
||||||
|
title: `Previous chat about ${username}`,
|
||||||
|
lastActivity: new Date(Date.now() - 86400000).toISOString(),
|
||||||
|
context: { type: 'candidate_screening' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 1,
|
||||||
|
hasMore: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async getChatMessages(sessionId: any) {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
sessionId,
|
||||||
|
sender: 'user',
|
||||||
|
content: 'Tell me about this candidate',
|
||||||
|
timestamp: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
status: 'done'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
sessionId,
|
||||||
|
sender: 'ai',
|
||||||
|
content: 'This candidate has strong technical skills in JavaScript and React...',
|
||||||
|
timestamp: new Date(Date.now() - 30000).toISOString(),
|
||||||
|
status: 'done'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
totalPages: 1,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendMessageStream(sessionId: any, query: any, onMessage: any, onError: any, onComplete: any) {
|
||||||
|
// Simulate user message
|
||||||
|
const userMessage = {
|
||||||
|
id: `msg-user-${Date.now()}`,
|
||||||
|
sessionId,
|
||||||
|
sender: 'user',
|
||||||
|
content: typeof query === 'string' ? query : query.prompt,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'done'
|
||||||
|
};
|
||||||
|
onMessage(userMessage);
|
||||||
|
|
||||||
|
// Simulate AI response with streaming
|
||||||
|
setTimeout(() => {
|
||||||
|
const aiMessage = {
|
||||||
|
id: `msg-ai-${Date.now()}`,
|
||||||
|
sessionId,
|
||||||
|
sender: 'ai',
|
||||||
|
content: 'This is a simulated AI response to your question...',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'done'
|
||||||
|
};
|
||||||
|
onMessage(aiMessage);
|
||||||
|
onComplete();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CandidateChatSystem = () => {
|
||||||
|
const [username, setUsername] = useState('johndoe');
|
||||||
|
const [sessions, setSessions] = useState<any>(null);
|
||||||
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [streaming, setStreaming] = useState(false);
|
||||||
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
|
const aiParameters = {
|
||||||
|
name: 'Chat Assistant',
|
||||||
|
model: 'gpt-4',
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2000,
|
||||||
|
topP: 0.95,
|
||||||
|
frequencyPenalty: 0.0,
|
||||||
|
presencePenalty: 0.0,
|
||||||
|
isDefault: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load sessions for the candidate
|
||||||
|
const loadSessions = async () => {
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await mockApiClient.getCandidateChatSessions(username);
|
||||||
|
setSessions(result as any);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load sessions:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load messages for current session
|
||||||
|
const loadMessages = async () => {
|
||||||
|
if (!currentSessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await mockApiClient.getChatMessages(currentSessionId);
|
||||||
|
setMessages(result.data as any);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new session
|
||||||
|
const createNewSession = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const newSession = await mockApiClient.createCandidateChatSession(
|
||||||
|
username,
|
||||||
|
'candidate_screening',
|
||||||
|
aiParameters,
|
||||||
|
`Interview Discussion - ${username}`
|
||||||
|
);
|
||||||
|
|
||||||
|
setCurrentSessionId(newSession.id as any);
|
||||||
|
setMessages([]);
|
||||||
|
await loadSessions(); // Refresh sessions list
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create session:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const sendMessage = async () => {
|
||||||
|
if (!newMessage.trim() || !currentSessionId || streaming) return;
|
||||||
|
|
||||||
|
const messageContent = newMessage;
|
||||||
|
setNewMessage('');
|
||||||
|
setStreaming(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mockApiClient.sendMessageStream(
|
||||||
|
currentSessionId,
|
||||||
|
{ prompt: messageContent },
|
||||||
|
(message : any) => {
|
||||||
|
setMessages(prev => {
|
||||||
|
const filtered = prev.filter((m : any)=> m.id !== message.id);
|
||||||
|
return [...filtered, message].sort((a, b) =>
|
||||||
|
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||||
|
) as any;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
console.error('Streaming error:', error);
|
||||||
|
setStreaming(false);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setStreaming(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send message:', error);
|
||||||
|
setStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new messages arrive
|
||||||
|
useEffect(() => {
|
||||||
|
(messagesEndRef.current as any)?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Load sessions when username changes
|
||||||
|
useEffect(() => {
|
||||||
|
loadSessions();
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
|
// Load messages when session changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentSessionId) {
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
}, [currentSessionId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3, maxWidth: 1200, mx: 'auto' }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Candidate Chat System
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Username Input */}
|
||||||
|
<Paper sx={{ p: 2, mb: 3 }}>
|
||||||
|
<Grid container spacing={2} alignItems="center">
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Candidate Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
loadSessions();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={loadSessions}
|
||||||
|
disabled={loading}
|
||||||
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||||
|
>
|
||||||
|
Load Sessions
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Sessions Sidebar */}
|
||||||
|
<Grid size={{ xs: 12, md: 4 }}>
|
||||||
|
<Paper sx={{ p: 2, height: '600px', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<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 || !username}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
New Session
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||||
|
{sessions ? (
|
||||||
|
<List>
|
||||||
|
{sessions.sessions.data.map((session : any) => (
|
||||||
|
<ListItem
|
||||||
|
key={session.id}
|
||||||
|
// selected={currentSessionId === session.id}
|
||||||
|
onClick={() => setCurrentSessionId(session.id)}
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: currentSessionId === session.id ? 'primary.main' : 'divider',
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.hover'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={session.title}
|
||||||
|
secondary={`${new Date(session.lastActivity).toLocaleDateString()} • ${session.context.type}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<Typography color="text.secondary" align="center">
|
||||||
|
Enter a username and click "Load Sessions"
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{sessions && (
|
||||||
|
<Box sx={{ mt: 2, p: 2, bgcolor: 'background.default', borderRadius: 1 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Candidate Info
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Name:</strong> {sessions.candidate.fullName}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Email:</strong> {sessions.candidate.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Chat Interface */}
|
||||||
|
<Grid size={{ xs: 12, md: 8 }}>
|
||||||
|
<Paper sx={{ height: '600px', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{currentSessionId ? (
|
||||||
|
<>
|
||||||
|
{/* Messages Area */}
|
||||||
|
<Box sx={{ flexGrow: 1, overflow: 'auto', p: 2 }}>
|
||||||
|
{messages.map((message: any) => (
|
||||||
|
<Box
|
||||||
|
key={message.id}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
mb: 2,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: message.sender === 'user' ? 'flex-end' : 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.sender === 'ai' && (
|
||||||
|
<Avatar sx={{ mr: 1, bgcolor: 'primary.main' }}>
|
||||||
|
🤖
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
maxWidth: '70%',
|
||||||
|
bgcolor: message.sender === 'user' ? 'primary.main' : 'background.paper',
|
||||||
|
color: message.sender === 'user' ? 'primary.contrastText' : 'text.primary'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{message.content}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
|
||||||
|
{new Date(message.timestamp).toLocaleTimeString()}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{message.sender === 'user' && (
|
||||||
|
<Avatar sx={{ ml: 1, bgcolor: 'secondary.main' }}>
|
||||||
|
👤
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{streaming && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Avatar sx={{ mr: 1, bgcolor: 'primary.main' }}>
|
||||||
|
🤖
|
||||||
|
</Avatar>
|
||||||
|
<Card>
|
||||||
|
<CardContent sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
|
||||||
|
<CircularProgress size={16} sx={{ mr: 1 }} />
|
||||||
|
<Typography variant="body2">AI is typing...</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Message Input */}
|
||||||
|
<Box sx={{ p: 2, display: 'flex', gap: 1 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
placeholder="Type your message about the candidate..."
|
||||||
|
value={newMessage}
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={streaming}
|
||||||
|
multiline
|
||||||
|
maxRows={4}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={!newMessage.trim() || streaming}
|
||||||
|
sx={{ minWidth: 'auto', px: 2 }}
|
||||||
|
>
|
||||||
|
▶
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h1" sx={{ fontSize: 64, color: 'text.secondary' }}>
|
||||||
|
🤖
|
||||||
|
</Typography>
|
||||||
|
<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>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CandidateChatSystem };
|
@ -39,6 +39,7 @@ import { BackstoryAppAnalysisPage } from 'documents/BackstoryAppAnalysisPage';
|
|||||||
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
|
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
|
||||||
import { UserManagement } from 'documents/UserManagement';
|
import { UserManagement } from 'documents/UserManagement';
|
||||||
import { MockupPage } from 'documents/MockupPage';
|
import { MockupPage } from 'documents/MockupPage';
|
||||||
|
import { CandidateChatSystem } from 'documents/CandidateChatSystem';
|
||||||
|
|
||||||
// Sidebar navigation component using MUI components
|
// Sidebar navigation component using MUI components
|
||||||
const Sidebar: React.FC<{
|
const Sidebar: React.FC<{
|
||||||
@ -144,6 +145,7 @@ const documents : DocType[] = [
|
|||||||
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information", icon: <LayersIcon /> },
|
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information", icon: <LayersIcon /> },
|
||||||
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions", icon: <DashboardIcon /> },
|
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions", icon: <DashboardIcon /> },
|
||||||
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts", icon: <DashboardIcon /> },
|
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts", icon: <DashboardIcon /> },
|
||||||
|
{ title: "Chat Mockup", route: "mockup-chat-system", description: "Mockup of chat system", icon: <DashboardIcon /> },
|
||||||
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles", icon: <PaletteIcon /> },
|
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles", icon: <PaletteIcon /> },
|
||||||
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application", icon: <AnalyticsIcon /> },
|
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application", icon: <AnalyticsIcon /> },
|
||||||
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." },
|
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." },
|
||||||
@ -255,6 +257,8 @@ const DocsPage = (props: BackstoryPageProps) => {
|
|||||||
// Render the appropriate content based on current page
|
// Render the appropriate content based on current page
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
switch (page) {
|
switch (page) {
|
||||||
|
case 'mockup-chat-system':
|
||||||
|
return (<CandidateChatSystem />);
|
||||||
case 'ui-overview':
|
case 'ui-overview':
|
||||||
return (<BackstoryUIOverviewPage />);
|
return (<BackstoryUIOverviewPage />);
|
||||||
case 'theme-visualizer':
|
case 'theme-visualizer':
|
||||||
|
Loading…
x
Reference in New Issue
Block a user