Compare commits

...

2 Commits

Author SHA1 Message Date
474bbbed52 Rolling back prod 2025-05-28 16:31:05 -07:00
68a4ccb6d3 Hooking back up 2025-05-28 16:12:32 -07:00
33 changed files with 1202 additions and 1275 deletions

View File

@ -95,7 +95,7 @@ services:
ports: ports:
- "8081:8081" - "8081:8081"
environment: environment:
- REDIS_HOSTS=local:redis:6379 - REDIS_HOSTS=redis:6379
networks: networks:
- internal - internal
depends_on: depends_on:

View File

@ -7,8 +7,8 @@ import { backstoryTheme } from './BackstoryTheme';
import { SeverityType } from 'components/Snack'; import { SeverityType } from 'components/Snack';
import { Query } from 'types/types'; import { Query } from 'types/types';
import { ConversationHandle } from 'components/Conversation'; import { ConversationHandle } from 'components/Conversation';
import { UserProvider } from 'components/UserContext'; import { UserProvider } from 'hooks/useUser';
import { UserRoute } from 'routes/UserRoute'; import { CandidateRoute } from 'routes/CandidateRoute';
import { BackstoryLayout } from 'components/layout/BackstoryLayout'; import { BackstoryLayout } from 'components/layout/BackstoryLayout';
import './BackstoryApp.css'; import './BackstoryApp.css';
@ -17,27 +17,17 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import { connectionBase } from './utils/Global'; import { debugConversion } from 'types/conversion';
import { User, Guest, Candidate } from 'types/types';
// Cookie handling functions
const getCookie = (name: string) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift();
return null;
};
const setCookie = (name: string, value: string, days = 7) => {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
};
const BackstoryApp = () => { const BackstoryApp = () => {
const [user, setUser] = useState<User | null>(null);
const [guest, setGuest] = useState<Guest | null>(null);
const [candidate, setCandidate] = useState<Candidate | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const snackRef = useRef<any>(null); const snackRef = useRef<any>(null);
const chatRef = useRef<ConversationHandle>(null); const chatRef = useRef<ConversationHandle>(null);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const setSnack = useCallback((message: string, severity?: SeverityType) => { const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity); snackRef.current?.setSnack(message, severity);
}, [snackRef]); }, [snackRef]);
@ -48,72 +38,50 @@ const BackstoryApp = () => {
}; };
const [page, setPage] = useState<string>(""); const [page, setPage] = useState<string>("");
// Extract session ID from URL query parameter or cookie const createGuestSession = () => {
const urlParams = new URLSearchParams(window.location.search); console.log("TODO: Convert this to query the server for the session instead of generating it.");
const urlSessionId = urlParams.get('id'); const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const cookieSessionId = getCookie('session_id'); const guest: Guest = {
sessionId,
// Fetch or join session on mount createdAt: new Date(),
useEffect(() => { lastActivity: new Date(),
const fetchSession = async () => { ipAddress: 'unknown',
try { userAgent: navigator.userAgent
let response;
let newSessionId;
let action = ""
if (urlSessionId) {
// Attempt to join session from URL
response = await fetch(`${connectionBase}/api/join-session/${urlSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
newSessionId = (await response.json()).id;
action = "Joined";
} else if (cookieSessionId) {
// Attempt to join session from cookie
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
// Cookie session invalid, create new session
response = await fetch(`${connectionBase}/api/create-session`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to create session');
}
action = "Created new";
} else {
action = "Joined";
}
newSessionId = (await response.json()).id;
} else {
// Create a new session
response = await fetch(`${connectionBase}/api/create-session`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to create session');
}
action = "Created new";
newSessionId = (await response.json()).id;
}
setSessionId(newSessionId);
setSnack(`${action} session ${newSessionId}`);
// Store in cookie if user opts in
setCookie('session_id', newSessionId);
// Clear all query parameters, preserve the current path
navigate(location.pathname, { replace: true });
} catch (err) {
setSnack("" + err);
}
}; };
fetchSession(); setGuest(guest);
}, [cookieSessionId, setSnack, urlSessionId, location.pathname, navigate]); debugConversion(guest, 'Guest Session');
};
const checkExistingAuth = () => {
const token = localStorage.getItem('accessToken');
const userData = localStorage.getItem('userData');
if (token && userData) {
try {
const user = JSON.parse(userData);
// Convert dates back to Date objects if they're stored as strings
if (user.createdAt && typeof user.createdAt === 'string') {
user.createdAt = new Date(user.createdAt);
}
if (user.updatedAt && typeof user.updatedAt === 'string') {
user.updatedAt = new Date(user.updatedAt);
}
if (user.lastLogin && typeof user.lastLogin === 'string') {
user.lastLogin = new Date(user.lastLogin);
}
setUser(user);
} catch (e) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('userData');
}
}
};
// Create guest session on component mount
useEffect(() => {
createGuestSession();
checkExistingAuth();
}, []);
useEffect(() => { useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/"; const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
@ -123,15 +91,14 @@ const BackstoryApp = () => {
// Render appropriate routes based on user type // Render appropriate routes based on user type
return ( return (
<ThemeProvider theme={backstoryTheme}> <ThemeProvider theme={backstoryTheme}>
<UserProvider sessionId={sessionId} setSnack={setSnack}> <UserProvider {...{ guest, user, candidate, setSnack }}>
<Routes> <Routes>
<Route path="/u/:username" element={<UserRoute sessionId={sessionId} setSnack={setSnack} />} /> <Route path="/u/:username" element={<CandidateRoute {...{ guest, candidate, setCandidate, setSnack }} />} />
{/* Static/shared routes */} {/* Static/shared routes */}
<Route <Route
path="/*" path="/*"
element={ element={
<BackstoryLayout <BackstoryLayout
sessionId={sessionId}
setSnack={setSnack} setSnack={setSnack}
page={page} page={page}
chatRef={chatRef} chatRef={chatRef}

View File

@ -95,8 +95,8 @@ const backstoryTheme = createTheme({
MuiPaper: { MuiPaper: {
styleOverrides: { styleOverrides: {
root: { root: {
padding: '2rem', // padding: '0.5rem',
borderRadius: '8px', borderRadius: '4px',
}, },
}, },
}, },

View File

@ -38,7 +38,7 @@ import {
} from './types/conversion'; } from './types/conversion';
import { import {
AuthResponse, BaseUser, Guest, Candidate AuthResponse, User, Guest, Candidate
} from './types/types' } from './types/types'
interface LoginRequest { interface LoginRequest {
@ -57,13 +57,14 @@ interface RegisterRequest {
const BackstoryTestApp: React.FC = () => { const BackstoryTestApp: React.FC = () => {
const apiClient = new ApiClient(); const apiClient = new ApiClient();
const [currentUser, setCurrentUser] = useState<BaseUser | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [guestSession, setGuestSession] = useState<Guest | null>(null); const [guestSession, setGuestSession] = useState<Guest | null>(null);
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const [phone, setPhone] = useState<E164Number | null>(null); const [phone, setPhone] = useState<E164Number | null>(null);
const name = (currentUser?.userType === 'candidate' ? (currentUser as Candidate).username : currentUser?.email) || '';
// Login form state // Login form state
const [loginForm, setLoginForm] = useState<LoginRequest>({ const [loginForm, setLoginForm] = useState<LoginRequest>({
@ -259,7 +260,7 @@ const BackstoryTestApp: React.FC = () => {
<Toolbar> <Toolbar>
<AccountCircle sx={{ mr: 2 }} /> <AccountCircle sx={{ mr: 2 }} />
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Welcome, {currentUser.username} Welcome, {name}
</Typography> </Typography>
<Button <Button
color="inherit" color="inherit"
@ -288,7 +289,7 @@ const BackstoryTestApp: React.FC = () => {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="body1" sx={{ mb: 1 }}> <Typography variant="body1" sx={{ mb: 1 }}>
<strong>Username:</strong> {currentUser.username} <strong>Username:</strong> {name}
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>

View File

@ -5,7 +5,6 @@ import { ChatSubmitQueryInterface } from './ChatQuery';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
interface BackstoryElementProps { interface BackstoryElementProps {
sessionId: string,
setSnack: SetSnackType, setSnack: SetSnackType,
submitQuery: ChatSubmitQueryInterface, submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>, sx?: SxProps<Theme>,

View File

@ -7,32 +7,28 @@ import {
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { useUser } from "./UserContext"; import { useUser } from "../hooks/useUser";
import { Candidate } from '../types/types'; import { Candidate } from '../types/types';
import { CopyBubble } from "./CopyBubble"; import { CopyBubble } from "./CopyBubble";
interface CandidateInfoProps { interface CandidateInfoProps {
sessionId: string; candidate: Candidate;
user?: Candidate;
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
}; };
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => { const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
const { user } = useUser(); const { candidate } = props;
const { const {
sx, sx,
action = '', action = '',
sessionId,
} = props; } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const candidate: Candidate | null = props.user || (user as Candidate); if (!candidate) {
return <Box>No user loaded.</Box>;
if (!candidate) { }
return <Box>No user loaded.</Box>;
}
return ( return (
<Card <Card
elevation={1} elevation={1}
@ -58,7 +54,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
maxWidth: "80px" maxWidth: "80px"
}}> }}>
<Avatar <Avatar
src={candidate.hasProfile ? `/api/u/${candidate.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''} src={candidate.hasProfile ? `/api/u/${candidate.username}/profile?timestamp=${Date.now()}` : ''}
alt={`${candidate.fullName}'s profile`} alt={`${candidate.fullName}'s profile`}
sx={{ sx={{
alignSelf: "flex-start", alignSelf: "flex-start",

View File

@ -8,18 +8,23 @@ import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from "react-spinners/PropagateLoader";
import { Message, MessageList, BackstoryMessage, MessageRoles } from './Message'; import { Message, MessageRoles } from './Message';
import { DeleteConfirmation } from 'components/DeleteConfirmation'; import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { Query } from 'types/types';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { connectionBase } from 'utils/Global'; import { connectionBase } from 'utils/Global';
import { useUser } from "components/UserContext"; import { useUser } from "hooks/useUser";
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse'; import { ApiClient, StreamingResponse } from 'types/api-client';
import { ChatMessage, ChatContext, ChatSession, AIParameters, Query } from 'types/types';
import { PaginatedResponse } from 'types/conversion';
import './Conversation.css'; import './Conversation.css';
const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establishing connection with server..." }; const defaultMessage: ChatMessage = {
status: "thinking", sender: "system", sessionId: "", timestamp: new Date(), content: ""
};
const loadingMessage: ChatMessage = { ...defaultMessage, content: "Establishing connection with server..." };
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona'; type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona';
@ -37,18 +42,17 @@ interface ConversationProps extends BackstoryElementProps {
resetLabel?: string, // Label to put on Reset button resetLabel?: string, // Label to put on Reset button
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
defaultQuery?: string, // Default text to populate the TextField input defaultQuery?: string, // Default text to populate the TextField input
preamble?: MessageList, // Messages to display at start of Conversation until Action has been invoked preamble?: ChatMessage[], // Messages to display at start of Conversation until Action has been invoked
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked
messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation messageFilter?: ((messages: ChatMessage[]) => ChatMessage[]) | undefined, // Filter callback to determine which Messages to display in Conversation
messages?: MessageList, // messages?: ChatMessage[], //
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
onResponse?: ((message: BackstoryMessage) => void) | undefined, // Event called when a query completes (provides messages) onResponse?: ((message: ChatMessage) => void) | undefined, // Event called when a query completes (provides messages)
}; };
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => { const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
const { const {
sessionId,
actionLabel, actionLabel,
defaultPrompts, defaultPrompts,
hideDefaultPrompts, hideDefaultPrompts,
@ -65,20 +69,22 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
sx, sx,
type, type,
} = props; } = props;
const { user } = useUser() const apiClient = new ApiClient();
const { candidate } = useUser()
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0); const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<MessageList>([]); const [conversation, setConversation] = useState<ChatMessage[]>([]);
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]); const conversationRef = useRef<ChatMessage[]>([]);
const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined); const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]);
const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | undefined>(undefined); const [processingMessage, setProcessingMessage] = useState<ChatMessage | undefined>(undefined);
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | undefined>(undefined);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
const [noInteractions, setNoInteractions] = useState<boolean>(true); const [noInteractions, setNoInteractions] = useState<boolean>(true);
const conversationRef = useRef<MessageList>([]);
const viewableElementRef = useRef<HTMLDivElement>(null); const viewableElementRef = useRef<HTMLDivElement>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const stopRef = useRef(false); const stopRef = useRef(false);
const controllerRef = useRef<StreamQueryController>(null); const controllerRef = useRef<StreamingResponse>(null);
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
// Keep the ref updated whenever items changes // Keep the ref updated whenever items changes
useEffect(() => { useEffect(() => {
@ -113,72 +119,76 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
}; };
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]); }, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
const fetchHistory = useCallback(async () => { useEffect(() => {
let retries = 5; if (chatSession) {
while (--retries > 0) { return;
}
const createChatSession = async () => {
try { try {
const response = await fetch(connectionBase + `/api/history/${sessionId}/${type}`, { const aiParameters: AIParameters = {
method: 'GET', name: '',
headers: { model: 'custom',
'Content-Type': 'application/json', temperature: 0.7,
}, maxTokens: -1,
}); topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date()
};
if (!response.ok) { const chatContext: ChatContext = {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`); type: "general",
} aiParameters
};
const { messages } = await response.json(); const response: ChatSession = await apiClient.createChatSession(chatContext);
setChatSession(response);
if (messages === undefined || messages.length === 0) { } catch (e) {
console.log(`History returned for ${type} from server with 0 entries`) console.error(e);
setConversation([]) setSnack("Unable to create chat session.", "error");
setNoInteractions(true);
} else {
console.log(`History returned for ${type} from server with ${messages.length} entries:`, messages)
const backstoryMessages: BackstoryMessage[] = messages;
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => {
if (backstoryMessage.status === "partial") {
return [{
...backstoryMessage,
role: "assistant",
content: backstoryMessage.response || "",
expanded: false,
expandable: true,
}]
}
return [{
role: 'user',
content: backstoryMessage.prompt || "",
}, {
...backstoryMessage,
role: ['done'].includes(backstoryMessage.status || "") ? "assistant" : backstoryMessage.status,
content: backstoryMessage.response || "",
}] as MessageList;
}));
setNoInteractions(false);
}
setProcessingMessage(undefined);
setStreamingMessage(undefined);
return;
} catch (error) {
console.error('Error generating session ID:', error);
setProcessingMessage({ role: "error", content: `Unable to obtain history from server. Retrying in 3 seconds (${retries} remain.)` });
setTimeout(() => {
setProcessingMessage(undefined);
}, 3000);
await new Promise(resolve => setTimeout(resolve, 3000));
setSnack("Unable to obtain chat history.", "error");
} }
}; };
}, [setConversation,setSnack, type, sessionId]);
createChatSession();
}, [chatSession, setChatSession]);
const getChatMessages = useCallback(async () => {
if (!chatSession || !chatSession.id) {
return;
}
try {
const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(chatSession.id);
const messages: ChatMessage[] = response.data;
setProcessingMessage(undefined);
setStreamingMessage(undefined);
if (messages.length === 0) {
console.log(`History returned with 0 entries`)
setConversation([])
setNoInteractions(true);
} else {
console.log(`History returned with ${messages.length} entries:`, messages)
setConversation(messages);
setNoInteractions(false);
}
} catch (error) {
console.error('Unable to obtain chat history', error);
setProcessingMessage({ ...defaultMessage, status: "error", content: `Unable to obtain history from server.` });
setTimeout(() => {
setProcessingMessage(undefined);
setNoInteractions(true);
}, 3000);
setSnack("Unable to obtain chat history.", "error");
}
}, [chatSession]);
// Set the initial chat history to "loading" or the welcome message if loaded. // Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => { useEffect(() => {
if (sessionId === undefined) { if (!chatSession) {
setProcessingMessage(loadingMessage); setProcessingMessage(loadingMessage);
return; return;
} }
@ -188,33 +198,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
setConversation([]); setConversation([]);
setNoInteractions(true); setNoInteractions(true);
if (user) { getChatMessages();
fetchHistory();
}
}, [fetchHistory, sessionId, setProcessing, user]);
const startCountdown = (seconds: number) => { }, [chatSession]);
if (timerRef.current) clearInterval(timerRef.current);
setCountdown(seconds);
timerRef.current = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timerRef.current);
timerRef.current = null;
return 0;
}
return prev - 1;
});
}, 1000);
};
const stopCountdown = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
setCountdown(0);
}
};
const handleEnter = (value: string) => { const handleEnter = (value: string) => {
const query: Query = { const query: Query = {
@ -227,76 +213,69 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
submitQuery: (query: Query) => { submitQuery: (query: Query) => {
processQuery(query); processQuery(query);
}, },
fetchHistory: () => { return fetchHistory(); } fetchHistory: () => { getChatMessages(); }
})); }));
const reset = async () => { // const reset = async () => {
try { // try {
const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, { // const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
method: 'PUT', // method: 'PUT',
headers: { // headers: {
'Content-Type': 'application/json', // 'Content-Type': 'application/json',
'Accept': 'application/json', // 'Accept': 'application/json',
}, // },
body: JSON.stringify({ reset: ['history'] }) // body: JSON.stringify({ reset: ['history'] })
}); // });
if (!response.ok) { // if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`); // throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
} // }
if (!response.body) { // if (!response.body) {
throw new Error('Response body is null'); // throw new Error('Response body is null');
} // }
setProcessingMessage(undefined); // setProcessingMessage(undefined);
setStreamingMessage(undefined); // setStreamingMessage(undefined);
setConversation([]); // setConversation([]);
setNoInteractions(true); // setNoInteractions(true);
} catch (e) { // } catch (e) {
setSnack("Error resetting history", "error") // setSnack("Error resetting history", "error")
console.error('Error resetting history:', e); // console.error('Error resetting history:', e);
} // }
}; // };
const cancelQuery = () => { const cancelQuery = () => {
console.log("Stop query"); console.log("Stop query");
if (controllerRef.current) { if (controllerRef.current) {
controllerRef.current.abort(); controllerRef.current.cancel();
} }
controllerRef.current = null; controllerRef.current = null;
}; };
const processQuery = (query: Query) => { const processQuery = (query: Query) => {
if (controllerRef.current) { if (controllerRef.current || !chatSession || !chatSession.id) {
return; return;
} }
const sessionId: string = chatSession.id;
setNoInteractions(false); setNoInteractions(false);
setConversation([ setConversation([
...conversationRef.current, ...conversationRef.current,
{ {
role: 'user', ...defaultMessage,
origin: type, sender: 'user',
content: query.prompt, content: query.prompt,
disableCopy: true
} }
]); ]);
setProcessing(true); setProcessing(true);
setProcessingMessage( setProcessingMessage(
{ role: 'status', content: 'Submitting request...', disableCopy: true } { ...defaultMessage, content: 'Submitting request...' }
); );
controllerRef.current = streamQueryResponse({ controllerRef.current = apiClient.sendMessageStream(sessionId, query, {
query,
type,
sessionId,
connectionBase,
onComplete: (msg) => { onComplete: (msg) => {
console.log(msg); console.log(msg);
switch (msg.status) { switch (msg.status) {
@ -307,14 +286,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
...msg, ...msg,
role: 'assistant', role: 'assistant',
origin: type, origin: type,
prompt: ['done', 'partial'].includes(msg.status || "") ? msg.prompt : '', }] as ChatMessage[]);
content: msg.response || "",
expanded: msg.status === "done" ? true : false,
expandable: msg.status === "done" ? false : true,
}] as MessageList);
startCountdown(Math.ceil(msg.remaining_time || 0));
if (msg.status === "done") { if (msg.status === "done") {
stopCountdown();
setStreamingMessage(undefined); setStreamingMessage(undefined);
setProcessingMessage(undefined); setProcessingMessage(undefined);
setProcessing(false); setProcessing(false);
@ -327,28 +300,27 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
case "error": case "error":
// Show error // Show error
setConversation([ setConversation([
...conversationRef.current, { ...conversationRef.current,
...msg, msg
role: 'error', ]);
origin: type,
content: msg.response || "",
}] as MessageList);
setProcessingMessage(msg); setProcessingMessage(msg);
setProcessing(false); setProcessing(false);
stopCountdown();
controllerRef.current = null; controllerRef.current = null;
break; break;
default: default:
setProcessingMessage({ role: (msg.status || "error") as MessageRoles, content: msg.response || "", disableCopy: true }); setProcessingMessage(msg);
break; break;
} }
}, },
onStreaming: (chunk) => { onPartialMessage: (chunk) => {
setStreamingMessage({ role: "streaming", content: chunk, disableCopy: true }); setStreamingMessage({ ...defaultMessage, status: "streaming", content: chunk });
} }
}); });
}; };
if (!chatSession) {
return (<></>);
}
return ( return (
// <Scrollable // <Scrollable
// className={`${className || ""} Conversation`} // className={`${className || ""} Conversation`}
@ -365,16 +337,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
<Box sx={{ p: 1, mt: 0, ...sx }}> <Box sx={{ p: 1, mt: 0, ...sx }}>
{ {
filteredConversation.map((message, index) => filteredConversation.map((message, index) =>
<Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery: processQuery, message, connectionBase, sessionId, setSnack, submitQuery }} /> <Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, setSnack, submitQuery }} />
) )
} }
{ {
processingMessage !== undefined && processingMessage !== undefined &&
<Message {...{ sendQuery: processQuery, connectionBase, sessionId, setSnack, message: processingMessage, submitQuery }} /> <Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: processingMessage, submitQuery }} />
} }
{ {
streamingMessage !== undefined && streamingMessage !== undefined &&
<Message {...{ sendQuery: processQuery, connectionBase, sessionId, setSnack, message: streamingMessage, submitQuery }} /> <Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: streamingMessage, submitQuery }} />
} }
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
@ -415,14 +387,14 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}> <Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
<DeleteConfirmation <DeleteConfirmation
label={resetLabel || "all data"} label={resetLabel || "all data"}
disabled={sessionId === undefined || processingMessage !== undefined || noInteractions} disabled={!chatSession || processingMessage !== undefined || noInteractions}
onDelete={() => { reset(); resetAction && resetAction(); }} /> onDelete={() => { /*reset(); resetAction && resetAction(); */ }} />
<Tooltip title={actionLabel || "Send"}> <Tooltip title={actionLabel || "Send"}>
<span style={{ display: "flex", flexGrow: 1 }}> <span style={{ display: "flex", flexGrow: 1 }}>
<Button <Button
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
disabled={sessionId === undefined || processingMessage !== undefined} disabled={!chatSession || processingMessage !== undefined}
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}> onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
{actionLabel}<SendIcon /> {actionLabel}<SendIcon />
</Button> </Button>
@ -436,7 +408,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
sx={{ display: "flex", margin: 'auto 0px' }} sx={{ display: "flex", margin: 'auto 0px' }}
size="large" size="large"
edge="start" edge="start"
disabled={stopRef.current || sessionId === undefined || processing === false} disabled={stopRef.current || !chatSession || processing === false}
> >
<CancelIcon /> <CancelIcon />
</IconButton> </IconButton>

