Compare commits

..

No commits in common. "474bbbed52ac98d12ca090ec86984ac44ab177a2" and "f7e41c710ca777ff322ecec78f30dc3a159c07dc" have entirely different histories.

33 changed files with 1274 additions and 1201 deletions

View File

@ -95,7 +95,7 @@ services:
ports: ports:
- "8081:8081" - "8081:8081"
environment: environment:
- REDIS_HOSTS=redis:6379 - REDIS_HOSTS=local: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 'hooks/useUser'; import { UserProvider } from 'components/UserContext';
import { CandidateRoute } from 'routes/CandidateRoute'; import { UserRoute } from 'routes/UserRoute';
import { BackstoryLayout } from 'components/layout/BackstoryLayout'; import { BackstoryLayout } from 'components/layout/BackstoryLayout';
import './BackstoryApp.css'; import './BackstoryApp.css';
@ -17,17 +17,27 @@ 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 { debugConversion } from 'types/conversion'; import { connectionBase } from './utils/Global';
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]);
@ -38,50 +48,72 @@ const BackstoryApp = () => {
}; };
const [page, setPage] = useState<string>(""); const [page, setPage] = useState<string>("");
const createGuestSession = () => { // Extract session ID from URL query parameter or cookie
console.log("TODO: Convert this to query the server for the session instead of generating it."); const urlParams = new URLSearchParams(window.location.search);
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const urlSessionId = urlParams.get('id');
const guest: Guest = { const cookieSessionId = getCookie('session_id');
sessionId,
createdAt: new Date(),
lastActivity: new Date(),
ipAddress: 'unknown',
userAgent: navigator.userAgent
};
setGuest(guest);
debugConversion(guest, 'Guest Session');
};
const checkExistingAuth = () => { // Fetch or join session on mount
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(() => { useEffect(() => {
createGuestSession(); const fetchSession = async () => {
checkExistingAuth(); try {
}, []); 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();
}, [cookieSessionId, setSnack, urlSessionId, location.pathname, navigate]);
useEffect(() => { useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/"; const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
@ -91,14 +123,15 @@ 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 {...{ guest, user, candidate, setSnack }}> <UserProvider sessionId={sessionId} setSnack={setSnack}>
<Routes> <Routes>
<Route path="/u/:username" element={<CandidateRoute {...{ guest, candidate, setCandidate, setSnack }} />} /> <Route path="/u/:username" element={<UserRoute sessionId={sessionId} setSnack={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: '0.5rem', padding: '2rem',
borderRadius: '4px', borderRadius: '8px',
}, },
}, },
}, },

View File

@ -38,7 +38,7 @@ import {
} from './types/conversion'; } from './types/conversion';
import { import {
AuthResponse, User, Guest, Candidate AuthResponse, BaseUser, Guest, Candidate
} from './types/types' } from './types/types'
interface LoginRequest { interface LoginRequest {
@ -57,14 +57,13 @@ interface RegisterRequest {
const BackstoryTestApp: React.FC = () => { const BackstoryTestApp: React.FC = () => {
const apiClient = new ApiClient(); const apiClient = new ApiClient();
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<BaseUser | 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>({
@ -260,7 +259,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, {name} Welcome, {currentUser.username}
</Typography> </Typography>
<Button <Button
color="inherit" color="inherit"
@ -289,7 +288,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> {name} <strong>Username:</strong> {currentUser.username}
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>

View File

@ -5,6 +5,7 @@ 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,25 +7,29 @@ import {
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { useUser } from "../hooks/useUser"; import { useUser } from "./UserContext";
import { Candidate } from '../types/types'; import { Candidate } from '../types/types';
import { CopyBubble } from "./CopyBubble"; import { CopyBubble } from "./CopyBubble";
interface CandidateInfoProps { interface CandidateInfoProps {
candidate: Candidate; sessionId: string;
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 { candidate } = props; const { user } = useUser();
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) { if (!candidate) {
return <Box>No user loaded.</Box>; return <Box>No user loaded.</Box>;
} }
@ -54,7 +58,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
maxWidth: "80px" maxWidth: "80px"
}}> }}>
<Avatar <Avatar
src={candidate.hasProfile ? `/api/u/${candidate.username}/profile?timestamp=${Date.now()}` : ''} src={candidate.hasProfile ? `/api/u/${candidate.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''}
alt={`${candidate.fullName}'s profile`} alt={`${candidate.fullName}'s profile`}
sx={{ sx={{
alignSelf: "flex-start", alignSelf: "flex-start",

View File

@ -8,23 +8,18 @@ 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, MessageRoles } from './Message'; import { Message, MessageList, BackstoryMessage, 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 "hooks/useUser"; import { useUser } from "components/UserContext";
import { ApiClient, StreamingResponse } from 'types/api-client'; import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
import { ChatMessage, ChatContext, ChatSession, AIParameters, Query } from 'types/types';
import { PaginatedResponse } from 'types/conversion';
import './Conversation.css'; import './Conversation.css';
const defaultMessage: ChatMessage = { const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establishing connection with server..." };
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';
@ -42,17 +37,18 @@ 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?: ChatMessage[], // Messages to display at start of Conversation until Action has been invoked preamble?: MessageList, // 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: ChatMessage[]) => ChatMessage[]) | undefined, // Filter callback to determine which Messages to display in Conversation messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation
messages?: ChatMessage[], // messages?: MessageList, //
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
onResponse?: ((message: ChatMessage) => void) | undefined, // Event called when a query completes (provides messages) onResponse?: ((message: BackstoryMessage) => 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,
@ -69,22 +65,20 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
sx, sx,
type, type,
} = props; } = props;
const apiClient = new ApiClient(); const { user } = useUser()
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<ChatMessage[]>([]); const [conversation, setConversation] = useState<MessageList>([]);
const conversationRef = useRef<ChatMessage[]>([]); const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]); const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined);
const [processingMessage, setProcessingMessage] = useState<ChatMessage | undefined>(undefined); const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | 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<StreamingResponse>(null); const controllerRef = useRef<StreamQueryController>(null);
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
// Keep the ref updated whenever items changes // Keep the ref updated whenever items changes
useEffect(() => { useEffect(() => {
@ -119,76 +113,72 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
}; };
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]); }, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
useEffect(() => { const fetchHistory = useCallback(async () => {
if (chatSession) { let retries = 5;
return; while (--retries > 0) {
}
const createChatSession = async () => {
try { try {
const aiParameters: AIParameters = { const response = await fetch(connectionBase + `/api/history/${sessionId}/${type}`, {
name: '', method: 'GET',
model: 'custom', headers: {
temperature: 0.7, 'Content-Type': 'application/json',
maxTokens: -1, },
topP: 1, });
frequencyPenalty: 0,
presencePenalty: 0,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date()
};
const chatContext: ChatContext = { if (!response.ok) {
type: "general", throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
aiParameters
};
const response: ChatSession = await apiClient.createChatSession(chatContext);
setChatSession(response);
} catch (e) {
console.error(e);
setSnack("Unable to create chat session.", "error");
} }
};
createChatSession(); const { messages } = await response.json();
}, [chatSession, setChatSession]); if (messages === undefined || messages.length === 0) {
console.log(`History returned for ${type} from server with 0 entries`)
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([]) setConversation([])
setNoInteractions(true); setNoInteractions(true);
} else { } else {
console.log(`History returned with ${messages.length} entries:`, messages) console.log(`History returned for ${type} from server with ${messages.length} entries:`, messages)
setConversation(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); setNoInteractions(false);
} }
setProcessingMessage(undefined);
setStreamingMessage(undefined);
return;
} catch (error) { } catch (error) {
console.error('Unable to obtain chat history', error); console.error('Error generating session ID:', error);
setProcessingMessage({ ...defaultMessage, status: "error", content: `Unable to obtain history from server.` }); setProcessingMessage({ role: "error", content: `Unable to obtain history from server. Retrying in 3 seconds (${retries} remain.)` });
setTimeout(() => { setTimeout(() => {
setProcessingMessage(undefined); setProcessingMessage(undefined);
setNoInteractions(true);
}, 3000); }, 3000);
await new Promise(resolve => setTimeout(resolve, 3000));
setSnack("Unable to obtain chat history.", "error"); setSnack("Unable to obtain chat history.", "error");
} }
}, [chatSession]); };
}, [setConversation,setSnack, type, sessionId]);
// 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 (!chatSession) { if (sessionId === undefined) {
setProcessingMessage(loadingMessage); setProcessingMessage(loadingMessage);
return; return;
} }
@ -198,9 +188,33 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
setConversation([]); setConversation([]);
setNoInteractions(true); setNoInteractions(true);
getChatMessages(); if (user) {
fetchHistory();
}
}, [fetchHistory, sessionId, setProcessing, user]);
}, [chatSession]); const startCountdown = (seconds: number) => {
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 = {
@ -213,69 +227,76 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
submitQuery: (query: Query) => { submitQuery: (query: Query) => {
processQuery(query); processQuery(query);
}, },
fetchHistory: () => { getChatMessages(); } fetchHistory: () => { return fetchHistory(); }
})); }));
// 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.cancel(); controllerRef.current.abort();
} }
controllerRef.current = null; controllerRef.current = null;
}; };
const processQuery = (query: Query) => { const processQuery = (query: Query) => {
if (controllerRef.current || !chatSession || !chatSession.id) { if (controllerRef.current) {
return; return;
} }
const sessionId: string = chatSession.id;
setNoInteractions(false); setNoInteractions(false);
setConversation([ setConversation([
...conversationRef.current, ...conversationRef.current,
{ {
...defaultMessage, role: 'user',
sender: 'user', origin: type,
content: query.prompt, content: query.prompt,
disableCopy: true
} }
]); ]);
setProcessing(true); setProcessing(true);
setProcessingMessage( setProcessingMessage(
{ ...defaultMessage, content: 'Submitting request...' } { role: 'status', content: 'Submitting request...', disableCopy: true }
); );
controllerRef.current = apiClient.sendMessageStream(sessionId, query, { controllerRef.current = streamQueryResponse({
query,
type,
sessionId,
connectionBase,
onComplete: (msg) => { onComplete: (msg) => {
console.log(msg); console.log(msg);
switch (msg.status) { switch (msg.status) {
@ -286,8 +307,14 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
...msg, ...msg,
role: 'assistant', role: 'assistant',
origin: type, origin: type,
}] as ChatMessage[]); prompt: ['done', 'partial'].includes(msg.status || "") ? msg.prompt : '',
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);
@ -300,27 +327,28 @@ 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(msg); setProcessingMessage({ role: (msg.status || "error") as MessageRoles, content: msg.response || "", disableCopy: true });
break; break;
} }
}, },
onPartialMessage: (chunk) => { onStreaming: (chunk) => {
setStreamingMessage({ ...defaultMessage, status: "streaming", content: chunk }); setStreamingMessage({ role: "streaming", content: chunk, disableCopy: true });
} }
}); });
}; };
if (!chatSession) {
return (<></>);
}
return ( return (
// <Scrollable // <Scrollable
// className={`${className || ""} Conversation`} // className={`${className || ""} Conversation`}
@ -337,16 +365,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} {...{ chatSession, sendQuery: processQuery, message, connectionBase, setSnack, submitQuery }} /> <Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery: processQuery, message, connectionBase, sessionId, setSnack, submitQuery }} />
) )
} }
{ {
processingMessage !== undefined && processingMessage !== undefined &&
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: processingMessage, submitQuery }} /> <Message {...{ sendQuery: processQuery, connectionBase, sessionId, setSnack, message: processingMessage, submitQuery }} />
} }
{ {
streamingMessage !== undefined && streamingMessage !== undefined &&
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: streamingMessage, submitQuery }} /> <Message {...{ sendQuery: processQuery, connectionBase, sessionId, setSnack, message: streamingMessage, submitQuery }} />
} }
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
@ -387,14 +415,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={!chatSession || processingMessage !== undefined || noInteractions} disabled={sessionId === undefined || 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={!chatSession || processingMessage !== undefined} disabled={sessionId === undefined || processingMessage !== undefined}
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}> onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
{actionLabel}<SendIcon /> {actionLabel}<SendIcon />
</Button> </Button>
@ -408,7 +436,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 || !chatSession || processing === false} disabled={stopRef.current || sessionId === undefined || processing === false}
> >
<CancelIcon /> <CancelIcon />
</IconButton> </IconButton>

View File

@ -7,10 +7,11 @@ interface DocumentProps extends BackstoryElementProps {
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, filepath } = props; const { sessionId, 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,25 +2,24 @@ 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 'hooks/useUser'; import { useUser } from 'components/UserContext';
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 { setSnack, chatSession, prompt } = props; const {sessionId, setSnack, 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<string>(null); const controllerRef = useRef<StreamQueryController>(null);
// Effect to trigger profile generation when user data is ready // Effect to trigger profile generation when user data is ready
useEffect(() => { useEffect(() => {
@ -35,54 +34,56 @@ 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: name, username: user?.username,
// } }
// }, },
// type: "image", type: "image",
// onComplete: (msg) => { sessionId,
// switch (msg.status) { connectionBase,
// case "partial": onComplete: (msg) => {
// case "done": switch (msg.status) {
// if (msg.status === "done") { case "partial":
// if (!msg.response) { case "done":
// setSnack("Image generation failed", "error"); if (msg.status === "done") {
// } else { if (!msg.response) {
// setImage(msg.response); setSnack("Image generation failed", "error");
// } } else {
// setProcessing(false); setImage(msg.response);
// controllerRef.current = null; }
// } setProcessing(false);
// break; controllerRef.current = null;
// case "error": }
// console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`); break;
// setSnack(msg.response || "", "error"); case "error":
// setProcessing(false); console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
// controllerRef.current = null; setSnack(msg.response || "", "error");
// break; setProcessing(false);
// default: controllerRef.current = null;
// let data: any = {}; break;
// try { default:
// data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response; let data: any = {};
// } catch (e) { try {
// data = { message: msg.response }; data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
// } } catch (e) {
// if (msg.status !== "heartbeat") { data = { message: msg.response };
// console.log(data); }
// } if (msg.status !== "heartbeat") {
// if (data.message) { console.log(data);
// setStatus(data.message); }
// } if (data.message) {
// break; setStatus(data.message);
// } }
// } break;
// }); }
}, [user, prompt, setSnack]); }
});
}, [user, prompt, sessionId, setSnack]);
if (!chatSession) { if (!sessionId) {
return <></>; return <></>;
} }
@ -95,7 +96,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}/${chatSession.id}`} />} {image !== '' && <img alt={prompt} src={`${image}/${sessionId}`} />}
{ 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,7 +32,6 @@ 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' |
@ -305,15 +304,14 @@ type MessageList = BackstoryMessage[];
interface MessageProps extends BackstoryElementProps { interface MessageProps extends BackstoryElementProps {
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
message: ChatMessage, message: BackstoryMessage,
expanded?: boolean, expanded?: boolean,
onExpand?: (open: boolean) => void, onExpand?: (open: boolean) => void,
className?: string, className?: string,
chatSession?: ChatSession,
}; };
interface MessageMetaProps { interface MessageMetaProps {
metadata: Record<string, any>, metadata: MessageMetaData,
messageProps: MessageProps messageProps: MessageProps
}; };
@ -448,11 +446,12 @@ const MessageMeta = (props: MessageMetaProps) => {
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded } = props; const { message, submitQuery, sx, className, onExpand, setSnack, sessionId, 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
}; };
@ -476,8 +475,7 @@ const Message = (props: MessageProps) => {
return ( return (
<ChatBubble <ChatBubble
role='assistant' className={`${className || ""} Message Message-${message.role}`}
className={`${className || ""} Message Message-${message.sender}`}
{...message} {...message}
expanded={expanded} expanded={expanded}
onExpand={onExpand} onExpand={onExpand}
@ -505,11 +503,11 @@ const Message = (props: MessageProps) => {
overflow: "auto", /* Handles scrolling for the div */ overflow: "auto", /* Handles scrolling for the div */
}} }}
> >
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={formattedContent} {...backstoryProps} /> <StyledMarkdown streaming={message.role === "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 }}>
@ -518,7 +516,7 @@ const Message = (props: MessageProps) => {
<ExpandMore <ExpandMore
expand={metaExpanded} expand={metaExpanded}
onClick={handleMetaExpandClick} onClick={handleMetaExpandClick}
aria-expanded={true /*message.expanded*/} aria-expanded={message.expanded}
aria-label="show more" aria-label="show more"
> >
<ExpandMoreIcon /> <ExpandMoreIcon />

View File

@ -13,17 +13,15 @@ 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, content, chatSession, submitQuery, sx, streaming, setSnack } = props; const { className, sessionId, content, submitQuery, sx, streaming, setSnack } = props;
const theme = useTheme(); const theme = useTheme();
const overrides: any = { const overrides: any = {
@ -79,7 +77,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); console.log("StyledMarkdown onClick:", href, sessionId);
if (href) { if (href) {
if (href.match(/^\//)) { if (href.match(/^\//)) {
event.preventDefault(); event.preventDefault();
@ -110,22 +108,19 @@ 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 {...{ chatSession, prompt, submitQuery, setSnack }} /> return <GenerateImage prompt={prompt} {...{sessionId, 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

@ -0,0 +1,71 @@
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 { setSnack, rag, inline, sx } = props; const { sessionId, 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))) { if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2)) || sessionId === undefined) {
return; return;
} }
const fetchCollection = async () => { const fetchCollection = async () => {
try { try {
const response = await fetch(connectionBase + `/api/umap/`, { const response = await fetch(connectionBase + `/api/umap/${sessionId}`, {
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, view2D]) }, [result, setSnack, sessionId, 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/`, { const response = await fetch(`${connectionBase}/api/similarity/${sessionId}`, {
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) return ( if (!plotData || sessionId === undefined) 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}`, { const response = await fetch(connectionBase + `/api/umap/entry/${node.id}/${sessionId}`, {
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 'hooks/useUser'; import { useUser } from 'components/UserContext';
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,15 +122,16 @@ 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;
}> = ({ setSnack, page, chatRef, snackRef, submitQuery }) => { }> = ({ sessionId, setSnack, page, chatRef, snackRef, submitQuery }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { user, guest, candidate } = useUser(); const { user } = useUser();
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]); const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
useEffect(() => { useEffect(() => {
@ -138,18 +139,18 @@ const BackstoryLayout: React.FC<{
}, [user]); }, [user]);
let dynamicRoutes; let dynamicRoutes;
if (guest) { if (sessionId) {
dynamicRoutes = getBackstoryDynamicRoutes({ dynamicRoutes = getBackstoryDynamicRoutes({
user, sessionId,
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, guest, user, candidate, currentPath: page, navigate, navigationLinks }} /> <Header {...{ setSnack, sessionId, user, currentPath: page, navigate, navigationLinks }} />
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
width: "100%", width: "100%",
@ -177,7 +178,7 @@ const BackstoryLayout: React.FC<{
}} }}
> >
<BackstoryPageContainer> <BackstoryPageContainer>
{!guest && {!sessionId &&
<Box> <Box>
<LoadingComponent <LoadingComponent
loadingText="Creating session..." loadingText="Creating session..."
@ -186,7 +187,7 @@ const BackstoryLayout: React.FC<{
fadeDuration={1200} /> fadeDuration={1200} />
</Box> </Box>
} }
{guest && <> {sessionId && <>
<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/FindCandidatePage'; import { CandidateListingPage } from 'pages/CandidateListingPage';
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,26 +33,25 @@ 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): ReactNode => { const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: User | null): ReactNode => {
const { user, setSnack, submitQuery, chatRef } = props; const { sessionId, 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} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} submitQuery={submitQuery} />} />, <Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{ setSnack, submitQuery }} />} />, <Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{sessionId, 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 {...{ setSnack, submitQuery }} />} />, <Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...{ sessionId, setSnack, submitQuery }} />} />,
<Route key={`${index++}`} path="/settings" element={<ControlsPage {...{ setSnack, submitQuery }} />} />, <Route key={`${index++}`} path="/settings" element={<ControlsPage {...{ sessionId, setSnack, submitQuery }} />} />,
]; ];
if (!user) { if (user === undefined || user === null) {
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,13 +32,12 @@ import {
import { NavigationLinkType } from 'components/layout/BackstoryLayout'; import { NavigationLinkType } from 'components/layout/BackstoryLayout';
import { Beta } from 'components/Beta'; import { Beta } from 'components/Beta';
import { useUser } from 'hooks/useUser'; import 'components/layout/Header.css';
import { Candidate, Employer } from 'types/types'; import { useUser } from 'components/UserContext';
// 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',
@ -46,8 +45,6 @@ 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 }) => ({
@ -99,8 +96,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 === "candidate") ? user as Candidate : null; // const candidate: Candidate | null = (user && user.userType === "UserType.CANDIDATE") ? user as Candidate : null;
const employer: Employer | null = (user && user.userType === "employer") ? user as Employer : null; // const employer: Employer | null = (user && user.userType === "UserType.EMPLOYER") ? user as Employer : null;
const { const {
transparent = false, transparent = false,
className, className,
@ -114,8 +111,6 @@ 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"
@ -304,10 +299,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
height: 32, height: 32,
bgcolor: theme.palette.secondary.main, bgcolor: theme.palette.secondary.main,
}}> }}>
{name.charAt(0).toUpperCase()} {user?.username.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}> <Box sx={{ display: { xs: 'none', sm: 'block' } }}>
{name} {user?.username}
</Box> </Box>
<ExpandMore fontSize="small" /> <ExpandMore fontSize="small" />
</UserButton> </UserButton>

View File

@ -1,42 +0,0 @@
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

@ -7,21 +7,26 @@ 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 { setSnack } = props; const { sessionId, setSnack } = props;
const [candidates, setCandidates] = useState<Candidate[] | null>(null); const [candidates, setCandidates] = useState<Candidate[] | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (candidates !== null) { if (candidates !== undefined) {
return; return;
} }
const getCandidates = async () => { const fetchCandidates = async () => {
try { try {
const results = await apiClient.getCandidates(); let response;
const candidates: Candidate[] = results.data; response = await fetch(`${connectionBase}/api/u/${sessionId}`, {
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) {
@ -39,8 +44,8 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
} }
}; };
getCandidates(); fetchCandidates();
}, [candidates, setSnack]); }, [candidates, sessionId, setSnack]);
return ( return (
<Box sx={{display: "flex", flexDirection: "column"}}> <Box sx={{display: "flex", flexDirection: "column"}}>
@ -61,7 +66,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
}} }}
sx={{ cursor: "pointer" }} sx={{ cursor: "pointer" }}
> >
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} candidate={u} /> <CandidateInfo sessionId={sessionId} sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} user={u} />
</Box> </Box>
)} )}
</Box> </Box>

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 "../hooks/useUser"; import { useUser } from "../components/UserContext";
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 { setSnack, submitQuery } = props; const { sessionId, 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,7 +42,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
} }
return ( return (
<Box> <Box>
<CandidateInfo candidate={candidate} action="Chat with Backstory AI about " /> <CandidateInfo sessionId={sessionId} action="Chat with Backstory AI about " />
<Conversation <Conversation
ref={ref} ref={ref}
{...{ {...{
@ -50,6 +50,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
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 } = props; const { setSnack, sessionId } = 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()) { if (serverTunables === undefined || systemPrompt === serverTunables.system_prompt || !systemPrompt.trim() || sessionId === undefined) {
return; return;
} }
const sendSystemPrompt = async (prompt: string) => { const sendSystemPrompt = async (prompt: string) => {
try { try {
const response = await fetch(connectionBase + `/api/tunables`, { const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
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, setSnack, serverTunables]); }, [systemPrompt, sessionId, 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/`, { const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
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) { if (systemInfo !== undefined || sessionId === undefined) {
return; return;
} }
const fetchSystemInfo = async () => { const fetchSystemInfo = async () => {
try { try {
const response = await fetch(connectionBase + `/api/system-info`, { const response = await fetch(connectionBase + `/api/system-info/${sessionId}`, {
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]) }, [systemInfo, setSystemInfo, setSnack, sessionId])
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`, { const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
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`, { const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
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) { if (serverTunables !== undefined || sessionId === 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`, { const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -285,7 +285,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
} }
fetchTunables(); fetchTunables();
}, [setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags, setSnack]); }, [sessionId, 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 { submitQuery, setSnack } = props; const { sessionId, submitQuery, setSnack } = props;
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { paramPage = '' } = useParams(); const { paramPage = '' } = useParams();
@ -244,6 +244,7 @@ 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}
/>} />}
@ -271,27 +272,28 @@ const DocsPage = (props: BackstoryPageProps) => {
} }
// Document grid for landing page // Document grid for landing page
return ( return (
<Paper sx={{ p: 1 }} elevation={0}> <Paper sx={{ p: 3 }} elevation={1}>
<Box sx={{ mb: 2 }}>
<Typography variant="h4" component="h1" gutterBottom> <Typography variant="h4" component="h1" gutterBottom>
Documentation Documentation
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary" paragraph>
Select a document from the sidebar to view detailed technical information about the application. Select a document from the sidebar to view detailed technical information about the application.
</Typography> </Typography>
</Box>
<Grid container spacing={1}> <Grid container spacing={2}>
{documents.map((doc, index) => { {documents.map((doc, index) => {
if (doc.route === null) return (<></>); if (doc.route === null) return (<></>);
return (<Grid sx={{ minWidth: "164px" }} size={{ xs: 12, sm: 6, md: 4 }} key={index}> return (<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card sx={{ minHeight: "180px" }}> <Card>
<CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}> <CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}>
<CardContent sx={{ display: "flex", flexDirection: "column", m: 0, p: 1 }}> <CardContent>
<Box sx={{ display: 'flex', flexDirection: "row", gap: 1, verticalAlign: 'top' }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<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> </Box>
<Typography variant="body2" color="text.secondary"> <Typography variant="h6">{doc.title}</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
{doc.description} {doc.description}
</Typography> </Typography>
</CardContent> </CardContent>

View File

@ -14,13 +14,14 @@ 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]",
@ -46,7 +47,7 @@ const emptyUser: Candidate = {
}; };
const GenerateCandidate = (props: BackstoryElementProps) => { const GenerateCandidate = (props: BackstoryElementProps) => {
const { setSnack, submitQuery } = props; const {sessionId, 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);
@ -59,7 +60,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<StreamingResponse>(null); const controllerRef = useRef<StreamQueryController>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const generatePersona = useCallback((query: Query) => { const generatePersona = useCallback((query: Query) => {
@ -76,68 +77,69 @@ 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",
// connectionBase, sessionId,
// onComplete: (msg) => { connectionBase,
// switch (msg.status) { onComplete: (msg) => {
// case "partial": switch (msg.status) {
// case "done": case "partial":
// setState(currentState => { case "done":
// switch (currentState) { setState(currentState => {
// case 0: /* Generating persona */ switch (currentState) {
// let partialUser = JSON.parse(jsonrepair((msg.response || '').trim())); case 0: /* Generating persona */
// if (!partialUser.fullName) { let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
// partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`; if (!partialUser.fullName) {
// } partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`;
// console.log("Setting final user data:", partialUser); }
// setUser({ ...partialUser }); console.log("Setting final user data:", partialUser);
// return 1; /* Generating resume */ setUser({ ...partialUser });
// case 1: /* Generating resume */ return 1; /* Generating resume */
// setResume(msg.response || ''); case 1: /* Generating resume */
// return 2; /* RAG generation */ setResume(msg.response || '');
// case 2: /* RAG generation */ return 2; /* RAG generation */
// return 3; /* Image generation */ case 2: /* RAG generation */
// default: return 3; /* Image generation */
// return currentState; default:
// } 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);
// } }
// }); });
}, [setSnack]); }, [sessionId, setSnack]);
const cancelQuery = useCallback(() => { const cancelQuery = useCallback(() => {
if (controllerRef.current) { if (controllerRef.current) {
controllerRef.current.cancel(); controllerRef.current.abort();
controllerRef.current = null; controllerRef.current = null;
setState(0); setState(0);
setProcessing(false); setProcessing(false);
@ -182,67 +184,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",
// sessionId,
// connectionBase,
// onComplete: (msg) => {
// // console.log("Profile generation response:", msg);
// switch (msg.status) {
// case "partial":
// case "done":
// if (msg.status === "done") {
// setProcessing(false);
// controllerRef.current = null;
// setState(0);
// setCanGenImage(true);
// setShouldGenerateProfile(false);
// setUser({
// ...(user ? user : emptyUser),
// hasProfile: true
// });
// }
// break;
// case "error":
// console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
// setSnack(msg.response || "", "error");
// setProcessing(false);
// controllerRef.current = null;
// setState(0);
// setCanGenImage(true);
// setShouldGenerateProfile(false);
// break;
// default:
// let data: any = {};
// try {
// data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
// } catch (e) {
// data = { message: msg.response };
// }
// if (msg.status !== "heartbeat") {
// console.log(data);
// }
// if (data.timestamp) {
// setTimestamp(data.timestamp);
// } else {
// setTimestamp(Date.now())
// }
// if (data.message) {
// setStatus(data.message);
// }
// break;
// }
// }
// });
} }
}, [shouldGenerateProfile, user, prompt, setSnack]); },
type: "image",
sessionId,
connectionBase,
onComplete: (msg) => {
// console.log("Profile generation response:", msg);
switch (msg.status) {
case "partial":
case "done":
if (msg.status === "done") {
setProcessing(false);
controllerRef.current = null;
setState(0);
setCanGenImage(true);
setShouldGenerateProfile(false);
setUser({
...(user ? user : emptyUser),
hasProfile: true
});
}
break;
case "error":
console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
setSnack(msg.response || "", "error");
setProcessing(false);
controllerRef.current = null;
setState(0);
setCanGenImage(true);
setShouldGenerateProfile(false);
break;
default:
let data: any = {};
try {
data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
} catch (e) {
data = { message: msg.response };
}
if (msg.status !== "heartbeat") {
console.log(data);
}
if (data.timestamp) {
setTimestamp(data.timestamp);
} else {
setTimestamp(Date.now())
}
if (data.message) {
setStatus(data.message);
}
break;
}
}
});
}
}, [shouldGenerateProfile, user, prompt, sessionId, setSnack]);
// Handle streaming updates based on current state // Handle streaming updates based on current state
useEffect(() => { useEffect(() => {
@ -272,6 +274,10 @@ 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",
@ -281,7 +287,8 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
maxWidth: { xs: '100%', md: '700px', lg: '1024px' }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
}}> }}>
{user && <CandidateInfo {user && <CandidateInfo
candidate={user} sessionId={sessionId}
user={user}
sx={{flexShrink: 1}}/> sx={{flexShrink: 1}}/>
} }
{ prompt && { prompt &&
@ -315,7 +322,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` : ''} src={user?.hasProfile ? `/api/u/${user.username}/profile/${sessionId}` : ''}
alt={`${user?.fullName}'s profile`} alt={`${user?.fullName}'s profile`}
sx={{ sx={{
width: 80, width: 80,
@ -332,7 +339,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={
processing || !canGenImage sessionId === undefined || processing || !canGenImage
} }
onClick={() => { setShouldGenerateProfile(true); }}> onClick={() => { setShouldGenerateProfile(true); }}>
{user?.hasProfile ? 'Re-' : ''}Generate Picture<SendIcon /> {user?.hasProfile ? 'Re-' : ''}Generate Picture<SendIcon />
@ -344,7 +351,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, submitQuery }} /> <StyledMarkdown {...{content: resume, setSnack, sessionId, submitQuery}}/>
</Scrollable> </Scrollable>
</Paper> } </Paper> }
<BackstoryTextField <BackstoryTextField
@ -360,7 +367,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={processing} disabled={sessionId === undefined || processing}
onClick={handleSendClick}> onClick={handleSendClick}>
Generate New Persona<SendIcon /> Generate New Persona<SendIcon />
</Button> </Button>
@ -374,7 +381,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 || processing === false} disabled={controllerRef.current === null || !sessionId || processing === false}
> >
<CancelIcon /> <CancelIcon />
</IconButton> </IconButton>

View File

@ -1,19 +1,18 @@
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { BackstoryPageProps } from '../components/BackstoryTab'; import { BackstoryPageProps } from '../components/BackstoryTab';
import { Message } from '../components/Message'; import { BackstoryMessage, Message } from '../components/Message';
import { ChatMessage } from 'types/types';
const LoadingPage = (props: BackstoryPageProps) => { const LoadingPage = (props: BackstoryPageProps) => {
const preamble: ChatMessage = { const backstoryPreamble: BackstoryMessage = {
sender: 'system', role: 'info',
status: 'done', title: 'Please wait while connecting to Backstory...',
sessionId: '', disableCopy: true,
content: 'Please wait while connecting to Backstory...', content: '...',
timestamp: new Date() expandable: false,
} }
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={preamble} {...props} /> <Message message={backstoryPreamble} {...props} />
</Box> </Box>
}; };

View File

@ -23,6 +23,7 @@ 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
@ -195,194 +196,191 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
setHasFacts(false); setHasFacts(false);
}, [setHasFacts]); }, [setHasFacts]);
return (<Box>Not re-implmented yet</Box>); const renderJobDescriptionView = useCallback((sx?: SxProps) => {
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
}];
// const renderJobDescriptionView = useCallback((sx?: SxProps) => { if (!hasJobDescription) {
// console.log('renderJobDescriptionView'); return <Conversation
// const jobDescriptionQuestions = [ ref={jobConversationRef}
// <Box sx={{ display: "flex", flexDirection: "column" }}> {...{
// <ChatQuery query={{ prompt: "What are the key skills necessary for this position?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} /> type: "job_description",
// <ChatQuery query={{ prompt: "How much should this position pay (accounting for inflation)?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} /> actionLabel: "Generate Resume",
// </Box>, preamble: jobDescriptionPreamble,
// ]; 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,
}}
/>
// const jobDescriptionPreamble: MessageList = [{ } else {
// role: 'info', return <Conversation
// content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions: 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]);
// 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. * 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>,
];
// For each '\`Skill\`' from **Job Analysis** phase: 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]);
// 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\`'. * Renders the fact check view
// 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>,
];
// See [About > Resume Generation Architecture](/about/resume-generation) for more details. return <Conversation
// `, 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>
// if (!hasJobDescription) { {/* Document display area */}
// return <Conversation <Box sx={{
// ref={jobConversationRef} display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
// {...{ overflow: "hidden"
// type: "job_description", }}>
// actionLabel: "Generate Resume", <Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
// preamble: jobDescriptionPreamble, <Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
// hidePreamble: true, <Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
// placeholder: "Paste a job description, then click Generate...", </Box>
// multiline: true, </Box>
// 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

@ -1,54 +0,0 @@
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

@ -0,0 +1,72 @@
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

@ -0,0 +1,161 @@
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 @@
/** /**
* Enhanced API Client with Streaming Support * API Client Example
* *
* 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, including streaming responses. * for seamless frontend-backend communication.
*/ */
// Import generated types (from running generate_types.py) // Import generated types (from running generate_types.py)
@ -21,40 +21,6 @@ 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>;
@ -291,7 +257,7 @@ class ApiClient {
} }
// ============================ // ============================
// Chat Methods (Enhanced with Streaming) // Chat Methods
// ============================ // ============================
async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> { async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> {
@ -312,186 +278,16 @@ 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({ query })) body: JSON.stringify(formatApiRequest({ content }))
}); });
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));
@ -537,11 +333,16 @@ 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;
} }
@ -551,153 +352,28 @@ 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
// ============================ // ============================
@ -706,54 +382,193 @@ function ChatInterface({ sessionId }: { sessionId: string }) {
// Initialize API client // Initialize API client
const apiClient = new ApiClient(); const apiClient = new ApiClient();
// Standard message sending (non-streaming) // Login and set auth token
try { try {
const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?'); const authResponse = await apiClient.login('user@example.com', 'password');
console.log('Response:', message.content); apiClient.setAuthToken(authResponse.accessToken);
console.log('Logged in as:', authResponse.user);
} catch (error) { } catch (error) {
console.error('Failed to send message:', error); console.error('Login failed:', error);
} }
// Streaming message with callbacks // Create a new candidate
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', { try {
onPartialMessage: (content, messageId) => { const newCandidate = await apiClient.createCandidate({
console.log('Partial content:', content); email: 'candidate@example.com',
// Update UI with partial content status: 'active',
firstName: 'John',
lastName: 'Doe',
skills: [],
experience: [],
education: [],
preferredJobTypes: ['full-time'],
location: {
city: 'San Francisco',
country: 'USA'
}, },
onStatusChange: (status) => { languages: [],
console.log('Status changed:', status); certifications: []
// Update UI status indicator });
}, console.log('Created candidate:', newCandidate);
onComplete: (finalMessage) => { } catch (error) {
console.log('Final message:', finalMessage.content); console.error('Failed to create candidate:', error);
// Handle completed message }
},
onError: (error) => { // Search for jobs
console.error('Streaming error:', error); try {
// Handle error const jobResults = await apiClient.searchJobs('software engineer', {
location: 'San Francisco',
experienceLevel: 'senior'
});
console.log(`Found ${jobResults.total} jobs:`);
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']
} }
}); });
// Can cancel the stream if needed console.log(`Page ${candidates.page} of ${candidates.totalPages}`);
setTimeout(() => { console.log(`${candidates.data.length} candidates on this page`);
streamResponse.cancel();
}, 10000); // Cancel after 10 seconds
// Wait for completion
try {
const finalMessage = await streamResponse.promise;
console.log('Stream completed:', finalMessage);
} catch (error) { } catch (error) {
console.error('Stream failed:', error); console.error('Failed to get candidates:', error);
} }
// Auto-detection: streaming if callbacks provided, standard otherwise // Start a chat session
await apiClient.sendMessageAuto(sessionId, 'Quick question', { try {
onPartialMessage: (content) => console.log('Streaming:', content) const chatSession = await apiClient.createChatSession({
}); // Will use streaming 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()
}
});
await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard // 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 } // ============================
export type { StreamingOptions, StreamingResponse, ChatMessageChunk }; // React Hook Examples
// ============================
/*
// 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/backend/models.py // Source: src/models.py
// Generated on: 2025-05-28T21:47:08.590102 // Generated on: 2025-05-28T20:34:39.642452
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
@ -17,8 +17,6 @@ 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";
@ -129,7 +127,7 @@ export interface Attachment {
export interface AuthResponse { export interface AuthResponse {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
user: any; user: BaseUser;
expiresAt: number; expiresAt: number;
} }
@ -150,6 +148,7 @@ 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;
@ -161,6 +160,7 @@ 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,6 +173,7 @@ 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;
@ -181,7 +182,6 @@ 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,7 +250,6 @@ 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;
@ -321,6 +320,7 @@ 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,8 +431,6 @@ 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,13 +403,6 @@ 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,13 +67,6 @@ 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"
@ -551,7 +544,6 @@ 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