View File

@ -7,11 +7,10 @@ interface DocumentProps extends BackstoryElementProps {
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { sessionId, setSnack, submitQuery, filepath } = props; const { setSnack, submitQuery, filepath } = props;
const backstoryProps = { const backstoryProps = {
submitQuery, submitQuery,
setSnack, setSnack,
sessionId
}; };
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>("");

View File

@ -2,24 +2,25 @@ import React, { useEffect, useState, useRef } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import PropagateLoader from 'react-spinners/PropagateLoader'; import PropagateLoader from 'react-spinners/PropagateLoader';
import { Quote } from 'components/Quote'; import { Quote } from 'components/Quote';
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
import { connectionBase } from 'utils/Global';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { useUser } from 'components/UserContext'; import { useUser } from 'hooks/useUser';
import { Candidate, ChatSession } from 'types/types';
interface GenerateImageProps extends BackstoryElementProps { interface GenerateImageProps extends BackstoryElementProps {
prompt: string prompt: string;
chatSession: ChatSession;
}; };
const GenerateImage = (props: GenerateImageProps) => { const GenerateImage = (props: GenerateImageProps) => {
const { user } = useUser(); const { user } = useUser();
const {sessionId, setSnack, prompt} = props; const { setSnack, chatSession, prompt } = props;
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = useState<string>(''); const [status, setStatus] = useState<string>('');
const [image, setImage] = useState<string>(''); const [image, setImage] = useState<string>('');
const name = (user?.userType === 'candidate' ? (user as Candidate).username : user?.email) || '';
// Only keep refs that are truly necessary // Only keep refs that are truly necessary
const controllerRef = useRef<StreamQueryController>(null); const controllerRef = useRef<string>(null);
// Effect to trigger profile generation when user data is ready // Effect to trigger profile generation when user data is ready
useEffect(() => { useEffect(() => {
@ -34,56 +35,54 @@ const GenerateImage = (props: GenerateImageProps) => {
setProcessing(true); setProcessing(true);
const start = Date.now(); const start = Date.now();
controllerRef.current = streamQueryResponse({ // controllerRef.current = streamQueryResponse({
query: { // query: {
prompt: prompt, // prompt: prompt,
agentOptions: { // agentOptions: {
username: user?.username, // username: name,
} // }
}, // },
type: "image", // type: "image",
sessionId, // onComplete: (msg) => {
connectionBase, // switch (msg.status) {
onComplete: (msg) => { // case "partial":
switch (msg.status) { // case "done":
case "partial": // if (msg.status === "done") {
case "done": // if (!msg.response) {
if (msg.status === "done") { // setSnack("Image generation failed", "error");
if (!msg.response) { // } else {
setSnack("Image generation failed", "error"); // setImage(msg.response);
} else { // }
setImage(msg.response); // setProcessing(false);
} // controllerRef.current = null;
setProcessing(false); // }
controllerRef.current = null; // break;
} // case "error":
break; // console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
case "error": // setSnack(msg.response || "", "error");
console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`); // setProcessing(false);
setSnack(msg.response || "", "error"); // controllerRef.current = null;
setProcessing(false); // break;
controllerRef.current = null; // default:
break; // let data: any = {};
default: // try {
let data: any = {}; // data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
try { // } catch (e) {
data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response; // data = { message: msg.response };
} catch (e) { // }
data = { message: msg.response }; // if (msg.status !== "heartbeat") {
} // console.log(data);
if (msg.status !== "heartbeat") { // }
console.log(data); // if (data.message) {
} // setStatus(data.message);
if (data.message) { // }
setStatus(data.message); // break;
} // }
break; // }
} // });
} }, [user, prompt, setSnack]);
});
}, [user, prompt, sessionId, setSnack]);
if (!sessionId) { if (!chatSession) {
return <></>; return <></>;
} }
@ -96,7 +95,7 @@ const GenerateImage = (props: GenerateImageProps) => {
maxWidth: { xs: '100%', md: '700px', lg: '1024px' }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
minHeight: "max-content", minHeight: "max-content",
}}> }}>
{image !== '' && <img alt={prompt} src={`${image}/${sessionId}`} />} {image !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />}
{ prompt && { prompt &&
<Quote size={processing ? "normal" : "small"} quote={prompt} sx={{ "& *": { color: "#2E2E2E !important" }}}/> <Quote size={processing ? "normal" : "small"} quote={prompt} sx={{ "& *": { color: "#2E2E2E !important" }}}/>
} }

View File

@ -32,6 +32,7 @@ import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { ChatMessage, ChatSession } from 'types/types';
type MessageRoles = type MessageRoles =
'assistant' | 'assistant' |
@ -304,14 +305,15 @@ type MessageList = BackstoryMessage[];
interface MessageProps extends BackstoryElementProps { interface MessageProps extends BackstoryElementProps {
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
message: BackstoryMessage, message: ChatMessage,
expanded?: boolean, expanded?: boolean,
onExpand?: (open: boolean) => void, onExpand?: (open: boolean) => void,
className?: string, className?: string,
chatSession?: ChatSession,
}; };
interface MessageMetaProps { interface MessageMetaProps {
metadata: MessageMetaData, metadata: Record<string, any>,
messageProps: MessageProps messageProps: MessageProps
}; };
@ -446,12 +448,11 @@ const MessageMeta = (props: MessageMetaProps) => {
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, sx, className, onExpand, setSnack, sessionId, expanded } = props; const { message, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false); const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const textFieldRef = useRef(null); const textFieldRef = useRef(null);
const backstoryProps = { const backstoryProps = {
submitQuery, submitQuery,
sessionId,
setSnack setSnack
}; };
@ -475,7 +476,8 @@ const Message = (props: MessageProps) => {
return ( return (
<ChatBubble <ChatBubble
className={`${className || ""} Message Message-${message.role}`} role='assistant'
className={`${className || ""} Message Message-${message.sender}`}
{...message} {...message}
expanded={expanded} expanded={expanded}
onExpand={onExpand} onExpand={onExpand}
@ -503,11 +505,11 @@ const Message = (props: MessageProps) => {
overflow: "auto", /* Handles scrolling for the div */ overflow: "auto", /* Handles scrolling for the div */
}} }}
> >
<StyledMarkdown streaming={message.role === "streaming"} content={formattedContent} {...backstoryProps} /> <StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={formattedContent} {...backstoryProps} />
</Scrollable> </Scrollable>
</CardContent> </CardContent>
<CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}> <CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}>
{(message.disableCopy === undefined || message.disableCopy === false) && <CopyBubble content={message.content} />} {/*(message.disableCopy === undefined || message.disableCopy === false) &&*/ <CopyBubble content={message.content} />}
{message.metadata && ( {message.metadata && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}> <Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
@ -516,7 +518,7 @@ const Message = (props: MessageProps) => {
<ExpandMore <ExpandMore
expand={metaExpanded} expand={metaExpanded}
onClick={handleMetaExpandClick} onClick={handleMetaExpandClick}
aria-expanded={message.expanded} aria-expanded={true /*message.expanded*/}
aria-label="show more" aria-label="show more"
> >
<ExpandMoreIcon /> <ExpandMoreIcon />

View File

@ -13,15 +13,17 @@ import { GenerateImage } from './GenerateImage';
import './StyledMarkdown.css'; import './StyledMarkdown.css';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { ChatSession } from 'types/types';
interface StyledMarkdownProps extends BackstoryElementProps { interface StyledMarkdownProps extends BackstoryElementProps {
className?: string, className?: string,
content: string, content: string,
streaming?: boolean, streaming?: boolean,
chatSession?: ChatSession,
}; };
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => { const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
const { className, sessionId, content, submitQuery, sx, streaming, setSnack } = props; const { className, content, chatSession, submitQuery, sx, streaming, setSnack } = props;
const theme = useTheme(); const theme = useTheme();
const overrides: any = { const overrides: any = {
@ -77,7 +79,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
props: { props: {
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => { onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
const href = event.currentTarget.getAttribute('href'); const href = event.currentTarget.getAttribute('href');
console.log("StyledMarkdown onClick:", href, sessionId); console.log("StyledMarkdown onClick:", href);
if (href) { if (href) {
if (href.match(/^\//)) { if (href.match(/^\//)) {
event.preventDefault(); event.preventDefault();
@ -108,19 +110,22 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
return props.query; return props.query;
} }
}, },
}, }
GenerateImage: { };
if (chatSession) {
overrides.GenerateImage = {
component: (props: { prompt: string }) => { component: (props: { prompt: string }) => {
const prompt = props.prompt.replace(/(\w+):/g, '"$1":'); const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
try { try {
return <GenerateImage prompt={prompt} {...{sessionId, submitQuery, setSnack}}/> return <GenerateImage {...{ chatSession, prompt, submitQuery, setSnack }} />
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", prompt, e); console.log("StyledMarkdown error:", prompt, e);
return props.prompt; return props.prompt;
} }
}, }
}, }
}; }
return <Box return <Box
className={`MuiMarkdown ${className || ""}`} className={`MuiMarkdown ${className || ""}`}

View File

@ -1,71 +0,0 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import { SetSnackType } from './Snack';
import { connectionBase } from '../utils/Global';
import { User } from '../types/types';
type UserContextType = {
user: User | null;
setUser: (user: User | null) => void;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useUser must be used within a UserProvider");
return ctx;
};
interface UserProviderProps {
children: React.ReactNode;
sessionId: string | undefined;
setSnack: SetSnackType;
};
const UserProvider: React.FC<UserProviderProps> = (props: UserProviderProps) => {
const { sessionId, children, setSnack } = props;
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
if (!sessionId || user) {
return;
}
const fetchUserFromSession = async (): Promise<User | null> => {
try {
let response;
response = await fetch(`${connectionBase}/api/user/${sessionId}`, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
const user: User = {
...(await response.json()),
};
console.log("Loaded user:", user);
setUser(user);
} catch (err) {
setSnack("" + err);
setUser(null);
}
return null;
};
fetchUserFromSession();
}, [sessionId, user, setUser, setSnack]);
if (sessionId === undefined) {
return <></>;
}
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export {
UserProvider,
useUser
};

View File

@ -187,7 +187,7 @@ type Node = {
}; };
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => { const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
const { sessionId, setSnack, rag, inline, sx } = props; const { setSnack, rag, inline, sx } = props;
const [plotData, setPlotData] = useState<PlotData | null>(null); const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>(''); const [newQuery, setNewQuery] = useState<string>('');
const [querySet, setQuerySet] = useState<QuerySet>(rag || emptyQuerySet); const [querySet, setQuerySet] = useState<QuerySet>(rag || emptyQuerySet);
@ -225,12 +225,12 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
// Get the collection to visualize // Get the collection to visualize
useEffect(() => { useEffect(() => {
if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2)) || sessionId === undefined) { if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2))) {
return; return;
} }
const fetchCollection = async () => { const fetchCollection = async () => {
try { try {
const response = await fetch(connectionBase + `/api/umap/${sessionId}`, { const response = await fetch(connectionBase + `/api/umap/`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -247,7 +247,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
}; };
fetchCollection(); fetchCollection();
}, [result, setSnack, sessionId, view2D]) }, [result, setSnack, view2D])
useEffect(() => { useEffect(() => {
if (!result || !result.embeddings) return; if (!result || !result.embeddings) return;
@ -389,7 +389,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
if (!query.trim()) return; if (!query.trim()) return;
setNewQuery(''); setNewQuery('');
try { try {
const response = await fetch(`${connectionBase}/api/similarity/${sessionId}`, { const response = await fetch(`${connectionBase}/api/similarity/`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -407,7 +407,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
}; };
}; };
if (!plotData || sessionId === undefined) return ( if (!plotData) return (
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}> <Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
<div>Loading visualization...</div> <div>Loading visualization...</div>
</Box> </Box>
@ -415,7 +415,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const fetchRAGMeta = async (node: Node) => { const fetchRAGMeta = async (node: Node) => {
try { try {
const response = await fetch(connectionBase + `/api/umap/entry/${node.id}/${sessionId}`, { const response = await fetch(connectionBase + `/api/umap/entry/${node.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -15,7 +15,7 @@ import { Header } from 'components/layout/Header';
import { Scrollable } from 'components/Scrollable'; import { Scrollable } from 'components/Scrollable';
import { Footer } from 'components/layout/Footer'; import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from 'components/Snack'; import { Snack, SetSnackType } from 'components/Snack';
import { useUser } from 'components/UserContext'; import { useUser } from 'hooks/useUser';
import { User } from 'types/types'; import { User } from 'types/types';
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes'; import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
import { LoadingComponent } from "components/LoadingComponent"; import { LoadingComponent } from "components/LoadingComponent";
@ -122,16 +122,15 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
} }
const BackstoryLayout: React.FC<{ const BackstoryLayout: React.FC<{
sessionId: string | undefined;
setSnack: SetSnackType; setSnack: SetSnackType;
page: string; page: string;
chatRef: React.Ref<any>; chatRef: React.Ref<any>;
snackRef: React.Ref<any>; snackRef: React.Ref<any>;
submitQuery: any; submitQuery: any;
}> = ({ sessionId, setSnack, page, chatRef, snackRef, submitQuery }) => { }> = ({ setSnack, page, chatRef, snackRef, submitQuery }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { user } = useUser(); const { user, guest, candidate } = useUser();
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]); const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
useEffect(() => { useEffect(() => {
@ -139,18 +138,18 @@ const BackstoryLayout: React.FC<{
}, [user]); }, [user]);
let dynamicRoutes; let dynamicRoutes;
if (sessionId) { if (guest) {
dynamicRoutes = getBackstoryDynamicRoutes({ dynamicRoutes = getBackstoryDynamicRoutes({
sessionId, user,
setSnack, setSnack,
submitQuery, submitQuery,
chatRef chatRef
}, user); });
} }
return ( return (
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}> <Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}>
<Header {...{ setSnack, sessionId, user, currentPath: page, navigate, navigationLinks }} /> <Header {...{ setSnack, guest, user, candidate, currentPath: page, navigate, navigationLinks }} />
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
width: "100%", width: "100%",
@ -178,7 +177,7 @@ const BackstoryLayout: React.FC<{
}} }}
> >
<BackstoryPageContainer> <BackstoryPageContainer>
{!sessionId && {!guest &&
<Box> <Box>
<LoadingComponent <LoadingComponent
loadingText="Creating session..." loadingText="Creating session..."
@ -187,7 +186,7 @@ const BackstoryLayout: React.FC<{
fadeDuration={1200} /> fadeDuration={1200} />
</Box> </Box>
} }
{sessionId && <> {guest && <>
<Outlet /> <Outlet />
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>} {dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
</> </>

View File

@ -13,7 +13,7 @@ import { CreateProfilePage } from 'pages/CreateProfilePage';
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage'; import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
import { HomePage } from 'pages/HomePage'; import { HomePage } from 'pages/HomePage';
import { BetaPage } from 'pages/BetaPage'; import { BetaPage } from 'pages/BetaPage';
import { CandidateListingPage } from 'pages/CandidateListingPage'; import { CandidateListingPage } from 'pages/FindCandidatePage';
import { JobAnalysisPage } from 'pages/JobAnalysisPage'; import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from "pages/GenerateCandidate"; import { GenerateCandidate } from "pages/GenerateCandidate";
import { ControlsPage } from 'pages/ControlsPage'; import { ControlsPage } from 'pages/ControlsPage';
@ -33,25 +33,26 @@ const LoginPage = () => (<BetaPage><Typography variant="h4">Login page...</Typog
// const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>); // const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
interface BackstoryDynamicRoutesProps extends BackstoryPageProps { interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle> chatRef: Ref<ConversationHandle>;
user?: User | null;
} }
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: User | null): ReactNode => { const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
const { sessionId, setSnack, submitQuery, chatRef } = props; const { user, setSnack, submitQuery, chatRef } = props;
let index=0 let index=0
const routes = [ const routes = [
<Route key={`${index++}`} path="/" element={<HomePage/>} />, <Route key={`${index++}`} path="/" element={<HomePage/>} />,
<Route key={`${index++}`} path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{sessionId, setSnack, submitQuery}} />} />, <Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{ setSnack, submitQuery }} />} />,
<Route key={`${index++}`} path="/job-analysis" element={<JobAnalysisPage />} />, <Route key={`${index++}`} path="/job-analysis" element={<JobAnalysisPage />} />,
<Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...{ sessionId, setSnack, submitQuery }} />} />, <Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...{ setSnack, submitQuery }} />} />,
<Route key={`${index++}`} path="/settings" element={<ControlsPage {...{ sessionId, setSnack, submitQuery }} />} />, <Route key={`${index++}`} path="/settings" element={<ControlsPage {...{ setSnack, submitQuery }} />} />,
]; ];
if (user === undefined || user === null) { if (!user) {
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />); routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage />} />); routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage />} />);
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />); routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);

View File

@ -32,12 +32,13 @@ import {
import { NavigationLinkType } from 'components/layout/BackstoryLayout'; import { NavigationLinkType } from 'components/layout/BackstoryLayout';
import { Beta } from 'components/Beta'; import { Beta } from 'components/Beta';
import 'components/layout/Header.css'; import { useUser } from 'hooks/useUser';
import { useUser } from 'components/UserContext'; import { Candidate, Employer } from 'types/types';
// import { Candidate, Employer } from '../types/types';
import { SetSnackType } from 'components/Snack'; import { SetSnackType } from 'components/Snack';
import { CopyBubble } from 'components/CopyBubble'; import { CopyBubble } from 'components/CopyBubble';
import 'components/layout/Header.css';
// Styled components // Styled components
const StyledAppBar = styled(AppBar, { const StyledAppBar = styled(AppBar, {
shouldForwardProp: (prop) => prop !== 'transparent', shouldForwardProp: (prop) => prop !== 'transparent',
@ -45,6 +46,8 @@ const StyledAppBar = styled(AppBar, {
backgroundColor: transparent ? 'transparent' : theme.palette.primary.main, backgroundColor: transparent ? 'transparent' : theme.palette.primary.main,
boxShadow: transparent ? 'none' : '', boxShadow: transparent ? 'none' : '',
transition: 'background-color 0.3s ease', transition: 'background-color 0.3s ease',
borderRadius: 0,
padding: 0,
})); }));
const NavLinksContainer = styled(Box)(({ theme }) => ({ const NavLinksContainer = styled(Box)(({ theme }) => ({
@ -96,8 +99,8 @@ interface HeaderProps {
const Header: React.FC<HeaderProps> = (props: HeaderProps) => { const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user } = useUser(); const { user } = useUser();
// const candidate: Candidate | null = (user && user.userType === "UserType.CANDIDATE") ? user as Candidate : null; const candidate: Candidate | null = (user && user.userType === "candidate") ? user as Candidate : null;
// const employer: Employer | null = (user && user.userType === "UserType.EMPLOYER") ? user as Employer : null; const employer: Employer | null = (user && user.userType === "employer") ? user as Employer : null;
const { const {
transparent = false, transparent = false,
className, className,
@ -111,6 +114,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const name = (candidate ? candidate.username : user?.email) || '';
const BackstoryLogo = () => { const BackstoryLogo = () => {
return <Typography return <Typography
variant="h6" variant="h6"
@ -299,10 +304,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
height: 32, height: 32,
bgcolor: theme.palette.secondary.main, bgcolor: theme.palette.secondary.main,
}}> }}>
{user?.username.charAt(0).toUpperCase()} {name.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}> <Box sx={{ display: { xs: 'none', sm: 'block' } }}>
{user?.username} {name}
</Box> </Box>
<ExpandMore fontSize="small" /> <ExpandMore fontSize="small" />
</UserButton> </UserButton>
@ -376,7 +381,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
position="fixed" position="fixed"
transparent={transparent} transparent={transparent}
className={className} className={className}
sx={{ overflow: "hidden" }} sx={{ overflow: "hidden" }}
> >
<Container maxWidth="xl"> <Container maxWidth="xl">
<Toolbar disableGutters> <Toolbar disableGutters>
@ -392,17 +397,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
{renderUserSection()} {renderUserSection()}
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<Tooltip title="Open Menu"> <Tooltip title="Open Menu">
<IconButton <IconButton
color="inherit" color="inherit"
aria-label="open drawer" aria-label="open drawer"
edge="end" edge="end"
onClick={handleDrawerToggle} onClick={handleDrawerToggle}
sx={{ display: { md: 'none' } }} sx={{ display: { md: 'none' } }}
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{sessionId && <CopyBubble {sessionId && <CopyBubble
tooltip="Copy link" tooltip="Copy link"

View File

@ -0,0 +1,42 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import { SetSnackType } from '../components/Snack';
import { User, Guest, Candidate } from 'types/types';
type UserContextType = {
user: User | null;
guest: Guest;
candidate: Candidate | null;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useUser must be used within a UserProvider");
return ctx;
};
interface UserProviderProps {
children: React.ReactNode;
candidate: Candidate | null;
user: User | null;
guest: Guest | null;
setSnack: SetSnackType;
};
const UserProvider: React.FC<UserProviderProps> = (props: UserProviderProps) => {
const { guest, user, children, candidate, setSnack } = props;
if (guest === null) {
return <></>;
}
return (
<UserContext.Provider value={{ candidate, user, guest }}>
{children}
</UserContext.Provider>
);
};
export {
UserProvider,
useUser
};

View File

@ -4,7 +4,7 @@ import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from './BackstoryTheme'; import { backstoryTheme } from './BackstoryTheme';
import { BrowserRouter as Router } from "react-router-dom"; import { BrowserRouter as Router } from "react-router-dom";
import { BackstoryApp } from './BackstoryApp'; import { BackstoryApp } from './BackstoryApp';
import { BackstoryTestApp } from 'TestApp'; // import { BackstoryTestApp } from 'TestApp';
import './index.css'; import './index.css';
@ -16,8 +16,8 @@ root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={backstoryTheme}> <ThemeProvider theme={backstoryTheme}>
<Router> <Router>
{/* <BackstoryApp /> */} <BackstoryApp />
<BackstoryTestApp /> {/* <BackstoryTestApp /> */}
</Router> </Router>
</ThemeProvider> </ThemeProvider>
</React.StrictMode> </React.StrictMode>

View File

@ -8,11 +8,11 @@ import { BackstoryPageProps } from '../components/BackstoryTab';
import { Conversation, ConversationHandle } from '../components/Conversation'; import { Conversation, ConversationHandle } from '../components/Conversation';
import { ChatQuery } from '../components/ChatQuery'; import { ChatQuery } from '../components/ChatQuery';
import { CandidateInfo } from 'components/CandidateInfo'; import { CandidateInfo } from 'components/CandidateInfo';
import { useUser } from "../components/UserContext"; import { useUser } from "../hooks/useUser";
import { Candidate } from "../types/types"; import { Candidate } from "../types/types";
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => { const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const { sessionId, setSnack, submitQuery } = props; const { setSnack, submitQuery } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [questions, setQuestions] = useState<React.ReactElement[]>([]); const [questions, setQuestions] = useState<React.ReactElement[]>([]);
@ -42,15 +42,14 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
} }
return ( return (
<Box> <Box>
<CandidateInfo sessionId={sessionId} action="Chat with Backstory AI about " /> <CandidateInfo candidate={candidate} action="Chat with Backstory AI about " />
<Conversation <Conversation
ref={ref} ref={ref}
{...{ {...{
multiline: true, multiline: true,
type: "chat", type: "chat",
placeholder: `What would you like to know about ${candidate?.firstName}?`, placeholder: `What would you like to know about ${candidate?.firstName}?`,
resetLabel: "chat", resetLabel: "chat",
sessionId,
setSnack, setSnack,
defaultPrompts: questions, defaultPrompts: questions,
submitQuery, submitQuery,

View File

@ -85,7 +85,7 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
}; };
const ControlsPage = (props: BackstoryPageProps) => { const ControlsPage = (props: BackstoryPageProps) => {
const { setSnack, sessionId } = props; const { setSnack } = props;
const [editSystemPrompt, setEditSystemPrompt] = useState<string>(""); const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined); const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
@ -95,12 +95,12 @@ const ControlsPage = (props: BackstoryPageProps) => {
const [serverTunables, setServerTunables] = useState<ServerTunables | undefined>(undefined); const [serverTunables, setServerTunables] = useState<ServerTunables | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (serverTunables === undefined || systemPrompt === serverTunables.system_prompt || !systemPrompt.trim() || sessionId === undefined) { if (serverTunables === undefined || systemPrompt === serverTunables.system_prompt || !systemPrompt.trim()) {
return; return;
} }
const sendSystemPrompt = async (prompt: string) => { const sendSystemPrompt = async (prompt: string) => {
try { try {
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, { const response = await fetch(connectionBase + `/api/tunables`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -122,11 +122,11 @@ const ControlsPage = (props: BackstoryPageProps) => {
sendSystemPrompt(systemPrompt); sendSystemPrompt(systemPrompt);
}, [systemPrompt, sessionId, setSnack, serverTunables]); }, [systemPrompt, setSnack, serverTunables]);
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt")[], message: string = "Update successful.") => { const reset = async (types: ("rags" | "tools" | "history" | "system_prompt")[], message: string = "Update successful.") => {
try { try {
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, { const response = await fetch(connectionBase + `/api/reset/`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -173,12 +173,12 @@ const ControlsPage = (props: BackstoryPageProps) => {
// Get the system information // Get the system information
useEffect(() => { useEffect(() => {
if (systemInfo !== undefined || sessionId === undefined) { if (systemInfo !== undefined) {
return; return;
} }
const fetchSystemInfo = async () => { const fetchSystemInfo = async () => {
try { try {
const response = await fetch(connectionBase + `/api/system-info/${sessionId}`, { const response = await fetch(connectionBase + `/api/system-info`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -207,7 +207,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
fetchSystemInfo(); fetchSystemInfo();
}, [systemInfo, setSystemInfo, setSnack, sessionId]) }, [systemInfo, setSystemInfo, setSnack])
useEffect(() => { useEffect(() => {
setEditSystemPrompt(systemPrompt.trim()); setEditSystemPrompt(systemPrompt.trim());
@ -216,7 +216,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
const toggleRag = async (tool: Tool) => { const toggleRag = async (tool: Tool) => {
tool.enabled = !tool.enabled tool.enabled = !tool.enabled
try { try {
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, { const response = await fetch(connectionBase + `/api/tunables`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -238,7 +238,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
const toggleTool = async (tool: Tool) => { const toggleTool = async (tool: Tool) => {
tool.enabled = !tool.enabled tool.enabled = !tool.enabled
try { try {
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, { const response = await fetch(connectionBase + `/api/tunables`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -259,13 +259,13 @@ const ControlsPage = (props: BackstoryPageProps) => {
// If the systemPrompt has not been set, fetch it from the server // If the systemPrompt has not been set, fetch it from the server
useEffect(() => { useEffect(() => {
if (serverTunables !== undefined || sessionId === undefined) { if (serverTunables !== undefined) {
return; return;
} }
const fetchTunables = async () => { const fetchTunables = async () => {
try { try {
// Make the fetch request with proper headers // Make the fetch request with proper headers
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, { const response = await fetch(connectionBase + `/api/tunables`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -285,7 +285,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
} }
fetchTunables(); fetchTunables();
}, [sessionId, setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags, setSnack]); }, [setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags, setSnack]);
const toggle = async (type: string, index: number) => { const toggle = async (type: string, index: number) => {
switch (type) { switch (type) {

View File

@ -66,7 +66,7 @@ const Sidebar: React.FC<{
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
borderBottom: 1, borderBottom: 1,
borderColor: 'divider' borderColor: 'divider',
}}> }}>
<Typography variant="h6" component="h2" fontWeight="bold"> <Typography variant="h6" component="h2" fontWeight="bold">
Documentation Documentation
@ -169,7 +169,7 @@ const documentTitleFromRoute = (route: string): string => {
} }
const DocsPage = (props: BackstoryPageProps) => { const DocsPage = (props: BackstoryPageProps) => {
const { sessionId, submitQuery, setSnack } = props; const { submitQuery, setSnack } = props;
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { paramPage = '' } = useParams(); const { paramPage = '' } = useParams();
@ -244,7 +244,6 @@ const DocsPage = (props: BackstoryPageProps) => {
</Box> </Box>
{page && <Document {page && <Document
filepath={`/docs/${page}.md`} filepath={`/docs/${page}.md`}
sessionId={sessionId}
submitQuery={submitQuery} submitQuery={submitQuery}
setSnack={setSnack} setSnack={setSnack}
/>} />}
@ -272,28 +271,27 @@ const DocsPage = (props: BackstoryPageProps) => {
} }
// Document grid for landing page // Document grid for landing page
return ( return (
<Paper sx={{ p: 3 }} elevation={1}> <Paper sx={{ p: 1 }} elevation={0}>
<Typography variant="h4" component="h1" gutterBottom> <Box sx={{ mb: 2 }}>
Documentation <Typography variant="h4" component="h1" gutterBottom>
</Typography> Documentation
<Typography variant="body1" color="text.secondary" paragraph> </Typography>
Select a document from the sidebar to view detailed technical information about the application. <Typography variant="body1" color="text.secondary">
</Typography> Select a document from the sidebar to view detailed technical information about the application.
</Typography>
<Grid container spacing={2}> </Box>
<Grid container spacing={1}>
{documents.map((doc, index) => { {documents.map((doc, index) => {
if (doc.route === null) return (<></>); if (doc.route === null) return (<></>);
return (<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}> return (<Grid sx={{ minWidth: "164px" }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card> <Card sx={{ minHeight: "180px" }}>
<CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}> <CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}>
<CardContent> <CardContent sx={{ display: "flex", flexDirection: "column", m: 0, p: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', flexDirection: "row", gap: 1, verticalAlign: 'top' }}>
<Box sx={{ color: 'primary.main', mr: 1.5 }}> {getDocumentIcon(doc.title)}
{getDocumentIcon(doc.title)} <Typography variant="h3" sx={{ m: "0 !important" }}>{doc.title}</Typography>
</Box>
<Typography variant="h6">{doc.title}</Typography>
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}> <Typography variant="body2" color="text.secondary">
{doc.description} {doc.description}
</Typography> </Typography>
</CardContent> </CardContent>

View File

@ -7,26 +7,21 @@ import { BackstoryPageProps } from '../components/BackstoryTab';
import { CandidateInfo } from 'components/CandidateInfo'; import { CandidateInfo } from 'components/CandidateInfo';
import { connectionBase } from '../utils/Global'; import { connectionBase } from '../utils/Global';
import { Candidate } from "../types/types"; import { Candidate } from "../types/types";
import { ApiClient } from 'types/api-client';
const CandidateListingPage = (props: BackstoryPageProps) => { const CandidateListingPage = (props: BackstoryPageProps) => {
const apiClient = new ApiClient();
const navigate = useNavigate(); const navigate = useNavigate();
const { sessionId, setSnack } = props; const { setSnack } = props;
const [candidates, setCandidates] = useState<Candidate[] | undefined>(undefined); const [candidates, setCandidates] = useState<Candidate[] | null>(null);
useEffect(() => { useEffect(() => {
if (candidates !== undefined) { if (candidates !== null) {
return; return;
} }
const fetchCandidates = async () => { const getCandidates = async () => {
try { try {
let response; const results = await apiClient.getCandidates();
response = await fetch(`${connectionBase}/api/u/${sessionId}`, { const candidates: Candidate[] = results.data;
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
const candidates: Candidate[] = await response.json();
candidates.sort((a, b) => { candidates.sort((a, b) => {
let result = a.lastName.localeCompare(b.lastName); let result = a.lastName.localeCompare(b.lastName);
if (result === 0) { if (result === 0) {
@ -44,8 +39,8 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
} }
}; };
fetchCandidates(); getCandidates();
}, [candidates, sessionId, setSnack]); }, [candidates, setSnack]);
return ( return (
<Box sx={{display: "flex", flexDirection: "column"}}> <Box sx={{display: "flex", flexDirection: "column"}}>
@ -66,7 +61,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
}} }}
sx={{ cursor: "pointer" }} sx={{ cursor: "pointer" }}
> >
<CandidateInfo sessionId={sessionId} sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} user={u} /> <CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} candidate={u} />
</Box> </Box>
)} )}
</Box> </Box>

View File

@ -14,14 +14,13 @@ import { jsonrepair } from 'jsonrepair';
import { CandidateInfo } from '../components/CandidateInfo'; import { CandidateInfo } from '../components/CandidateInfo';
import { Query } from '../types/types' import { Query } from '../types/types'
import { Quote } from 'components/Quote'; import { Quote } from 'components/Quote';
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
import { connectionBase } from 'utils/Global';
import { Candidate } from '../types/types'; import { Candidate } from '../types/types';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { StyledMarkdown } from 'components/StyledMarkdown'; import { StyledMarkdown } from 'components/StyledMarkdown';
import { Scrollable } from '../components/Scrollable'; import { Scrollable } from '../components/Scrollable';
import { Pulse } from 'components/Pulse'; import { Pulse } from 'components/Pulse';
import { StreamingResponse } from 'types/api-client';
const emptyUser: Candidate = { const emptyUser: Candidate = {
description: "[blank]", description: "[blank]",
@ -47,7 +46,7 @@ const emptyUser: Candidate = {
}; };
const GenerateCandidate = (props: BackstoryElementProps) => { const GenerateCandidate = (props: BackstoryElementProps) => {
const {sessionId, setSnack, submitQuery} = props; const { setSnack, submitQuery } = props;
const [streaming, setStreaming] = useState<string>(''); const [streaming, setStreaming] = useState<string>('');
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [user, setUser] = useState<Candidate | null>(null); const [user, setUser] = useState<Candidate | null>(null);
@ -60,7 +59,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false); const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false);
// Only keep refs that are truly necessary // Only keep refs that are truly necessary
const controllerRef = useRef<StreamQueryController>(null); const controllerRef = useRef<StreamingResponse>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const generatePersona = useCallback((query: Query) => { const generatePersona = useCallback((query: Query) => {
@ -77,69 +76,68 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setCanGenImage(false); setCanGenImage(false);
setShouldGenerateProfile(false); // Reset the flag setShouldGenerateProfile(false); // Reset the flag
controllerRef.current = streamQueryResponse({ // controllerRef.current = streamQueryResponse({
query, // query,
type: "persona", // type: "persona",
sessionId, // connectionBase,
connectionBase, // onComplete: (msg) => {
onComplete: (msg) => { // switch (msg.status) {
switch (msg.status) { // case "partial":
case "partial": // case "done":
case "done": // setState(currentState => {
setState(currentState => { // switch (currentState) {
switch (currentState) { // case 0: /* Generating persona */
case 0: /* Generating persona */ // let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
let partialUser = JSON.parse(jsonrepair((msg.response || '').trim())); // if (!partialUser.fullName) {
if (!partialUser.fullName) { // partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`;
partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`; // }
} // console.log("Setting final user data:", partialUser);
console.log("Setting final user data:", partialUser); // setUser({ ...partialUser });
setUser({ ...partialUser }); // return 1; /* Generating resume */
return 1; /* Generating resume */ // case 1: /* Generating resume */
case 1: /* Generating resume */ // setResume(msg.response || '');
setResume(msg.response || ''); // return 2; /* RAG generation */
return 2; /* RAG generation */ // case 2: /* RAG generation */
case 2: /* RAG generation */ // return 3; /* Image generation */
return 3; /* Image generation */ // default:
default: // return currentState;
return currentState; // }
} // });
});
if (msg.status === "done") { // if (msg.status === "done") {
setProcessing(false); // setProcessing(false);
setCanGenImage(true); // setCanGenImage(true);
setStatus(''); // setStatus('');
controllerRef.current = null; // controllerRef.current = null;
setState(0); // setState(0);
// Set flag to trigger profile generation after user state updates // // Set flag to trigger profile generation after user state updates
console.log("Persona generation complete, setting shouldGenerateProfile flag"); // console.log("Persona generation complete, setting shouldGenerateProfile flag");
setShouldGenerateProfile(true); // setShouldGenerateProfile(true);
} // }
break; // break;
case "thinking": // case "thinking":
setStatus(msg.response || ''); // setStatus(msg.response || '');
break; // break;
case "error": // case "error":
console.log(`Error generating persona: ${msg.response}`); // console.log(`Error generating persona: ${msg.response}`);
setSnack(msg.response || "", "error"); // setSnack(msg.response || "", "error");
setProcessing(false); // setProcessing(false);
setUser(emptyUser); // setUser(emptyUser);
controllerRef.current = null; // controllerRef.current = null;
setState(0); // setState(0);
break; // break;
} // }
}, // },
onStreaming: (chunk) => { // onStreaming: (chunk) => {
setStreaming(chunk); // setStreaming(chunk);
} // }
}); // });
}, [sessionId, setSnack]); }, [setSnack]);
const cancelQuery = useCallback(() => { const cancelQuery = useCallback(() => {
if (controllerRef.current) { if (controllerRef.current) {
controllerRef.current.abort(); controllerRef.current.cancel();
controllerRef.current = null; controllerRef.current = null;
setState(0); setState(0);
setProcessing(false); setProcessing(false);
@ -184,67 +182,67 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setState(3); setState(3);
const start = Date.now(); const start = Date.now();
controllerRef.current = streamQueryResponse({ // controllerRef.current = streamQueryResponse({
query: { // query: {
prompt: imagePrompt, // prompt: imagePrompt,
agentOptions: { // agentOptions: {
username: user?.username, // username: user?.username,
filename: "profile.png" // filename: "profile.png"
} // }
}, // },
type: "image", // type: "image",
sessionId, // sessionId,
connectionBase, // connectionBase,
onComplete: (msg) => { // onComplete: (msg) => {
// console.log("Profile generation response:", msg); // // console.log("Profile generation response:", msg);
switch (msg.status) { // switch (msg.status) {
case "partial": // case "partial":
case "done": // case "done":
if (msg.status === "done") { // if (msg.status === "done") {
setProcessing(false); // setProcessing(false);
controllerRef.current = null; // controllerRef.current = null;
setState(0); // setState(0);
setCanGenImage(true); // setCanGenImage(true);
setShouldGenerateProfile(false); // setShouldGenerateProfile(false);
setUser({ // setUser({
...(user ? user : emptyUser), // ...(user ? user : emptyUser),
hasProfile: true // hasProfile: true
}); // });
} // }
break; // break;
case "error": // case "error":
console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`); // console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
setSnack(msg.response || "", "error"); // setSnack(msg.response || "", "error");
setProcessing(false); // setProcessing(false);
controllerRef.current = null; // controllerRef.current = null;
setState(0); // setState(0);
setCanGenImage(true); // setCanGenImage(true);
setShouldGenerateProfile(false); // setShouldGenerateProfile(false);
break; // break;
default: // default:
let data: any = {}; // let data: any = {};
try { // try {
data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response; // data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
} catch (e) { // } catch (e) {
data = { message: msg.response }; // data = { message: msg.response };
} // }
if (msg.status !== "heartbeat") { // if (msg.status !== "heartbeat") {
console.log(data); // console.log(data);
} // }
if (data.timestamp) { // if (data.timestamp) {
setTimestamp(data.timestamp); // setTimestamp(data.timestamp);
} else { // } else {
setTimestamp(Date.now()) // setTimestamp(Date.now())
} // }
if (data.message) { // if (data.message) {
setStatus(data.message); // setStatus(data.message);
} // }
break; // break;
} // }
} // }
}); // });
} }
}, [shouldGenerateProfile, user, prompt, sessionId, setSnack]); }, [shouldGenerateProfile, user, prompt, setSnack]);
// Handle streaming updates based on current state // Handle streaming updates based on current state
useEffect(() => { useEffect(() => {
@ -274,10 +272,6 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
} }
}, [streaming, state]); }, [streaming, state]);
if (!sessionId) {
return <></>;
}
return ( return (
<Box className="GenerateCandidate" sx={{ <Box className="GenerateCandidate" sx={{
display: "flex", display: "flex",
@ -287,8 +281,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
maxWidth: { xs: '100%', md: '700px', lg: '1024px' }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
}}> }}>
{user && <CandidateInfo {user && <CandidateInfo
sessionId={sessionId} candidate={user}
user={user}
sx={{flexShrink: 1}}/> sx={{flexShrink: 1}}/>
} }
{ prompt && { prompt &&
@ -322,7 +315,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
}}> }}>
<Box sx={{ display: "flex", position: "relative", width: "min-content", height: "min-content" }}> <Box sx={{ display: "flex", position: "relative", width: "min-content", height: "min-content" }}>
<Avatar <Avatar
src={user?.hasProfile ? `/api/u/${user.username}/profile/${sessionId}` : ''} src={user?.hasProfile ? `/api/u/${user.username}/profile` : ''}
alt={`${user?.fullName}'s profile`} alt={`${user?.fullName}'s profile`}
sx={{ sx={{
width: 80, width: 80,
@ -339,7 +332,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
sx={{ m: 1, gap: 1, justifySelf: "flex-start", alignSelf: "center", flexGrow: 0, maxHeight: "min-content" }} sx={{ m: 1, gap: 1, justifySelf: "flex-start", alignSelf: "center", flexGrow: 0, maxHeight: "min-content" }}
variant="contained" variant="contained"
disabled={ disabled={
sessionId === undefined || processing || !canGenImage processing || !canGenImage
} }
onClick={() => { setShouldGenerateProfile(true); }}> onClick={() => { setShouldGenerateProfile(true); }}>
{user?.hasProfile ? 'Re-' : ''}Generate Picture<SendIcon /> {user?.hasProfile ? 'Re-' : ''}Generate Picture<SendIcon />
@ -351,7 +344,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
{ resume !== '' && { resume !== '' &&
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}> <Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
<Scrollable sx={{flexGrow: 1}}> <Scrollable sx={{flexGrow: 1}}>
<StyledMarkdown {...{content: resume, setSnack, sessionId, submitQuery}}/> <StyledMarkdown {...{ content: resume, setSnack, submitQuery }} />
</Scrollable> </Scrollable>
</Paper> } </Paper> }
<BackstoryTextField <BackstoryTextField
@ -367,7 +360,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
<Button <Button
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
disabled={sessionId === undefined || processing} disabled={processing}
onClick={handleSendClick}> onClick={handleSendClick}>
Generate New Persona<SendIcon /> Generate New Persona<SendIcon />
</Button> </Button>
@ -381,7 +374,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
sx={{ display: "flex", margin: 'auto 0px' }} sx={{ display: "flex", margin: 'auto 0px' }}
size="large" size="large"
edge="start" edge="start"
disabled={controllerRef.current === null || !sessionId || processing === false} disabled={controllerRef.current === null || processing === false}
> >
<CancelIcon /> <CancelIcon />
</IconButton> </IconButton>

View File

@ -1,18 +1,19 @@
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { BackstoryPageProps } from '../components/BackstoryTab'; import { BackstoryPageProps } from '../components/BackstoryTab';
import { BackstoryMessage, Message } from '../components/Message'; import { Message } from '../components/Message';
import { ChatMessage } from 'types/types';
const LoadingPage = (props: BackstoryPageProps) => { const LoadingPage = (props: BackstoryPageProps) => {
const backstoryPreamble: BackstoryMessage = { const preamble: ChatMessage = {
role: 'info', sender: 'system',
title: 'Please wait while connecting to Backstory...', status: 'done',
disableCopy: true, sessionId: '',
content: '...', content: 'Please wait while connecting to Backstory...',
expandable: false, timestamp: new Date()
} }
return <Box sx={{display: "flex", flexGrow: 1, maxWidth: "1024px", margin: "0 auto"}}> return <Box sx={{display: "flex", flexGrow: 1, maxWidth: "1024px", margin: "0 auto"}}>
<Message message={backstoryPreamble} {...props} /> <Message message={preamble} {...props} />
</Box> </Box>
}; };

View File

@ -23,7 +23,6 @@ import './ResumeBuilderPage.css';
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const { const {
sx, sx,
sessionId,
setSnack, setSnack,
submitQuery, submitQuery,
} = props } = props
@ -196,191 +195,194 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
setHasFacts(false); setHasFacts(false);
}, [setHasFacts]); }, [setHasFacts]);
const renderJobDescriptionView = useCallback((sx?: SxProps) => { return (<Box>Not re-implmented yet</Box>);
console.log('renderJobDescriptionView');
const jobDescriptionQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery query={{ prompt: "What are the key skills necessary for this position?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} />
<ChatQuery query={{ prompt: "How much should this position pay (accounting for inflation)?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} />
</Box>,
];
const jobDescriptionPreamble: MessageList = [{
role: 'info',
content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
1. **Job Analysis**: LLM extracts requirements from '\`Job Description\`' to generate a list of desired '\`Skills\`'.
2. **Candidate Analysis**: LLM determines candidate qualifications by performing skill assessments.
For each '\`Skill\`' from **Job Analysis** phase:
1. **RAG**: Retrieval Augmented Generation collection is queried for context related content for each '\`Skill\`'.
2. **Evidence Creation**: LLM is queried to generate supporting evidence of '\`Skill\`' from the '\`RAG\`' and '\`Candidate Resume\`'.
3. **Resume Generation**: LLM is provided the output from the **Candidate Analysis:Evidence Creation** phase and asked to generate a professional resume.
See [About > Resume Generation Architecture](/about/resume-generation) for more details.
`,
disableCopy: true
}];
if (!hasJobDescription) { // const renderJobDescriptionView = useCallback((sx?: SxProps) => {
return <Conversation // console.log('renderJobDescriptionView');
ref={jobConversationRef} // const jobDescriptionQuestions = [
{...{ // <Box sx={{ display: "flex", flexDirection: "column" }}>
type: "job_description", // <ChatQuery query={{ prompt: "What are the key skills necessary for this position?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} />
actionLabel: "Generate Resume", // <ChatQuery query={{ prompt: "How much should this position pay (accounting for inflation)?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} />
preamble: jobDescriptionPreamble, // </Box>,
hidePreamble: true, // ];
placeholder: "Paste a job description, then click Generate...",
multiline: true,
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterJobDescriptionMessages,
resetAction: resetJobDescription,
onResponse: jobResponse,
sessionId,
setSnack,
submitQuery,
sx,
}}
/>
} else { // const jobDescriptionPreamble: MessageList = [{
return <Conversation // role: 'info',
ref={jobConversationRef} // content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
{...{
type: "job_description",
actionLabel: "Send",
placeholder: "Ask a question about this job description...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterJobDescriptionMessages,
defaultPrompts: jobDescriptionQuestions,
resetAction: resetJobDescription,
onResponse: jobResponse,
sessionId,
setSnack,
submitQuery,
sx,
}}
/>
}
}, [filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume, submitQuery]);
/** // 1. **Job Analysis**: LLM extracts requirements from '\`Job Description\`' to generate a list of desired '\`Skills\`'.
* Renders the resume view with loading indicator // 2. **Candidate Analysis**: LLM determines candidate qualifications by performing skill assessments.
*/
const renderResumeView = useCallback((sx?: SxProps) => {
const resumeQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery query={{ prompt: "Is this resume a good fit for the provided job description?", tunables: { enableTools: false } }} submitQuery={handleResumeQuery} />
<ChatQuery query={{ prompt: "Provide a more concise resume.", tunables: { enableTools: false } }} submitQuery={handleResumeQuery} />
</Box>,
];
if (!hasFacts) { // For each '\`Skill\`' from **Job Analysis** phase:
return <Conversation
ref={resumeConversationRef}
{...{
type: "resume",
actionLabel: "Fact Check",
defaultQuery: "Fact check the resume.",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterResumeMessages,
onResponse: resumeResponse,
resetAction: resetResume,
sessionId,
setSnack,
submitQuery,
sx,
}}
/>
} else {
return <Conversation
ref={resumeConversationRef}
{...{
type: "resume",
actionLabel: "Send",
placeholder: "Ask a question about this job resume...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterResumeMessages,
onResponse: resumeResponse,
resetAction: resetResume,
sessionId,
setSnack,
defaultPrompts: resumeQuestions,
submitQuery,
sx,
}}
/>
}
}, [filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume, submitQuery]);
/** // 1. **RAG**: Retrieval Augmented Generation collection is queried for context related content for each '\`Skill\`'.
* Renders the fact check view // 2. **Evidence Creation**: LLM is queried to generate supporting evidence of '\`Skill\`' from the '\`RAG\`' and '\`Candidate Resume\`'.
*/ // 3. **Resume Generation**: LLM is provided the output from the **Candidate Analysis:Evidence Creation** phase and asked to generate a professional resume.
const renderFactCheckView = useCallback((sx?: SxProps) => {
const factsQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enableTools: false } }} submitQuery={handleFactsQuery} />
</Box>,
];
return <Conversation // See [About > Resume Generation Architecture](/about/resume-generation) for more details.
ref={factsConversationRef} // `,
{...{ // disableCopy: true
type: "fact_check", // }];
actionLabel: "Send",
placeholder: "Ask a question about any discrepencies...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterFactsMessages,
defaultPrompts: factsQuestions,
resetAction: resetFacts,
onResponse: factsResponse,
sessionId,
submitQuery,
setSnack,
sx,
}}
/>
}, [ sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts, submitQuery]);
return (
<Box className="ResumeBuilder"
sx={{
p: 0,
m: 0,
display: "flex",
flexGrow: 1,
margin: "0 auto",
overflow: "hidden",
backgroundColor: "#F5F5F5",
flexDirection: "column",
maxWidth: "1024px",
}}
>
{/* Tabs */}
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="fullWidth"
sx={{ bgcolor: 'background.paper' }}
>
<Tab value={0} label="Job Description" />
{hasResume && <Tab value={1} label="Resume" />}
{hasFacts && <Tab value={2} label="Fact Check" />}
</Tabs>
{/* Document display area */} // if (!hasJobDescription) {
<Box sx={{ // return <Conversation
display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx, // ref={jobConversationRef}
overflow: "hidden" // {...{
}}> // type: "job_description",
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box> // actionLabel: "Generate Resume",
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box> // preamble: jobDescriptionPreamble,
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box> // hidePreamble: true,
</Box> // placeholder: "Paste a job description, then click Generate...",
</Box> // multiline: true,
); // resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
// messageFilter: filterJobDescriptionMessages,
// resetAction: resetJobDescription,
// onResponse: jobResponse,
// sessionId,
// setSnack,
// submitQuery,
// sx,
// }}
// />
// } else {
// return <Conversation
// ref={jobConversationRef}
// {...{
// type: "job_description",
// actionLabel: "Send",
// placeholder: "Ask a question about this job description...",
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
// messageFilter: filterJobDescriptionMessages,
// defaultPrompts: jobDescriptionQuestions,
// resetAction: resetJobDescription,
// onResponse: jobResponse,
// sessionId,
// setSnack,
// submitQuery,
// sx,
// }}
// />
// }
// }, [filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume, submitQuery]);
// /**
// * Renders the resume view with loading indicator
// */
// const renderResumeView = useCallback((sx?: SxProps) => {
// const resumeQuestions = [
// <Box sx={{ display: "flex", flexDirection: "column" }}>
// <ChatQuery query={{ prompt: "Is this resume a good fit for the provided job description?", tunables: { enableTools: false } }} submitQuery={handleResumeQuery} />
// <ChatQuery query={{ prompt: "Provide a more concise resume.", tunables: { enableTools: false } }} submitQuery={handleResumeQuery} />
// </Box>,
// ];
// if (!hasFacts) {
// return <Conversation
// ref={resumeConversationRef}
// {...{
// type: "resume",
// actionLabel: "Fact Check",
// defaultQuery: "Fact check the resume.",
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
// messageFilter: filterResumeMessages,
// onResponse: resumeResponse,
// resetAction: resetResume,
// sessionId,
// setSnack,
// submitQuery,
// sx,
// }}
// />
// } else {
// return <Conversation
// ref={resumeConversationRef}
// {...{
// type: "resume",
// actionLabel: "Send",
// placeholder: "Ask a question about this job resume...",
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
// messageFilter: filterResumeMessages,
// onResponse: resumeResponse,
// resetAction: resetResume,
// sessionId,
// setSnack,
// defaultPrompts: resumeQuestions,
// submitQuery,
// sx,
// }}
// />
// }
// }, [filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume, submitQuery]);
// /**
// * Renders the fact check view
// */
// const renderFactCheckView = useCallback((sx?: SxProps) => {
// const factsQuestions = [
// <Box sx={{ display: "flex", flexDirection: "column" }}>
// <ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enableTools: false } }} submitQuery={handleFactsQuery} />
// </Box>,
// ];
// return <Conversation
// ref={factsConversationRef}
// {...{
// type: "fact_check",
// actionLabel: "Send",
// placeholder: "Ask a question about any discrepencies...",
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
// messageFilter: filterFactsMessages,
// defaultPrompts: factsQuestions,
// resetAction: resetFacts,
// onResponse: factsResponse,
// sessionId,
// submitQuery,
// setSnack,
// sx,
// }}
// />
// }, [ sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts, submitQuery]);
// return (
// <Box className="ResumeBuilder"
// sx={{
// p: 0,
// m: 0,
// display: "flex",
// flexGrow: 1,
// margin: "0 auto",
// overflow: "hidden",
// backgroundColor: "#F5F5F5",
// flexDirection: "column",
// maxWidth: "1024px",
// }}
// >
// {/* Tabs */}
// <Tabs
// value={activeTab}
// onChange={handleTabChange}
// variant="fullWidth"
// sx={{ bgcolor: 'background.paper' }}
// >
// <Tab value={0} label="Job Description" />
// {hasResume && <Tab value={1} label="Resume" />}
// {hasFacts && <Tab value={2} label="Fact Check" />}
// </Tabs>
// {/* Document display area */}
// <Box sx={{
// display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
// overflow: "hidden"
// }}>
// <Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
// <Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
// <Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
// </Box>
// </Box>
// );
}; };
export { export {

View File

@ -0,0 +1,54 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useUser } from "../hooks/useUser";
import { Box } from "@mui/material";
import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from "../components/LoadingComponent";
import { User, Guest, Candidate } from 'types/types';
import { ApiClient } from "types/api-client";
interface CandidateRouteProps {
guest?: Guest | null;
user?: User | null;
setSnack: SetSnackType,
};
const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => {
const apiClient = new ApiClient();
const { setSnack } = props;
const { username } = useParams<{ username: string }>();
const [candidate, setCandidate] = useState<Candidate|null>(null);
const navigate = useNavigate();
useEffect(() => {
if (candidate?.username === username || !username) {
return;
}
const getCandidate = async (username: string) => {
try {
const result : Candidate = await apiClient.getCandidate(username);
setCandidate(result);
navigate('/chat');
} catch {
setSnack(`Unable to obtain information for ${username}.`, "error");
}
}
getCandidate(username);
}, [candidate, username, setCandidate]);
if (candidate === null) {
return (<Box>
<LoadingComponent
loadingText="Fetching candidate information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>);
} else {
return (<></>);
}
};
export { CandidateRoute };

View File

@ -1,72 +0,0 @@
import React, { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useUser } from "../components/UserContext";
import { User } from "../types/types";
import { Box } from "@mui/material";
import { connectionBase } from "../utils/Global";
import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from "../components/LoadingComponent";
interface UserRouteProps {
sessionId?: string | null;
setSnack: SetSnackType,
};
const UserRoute: React.FC<UserRouteProps> = (props: UserRouteProps) => {
const { sessionId, setSnack } = props;
const { username } = useParams<{ username: string }>();
const { user, setUser } = useUser();
const navigate = useNavigate();
useEffect(() => {
if (!sessionId) {
return;
}
const fetchUser = async (username: string): Promise<User | null> => {
try {
let response;
response = await fetch(`${connectionBase}/api/u/${username}/${sessionId}`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
const user: User = await response.json();
console.log("Loaded user:", user);
setUser(user);
navigate('/chat');
} catch (err) {
setSnack("" + err);
setUser(null);
navigate('/');
}
return null;
};
if (user?.username !== username && username) {
fetchUser(username);
} else {
if (user?.username) {
navigate('/chat');
} else {
navigate('/');
}
}
}, [user, username, setUser, sessionId, setSnack, navigate]);
if (sessionId === undefined || !user) {
return (<Box>
<LoadingComponent
loadingText="Fetching user information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>);
} else {
return (<></>);
}
};
export { UserRoute };

View File

@ -1,161 +0,0 @@
import { BackstoryMessage } from 'components/Message';
import { Query } from 'types/types';
import { jsonrepair } from 'jsonrepair';
type StreamQueryOptions = {
query: Query;
type: string;
sessionId: string;
connectionBase: string;
onComplete: (message: BackstoryMessage) => void;
onStreaming?: (message: string) => void;
};
type StreamQueryController = {
abort: () => void
};
const streamQueryResponse = (options: StreamQueryOptions) => {
const {
query,
type,
sessionId,
connectionBase,
onComplete,
onStreaming,
} = options;
const abortController = new AbortController();
const run = async () => {
query.prompt = query.prompt.trim();
let data: any = query;
if (type === "job_description") {
data = {
prompt: "",
agent_options: {
job_description: query.prompt,
},
};
}
try {
const response = await fetch(`${connectionBase}/api/${type}/${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data),
signal: abortController.signal,
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let streaming_response = '';
const processLine = async (line: string) => {
const update = JSON.parse(jsonrepair(line));
switch (update.status) {
case "streaming":
streaming_response += update.chunk;
onStreaming?.(streaming_response);
break;
case 'error':
const errorMessage: BackstoryMessage = {
...update,
role: 'error',
origin: type,
content: update.response ?? '',
};
onComplete(errorMessage);
break;
default:
const message: BackstoryMessage = {
...update,
role: 'assistant',
origin: type,
prompt: update.prompt ?? '',
content: update.response ?? '',
expanded: update.status === 'done',
expandable: update.status !== 'done',
};
streaming_response = '';
onComplete(message);
break;
}
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
await processLine(line);
} catch (e) {
console.error('Error processing line:', e);
console.error(line);
}
}
}
if (buffer.trim()) {
try {
await processLine(buffer);
} catch (e) {
console.error('Error processing remaining buffer:', e);
}
}
} catch (error) {
if ((error as any).name === 'AbortError') {
console.log('Query aborted');
onComplete({
role: 'error',
origin: type,
content: 'Query was cancelled.',
response: error,
status: 'error',
} as BackstoryMessage);
} else {
console.error('Fetch error:', error);
onComplete({
role: 'error',
origin: type,
content: 'Unable to process query',
response: "" + error,
status: 'error',
} as BackstoryMessage);
}
}
};
run();
return {
abort: () => abortController.abort(),
};
};
export type {
StreamQueryController
};
export { streamQueryResponse };

View File

@ -1,8 +1,8 @@
/** /**
* API Client Example * Enhanced API Client with Streaming Support
* *
* This demonstrates how to use the generated types with the conversion utilities * This demonstrates how to use the generated types with the conversion utilities
* for seamless frontend-backend communication. * for seamless frontend-backend communication, including streaming responses.
*/ */
// Import generated types (from running generate_types.py) // Import generated types (from running generate_types.py)
@ -21,6 +21,40 @@ import {
PaginatedRequest PaginatedRequest
} from './conversion'; } from './conversion';
// ============================
// Streaming Types and Interfaces
// ============================
interface StreamingOptions {
onMessage?: (message: Types.ChatMessage) => void;
onPartialMessage?: (partialContent: string, messageId?: string) => void;
onComplete?: (finalMessage: Types.ChatMessage) => void;
onError?: (error: Error) => void;
onStatusChange?: (status: Types.ChatStatusType) => void;
signal?: AbortSignal;
}
interface StreamingResponse {
messageId: string;
cancel: () => void;
promise: Promise<Types.ChatMessage>;
}
interface ChatMessageChunk {
id?: string;
sessionId: string;
status: Types.ChatStatusType;
sender: Types.ChatSenderType;
content: string;
isPartial?: boolean;
timestamp: Date;
metadata?: Record<string, any>;
}
// ============================
// Enhanced API Client Class
// ============================
class ApiClient { class ApiClient {
private baseUrl: string; private baseUrl: string;
private defaultHeaders: Record<string, string>; private defaultHeaders: Record<string, string>;
@ -257,7 +291,7 @@ class ApiClient {
} }
// ============================ // ============================
// Chat Methods // Chat Methods (Enhanced with Streaming)
// ============================ // ============================
async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> { async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> {
@ -278,16 +312,186 @@ class ApiClient {
return handleApiResponse<Types.ChatSession>(response); return handleApiResponse<Types.ChatSession>(response);
} }
async sendMessage(sessionId: string, content: string): Promise<Types.ChatMessage> { /**
* Send message with standard response (non-streaming)
*/
async sendMessage(sessionId: string, query: Types.Query): Promise<Types.ChatMessage> {
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages`, { const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages`, {
method: 'POST', method: 'POST',
headers: this.defaultHeaders, headers: this.defaultHeaders,
body: JSON.stringify(formatApiRequest({ content })) body: JSON.stringify(formatApiRequest({ query }))
}); });
return handleApiResponse<Types.ChatMessage>(response); return handleApiResponse<Types.ChatMessage>(response);
} }
/**
* Send message with streaming response support
*/
sendMessageStream(
sessionId: string,
query: Types.Query,
options: StreamingOptions = {}
): StreamingResponse {
const abortController = new AbortController();
const signal = options.signal || abortController.signal;
let messageId = '';
let accumulatedContent = '';
let currentMessage: Partial<Types.ChatMessage> = {};
const promise = new Promise<Types.ChatMessage>(async (resolve, reject) => {
try {
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages/stream`, {
method: 'POST',
headers: {
...this.defaultHeaders,
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache'
},
body: JSON.stringify(formatApiRequest({ query })),
signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Response body is not readable');
}
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim() === '') continue;
try {
// Handle Server-Sent Events format
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
// Stream completed
const finalMessage: Types.ChatMessage = {
id: messageId,
sessionId,
status: 'done',
sender: currentMessage.sender || 'ai',
content: accumulatedContent,
timestamp: currentMessage.timestamp || new Date(),
...currentMessage
};
options.onComplete?.(finalMessage);
resolve(finalMessage);
return;
}
const messageChunk: ChatMessageChunk = JSON.parse(data);
// Update accumulated state
if (messageChunk.id) messageId = messageChunk.id;
if (messageChunk.content) {
accumulatedContent += messageChunk.content;
}
// Update current message properties
Object.assign(currentMessage, {
...messageChunk,
content: accumulatedContent
});
// Trigger callbacks
if (messageChunk.status) {
options.onStatusChange?.(messageChunk.status);
}
if (messageChunk.isPartial) {
options.onPartialMessage?.(messageChunk.content, messageId);
}
const currentCompleteMessage: Types.ChatMessage = {
id: messageId,
sessionId,
status: messageChunk.status,
sender: messageChunk.sender,
content: accumulatedContent,
timestamp: messageChunk.timestamp,
...currentMessage
};
options.onMessage?.(currentCompleteMessage);
}
} catch (parseError) {
console.warn('Failed to parse SSE chunk:', parseError);
// Continue processing other lines
}
}
}
} finally {
reader.releaseLock();
}
// If we get here without a [DONE] signal, create final message
const finalMessage: Types.ChatMessage = {
id: messageId || `msg_${Date.now()}`,
sessionId,
status: 'done',
sender: currentMessage.sender || 'ai',
content: accumulatedContent,
timestamp: currentMessage.timestamp || new Date(),
...currentMessage
};
options.onComplete?.(finalMessage);
resolve(finalMessage);
} catch (error) {
if (signal.aborted) {
reject(new Error('Request was aborted'));
} else {
options.onError?.(error as Error);
reject(error);
}
}
});
return {
messageId,
cancel: () => abortController.abort(),
promise
};
}
/**
* Send message with automatic streaming detection
*/
async sendMessageAuto(
sessionId: string,
query: Types.Query,
options?: StreamingOptions
): Promise<Types.ChatMessage> {
// If streaming options are provided, use streaming
if (options && (options.onMessage || options.onPartialMessage || options.onStatusChange)) {
const streamResponse = this.sendMessageStream(sessionId, query, options);
return streamResponse.promise;
}
// Otherwise, use standard response
return this.sendMessage(sessionId, query);
}
async getChatMessages(sessionId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.ChatMessage>> { async getChatMessages(sessionId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.ChatMessage>> {
const paginatedRequest = createPaginatedRequest(request); const paginatedRequest = createPaginatedRequest(request);
const params = toUrlParams(formatApiRequest(paginatedRequest)); const params = toUrlParams(formatApiRequest(paginatedRequest));
@ -333,16 +537,11 @@ class ApiClient {
// Error Handling Helper // Error Handling Helper
// ============================ // ============================
// ============================
// Error Handling Helper
// ============================
async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> { async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> {
try { try {
const response = await requestFn(); const response = await requestFn();
return await handleApiResponse<T>(response); return await handleApiResponse<T>(response);
} catch (error) { } catch (error) {
// Log error for debugging
console.error('API request failed:', error); console.error('API request failed:', error);
throw error; throw error;
} }
@ -352,28 +551,153 @@ class ApiClient {
// Utility Methods // Utility Methods
// ============================ // ============================
/**
* Update authorization token for future requests
*/
setAuthToken(token: string): void { setAuthToken(token: string): void {
this.defaultHeaders['Authorization'] = `Bearer ${token}`; this.defaultHeaders['Authorization'] = `Bearer ${token}`;
} }
/**
* Remove authorization token
*/
clearAuthToken(): void { clearAuthToken(): void {
delete this.defaultHeaders['Authorization']; delete this.defaultHeaders['Authorization'];
} }
/**
* Get current base URL
*/
getBaseUrl(): string { getBaseUrl(): string {
return this.baseUrl; return this.baseUrl;
} }
} }
// ============================
// React Hooks for Streaming
// ============================
/* React Hook Examples for Streaming Chat
import { useState, useEffect, useCallback, useRef } from 'react';
export function useStreamingChat(sessionId: string) {
const [messages, setMessages] = useState<Types.ChatMessage[]>([]);
const [currentMessage, setCurrentMessage] = useState<Types.ChatMessage | null>(null);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState<string | null>(null);
const apiClient = useApiClient();
const streamingRef = useRef<StreamingResponse | null>(null);
const sendMessage = useCallback(async (query: Types.Query) => {
setError(null);
setIsStreaming(true);
setCurrentMessage(null);
const streamingOptions: StreamingOptions = {
onMessage: (message) => {
setCurrentMessage(message);
},
onPartialMessage: (content, messageId) => {
setCurrentMessage(prev => prev ?
{ ...prev, content: prev.content + content } :
{
id: messageId || '',
sessionId,
status: 'streaming',
sender: 'ai',
content,
timestamp: new Date()
}
);
},
onStatusChange: (status) => {
setCurrentMessage(prev => prev ? { ...prev, status } : null);
},
onComplete: (finalMessage) => {
setMessages(prev => [...prev, finalMessage]);
setCurrentMessage(null);
setIsStreaming(false);
},
onError: (err) => {
setError(err.message);
setIsStreaming(false);
setCurrentMessage(null);
}
};
try {
streamingRef.current = apiClient.sendMessageStream(sessionId, query, streamingOptions);
await streamingRef.current.promise;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to send message');
setIsStreaming(false);
}
}, [sessionId, apiClient]);
const cancelStreaming = useCallback(() => {
if (streamingRef.current) {
streamingRef.current.cancel();
setIsStreaming(false);
setCurrentMessage(null);
}
}, []);
return {
messages,
currentMessage,
isStreaming,
error,
sendMessage,
cancelStreaming
};
}
// Usage in React component:
function ChatInterface({ sessionId }: { sessionId: string }) {
const {
messages,
currentMessage,
isStreaming,
error,
sendMessage,
cancelStreaming
} = useStreamingChat(sessionId);
const handleSendMessage = (text: string) => {
sendMessage(text);
};
return (
<div>
<div className="messages">
{messages.map(message => (
<div key={message.id}>
<strong>{message.sender}:</strong> {message.content}
</div>
))}
{currentMessage && (
<div className="current-message">
<strong>{currentMessage.sender}:</strong> {currentMessage.content}
{isStreaming && <span className="streaming-indicator">...</span>}
</div>
)}
</div>
{error && <div className="error">{error}</div>}
<div className="input-area">
<input
type="text"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
disabled={isStreaming}
/>
{isStreaming && (
<button onClick={cancelStreaming}>Cancel</button>
)}
</div>
</div>
);
}
*/
// ============================ // ============================
// Usage Examples // Usage Examples
// ============================ // ============================
@ -382,193 +706,54 @@ class ApiClient {
// Initialize API client // Initialize API client
const apiClient = new ApiClient(); const apiClient = new ApiClient();
// Login and set auth token // Standard message sending (non-streaming)
try { try {
const authResponse = await apiClient.login('user@example.com', 'password'); const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?');
apiClient.setAuthToken(authResponse.accessToken); console.log('Response:', message.content);
console.log('Logged in as:', authResponse.user);
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error('Failed to send message:', error);
} }
// Create a new candidate // Streaming message with callbacks
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', {
onPartialMessage: (content, messageId) => {
console.log('Partial content:', content);
// Update UI with partial content
},
onStatusChange: (status) => {
console.log('Status changed:', status);
// Update UI status indicator
},
onComplete: (finalMessage) => {
console.log('Final message:', finalMessage.content);
// Handle completed message
},
onError: (error) => {
console.error('Streaming error:', error);
// Handle error
}
});
// Can cancel the stream if needed
setTimeout(() => {
streamResponse.cancel();
}, 10000); // Cancel after 10 seconds
// Wait for completion
try { try {
const newCandidate = await apiClient.createCandidate({ const finalMessage = await streamResponse.promise;
email: 'candidate@example.com', console.log('Stream completed:', finalMessage);
status: 'active',
firstName: 'John',
lastName: 'Doe',
skills: [],
experience: [],
education: [],
preferredJobTypes: ['full-time'],
location: {
city: 'San Francisco',
country: 'USA'
},
languages: [],
certifications: []
});
console.log('Created candidate:', newCandidate);
} catch (error) { } catch (error) {
console.error('Failed to create candidate:', error); console.error('Stream failed:', error);
} }
// Search for jobs // Auto-detection: streaming if callbacks provided, standard otherwise
try { await apiClient.sendMessageAuto(sessionId, 'Quick question', {
const jobResults = await apiClient.searchJobs('software engineer', { onPartialMessage: (content) => console.log('Streaming:', content)
location: 'San Francisco', }); // Will use streaming
experienceLevel: 'senior'
});
console.log(`Found ${jobResults.total} jobs:`); await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard
jobResults.data.forEach(job => {
console.log(`- ${job.title} at ${job.location.city}`);
});
} catch (error) {
console.error('Job search failed:', error);
}
// Get paginated candidates
try {
const candidates = await apiClient.getCandidates({
page: 1,
limit: 10,
sortBy: 'createdAt',
sortOrder: 'desc',
filters: {
status: 'active',
skills: ['javascript', 'react']
}
});
console.log(`Page ${candidates.page} of ${candidates.totalPages}`);
console.log(`${candidates.data.length} candidates on this page`);
} catch (error) {
console.error('Failed to get candidates:', error);
}
// Start a chat session
try {
const chatSession = await apiClient.createChatSession({
type: 'job_search',
aiParameters: {
name: 'Job Search 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()
}
});
// Send a message
const message = await apiClient.sendMessage(
chatSession.id,
'Help me find software engineering jobs in San Francisco'
);
console.log('AI Response:', message.content);
} catch (error) {
console.error('Chat session failed:', error);
}
*/ */
// ============================ export { ApiClient }
// React Hook Examples export type { StreamingOptions, StreamingResponse, ChatMessageChunk };
// ============================
/*
// Custom hooks for React applications
import { useState, useEffect } from 'react';
export function useApiClient() {
const [client] = useState(() => new ApiClient(process.env.REACT_APP_API_URL || ''));
return client;
}
export function useCandidates(request?: Partial<PaginatedRequest>) {
const [data, setData] = useState<PaginatedResponse<Types.Candidate> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const apiClient = useApiClient();
useEffect(() => {
async function fetchCandidates() {
try {
setLoading(true);
setError(null);
const result = await apiClient.getCandidates(request);
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch candidates');
} finally {
setLoading(false);
}
}
fetchCandidates();
}, [request]);
return { data, loading, error, refetch: () => fetchCandidates() };
}
export function useJobs(request?: Partial<PaginatedRequest>) {
const [data, setData] = useState<PaginatedResponse<Types.Job> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const apiClient = useApiClient();
useEffect(() => {
async function fetchJobs() {
try {
setLoading(true);
setError(null);
const result = await apiClient.getJobs(request);
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch jobs');
} finally {
setLoading(false);
}
}
fetchJobs();
}, [request]);
return { data, loading, error, refetch: () => fetchJobs() };
}
// Usage in React component:
function CandidateList() {
const { data: candidates, loading, error } = useCandidates({
limit: 10,
sortBy: 'createdAt'
});
if (loading) return <div>Loading candidates...</div>;
if (error) return <div>Error: {error}</div>;
if (!candidates) return <div>No candidates found</div>;
return (
<div>
<h2>Candidates ({candidates.total})</h2>
{candidates.data.map(candidate => (
<div key={candidate.id}>
{candidate.firstName} {candidate.lastName} - {candidate.email}
</div>
))}
{candidates.hasMore && (
<button>Load More</button>
)}
</div>
);
}
*/
export { ApiClient };

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models // Generated TypeScript types from Pydantic models
// Source: src/models.py // Source: src/backend/models.py
// Generated on: 2025-05-28T20:34:39.642452 // Generated on: 2025-05-28T21:47:08.590102
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
@ -17,6 +17,8 @@ export type ChatContextType = "job_search" | "candidate_screening" | "interview_
export type ChatSenderType = "user" | "ai" | "system"; export type ChatSenderType = "user" | "ai" | "system";
export type ChatStatusType = "partial" | "done" | "streaming" | "thinking" | "error";
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none"; export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none";
export type DataSourceType = "document" | "website" | "api" | "database" | "internal"; export type DataSourceType = "document" | "website" | "api" | "database" | "internal";
@ -127,7 +129,7 @@ export interface Attachment {
export interface AuthResponse { export interface AuthResponse {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
user: BaseUser; user: any;
expiresAt: number; expiresAt: number;
} }
@ -148,7 +150,6 @@ export interface Authentication {
export interface BaseUser { export interface BaseUser {
id?: string; id?: string;
username: string;
email: string; email: string;
phone?: string; phone?: string;
createdAt: Date; createdAt: Date;
@ -160,7 +161,6 @@ export interface BaseUser {
export interface BaseUserWithType { export interface BaseUserWithType {
id?: string; id?: string;
username: string;
email: string; email: string;
phone?: string; phone?: string;
createdAt: Date; createdAt: Date;
@ -173,7 +173,6 @@ export interface BaseUserWithType {
export interface Candidate { export interface Candidate {
id?: string; id?: string;
username: string;
email: string; email: string;
phone?: string; phone?: string;
createdAt: Date; createdAt: Date;
@ -182,6 +181,7 @@ export interface Candidate {
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: "active" | "inactive" | "pending" | "banned";
userType?: "candidate"; userType?: "candidate";
username: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
fullName: string; fullName: string;
@ -250,6 +250,7 @@ export interface ChatContext {
export interface ChatMessage { export interface ChatMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
status: "partial" | "done" | "streaming" | "thinking" | "error";
sender: "user" | "ai" | "system"; sender: "user" | "ai" | "system";
senderId?: string; senderId?: string;
content: string; content: string;
@ -320,7 +321,6 @@ export interface Education {
export interface Employer { export interface Employer {
id?: string; id?: string;
username: string;
email: string; email: string;
phone?: string; phone?: string;
createdAt: Date; createdAt: Date;

View File

@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional, Union, get_origin, get_args
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
import stat
def run_command(command: str, description: str, cwd: str | None = None) -> bool: def run_command(command: str, description: str, cwd: str | None = None) -> bool:
"""Run a command and return success status""" """Run a command and return success status"""
try: try:
@ -431,6 +431,8 @@ Examples:
# Step 4: Write to output file # Step 4: Write to output file
with open(args.output, 'w') as f: with open(args.output, 'w') as f:
f.write(ts_content) f.write(ts_content)
# Set read-only permissions (owner can read, others can read)
os.chmod(args.output, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
file_size = len(ts_content) file_size = len(ts_content)
print(f"✅ TypeScript types generated: {args.output} ({file_size} characters)") print(f"✅ TypeScript types generated: {args.output} ({file_size} characters)")

View File

@ -403,6 +403,13 @@ async def create_candidate(
# Create candidate # Create candidate
candidate = Candidate.model_validate(candidate_data) candidate = Candidate.model_validate(candidate_data)
# Check if candidate already exists
existing_candidate = await database.get_candidate(candidate.id)
if existing_candidate:
return JSONResponse(
status_code=400,
content=create_error_response("ALREADY_EXISTS", "Candidate already exists")
)
await database.set_candidate(candidate.id, candidate.model_dump()) await database.set_candidate(candidate.id, candidate.model_dump())
# Add to users for auth (simplified) # Add to users for auth (simplified)

View File

@ -67,6 +67,13 @@ class ChatSenderType(str, Enum):
AI = "ai" AI = "ai"
SYSTEM = "system" SYSTEM = "system"
class ChatStatusType(str, Enum):
PARTIAL = "partial"
DONE = "done"
STREAMING = "streaming"
THINKING = "thinking"
ERROR = "error"
class ChatContextType(str, Enum): class ChatContextType(str, Enum):
JOB_SEARCH = "job_search" JOB_SEARCH = "job_search"
CANDIDATE_SCREENING = "candidate_screening" CANDIDATE_SCREENING = "candidate_screening"
@ -544,6 +551,7 @@ class ChatContext(BaseModel):
class ChatMessage(BaseModel): class ChatMessage(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4())) id: str = Field(default_factory=lambda: str(uuid.uuid4()))
session_id: str = Field(..., alias="sessionId") session_id: str = Field(..., alias="sessionId")
status: ChatStatusType
sender: ChatSenderType sender: ChatSenderType
sender_id: Optional[str] = Field(None, alias="senderId") sender_id: Optional[str] = Field(None, alias="senderId")
content: str content: str