Hooking back up

This commit is contained in:
James Ketr 2025-05-28 16:12:32 -07:00
parent f7e41c710c
commit 68a4ccb6d3
30 changed files with 1175 additions and 1253 deletions

View File

@ -7,8 +7,8 @@ import { backstoryTheme } from './BackstoryTheme';
import { SeverityType } from 'components/Snack';
import { Query } from 'types/types';
import { ConversationHandle } from 'components/Conversation';
import { UserProvider } from 'components/UserContext';
import { UserRoute } from 'routes/UserRoute';
import { UserProvider } from 'hooks/useUser';
import { CandidateRoute } from 'routes/CandidateRoute';
import { BackstoryLayout } from 'components/layout/BackstoryLayout';
import './BackstoryApp.css';
@ -17,27 +17,17 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { connectionBase } from './utils/Global';
// 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`;
};
import { debugConversion } from 'types/conversion';
import { User, Guest, Candidate } from 'types/types';
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 location = useLocation();
const snackRef = useRef<any>(null);
const chatRef = useRef<ConversationHandle>(null);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
@ -48,72 +38,50 @@ const BackstoryApp = () => {
};
const [page, setPage] = useState<string>("");
// Extract session ID from URL query parameter or cookie
const urlParams = new URLSearchParams(window.location.search);
const urlSessionId = urlParams.get('id');
const cookieSessionId = getCookie('session_id');
// Fetch or join session on mount
useEffect(() => {
const fetchSession = async () => {
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);
}
const createGuestSession = () => {
console.log("TODO: Convert this to query the server for the session instead of generating it.");
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const guest: Guest = {
sessionId,
createdAt: new Date(),
lastActivity: new Date(),
ipAddress: 'unknown',
userAgent: navigator.userAgent
};
fetchSession();
}, [cookieSessionId, setSnack, urlSessionId, location.pathname, navigate]);
setGuest(guest);
debugConversion(guest, 'Guest Session');
};
const checkExistingAuth = () => {
const token = localStorage.getItem('accessToken');
const userData = localStorage.getItem('userData');
if (token && userData) {
try {
const user = JSON.parse(userData);
// Convert dates back to Date objects if they're stored as strings
if (user.createdAt && typeof user.createdAt === 'string') {
user.createdAt = new Date(user.createdAt);
}
if (user.updatedAt && typeof user.updatedAt === 'string') {
user.updatedAt = new Date(user.updatedAt);
}
if (user.lastLogin && typeof user.lastLogin === 'string') {
user.lastLogin = new Date(user.lastLogin);
}
setUser(user);
} catch (e) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('userData');
}
}
};
// Create guest session on component mount
useEffect(() => {
createGuestSession();
checkExistingAuth();
}, []);
useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
@ -123,15 +91,14 @@ const BackstoryApp = () => {
// Render appropriate routes based on user type
return (
<ThemeProvider theme={backstoryTheme}>
<UserProvider sessionId={sessionId} setSnack={setSnack}>
<UserProvider {...{ guest, user, candidate, setSnack }}>
<Routes>
<Route path="/u/:username" element={<UserRoute sessionId={sessionId} setSnack={setSnack} />} />
<Route path="/u/:username" element={<CandidateRoute {...{ guest, candidate, setCandidate, setSnack }} />} />
{/* Static/shared routes */}
<Route
path="/*"
element={
<BackstoryLayout
sessionId={sessionId}
setSnack={setSnack}
page={page}
chatRef={chatRef}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import { Candidate } from "../types/types";
const CandidateListingPage = (props: BackstoryPageProps) => {
const navigate = useNavigate();
const { sessionId, setSnack } = props;
const { setSnack } = props;
const [candidates, setCandidates] = useState<Candidate[] | undefined>(undefined);
useEffect(() => {
@ -20,7 +20,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
const fetchCandidates = async () => {
try {
let response;
response = await fetch(`${connectionBase}/api/u/${sessionId}`, {
response = await fetch(`${connectionBase}/api/u`, {
credentials: 'include',
});
if (!response.ok) {
@ -45,7 +45,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
};
fetchCandidates();
}, [candidates, sessionId, setSnack]);
}, [candidates, setSnack]);
return (
<Box sx={{display: "flex", flexDirection: "column"}}>
@ -66,7 +66,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
}}
sx={{ cursor: "pointer" }}
>
<CandidateInfo sessionId={sessionId} sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} user={u} />
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} candidate={u} />
</Box>
)}
</Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@ import './ResumeBuilderPage.css';
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const {
sx,
sessionId,
setSnack,
submitQuery,
} = props
@ -196,191 +195,194 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
setHasFacts(false);
}, [setHasFacts]);
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>,
];
return (<Box>Not re-implmented yet</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.
// 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:
// 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.
// 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
}];
// See [About > Resume Generation Architecture](/about/resume-generation) for more details.
// `,
// disableCopy: true
// }];
if (!hasJobDescription) {
return <Conversation
ref={jobConversationRef}
{...{
type: "job_description",
actionLabel: "Generate Resume",
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,
}}
/>
// if (!hasJobDescription) {
// return <Conversation
// ref={jobConversationRef}
// {...{
// type: "job_description",
// actionLabel: "Generate Resume",
// 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,
// }}
// />
} 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]);
// } 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>,
];
// /**
// * 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]);
// 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>,
];
// /**
// * 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 <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>
// 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>
);
// {/* 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 {

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
/**
* API Client Example
* Enhanced API Client with Streaming Support
*
* This demonstrates how to use the generated types with the conversion utilities
* for seamless frontend-backend communication.
* for seamless frontend-backend communication, including streaming responses.
*/
// Import generated types (from running generate_types.py)
@ -21,6 +21,40 @@ import {
PaginatedRequest
} 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 {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
@ -257,7 +291,7 @@ class ApiClient {
}
// ============================
// Chat Methods
// Chat Methods (Enhanced with Streaming)
// ============================
async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> {
@ -278,16 +312,186 @@ class ApiClient {
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`, {
method: 'POST',
headers: this.defaultHeaders,
body: JSON.stringify(formatApiRequest({ content }))
body: JSON.stringify(formatApiRequest({ query }))
});
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>> {
const paginatedRequest = createPaginatedRequest(request);
const params = toUrlParams(formatApiRequest(paginatedRequest));
@ -333,16 +537,11 @@ class ApiClient {
// Error Handling Helper
// ============================
// ============================
// Error Handling Helper
// ============================
async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> {
try {
const response = await requestFn();
return await handleApiResponse<T>(response);
} catch (error) {
// Log error for debugging
console.error('API request failed:', error);
throw error;
}
@ -352,28 +551,153 @@ class ApiClient {
// Utility Methods
// ============================
/**
* Update authorization token for future requests
*/
setAuthToken(token: string): void {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
}
/**
* Remove authorization token
*/
clearAuthToken(): void {
delete this.defaultHeaders['Authorization'];
}
/**
* Get current base URL
*/
getBaseUrl(): string {
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
// ============================
@ -382,193 +706,54 @@ class ApiClient {
// Initialize API client
const apiClient = new ApiClient();
// Login and set auth token
// Standard message sending (non-streaming)
try {
const authResponse = await apiClient.login('user@example.com', 'password');
apiClient.setAuthToken(authResponse.accessToken);
console.log('Logged in as:', authResponse.user);
const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?');
console.log('Response:', message.content);
} catch (error) {
console.error('Login failed:', error);
console.error('Failed to send message:', error);
}
// Create a new candidate
// Streaming message with callbacks
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', {
onPartialMessage: (content, messageId) => {
console.log('Partial content:', content);
// Update UI with partial content
},
onStatusChange: (status) => {
console.log('Status changed:', status);
// Update UI status indicator
},
onComplete: (finalMessage) => {
console.log('Final message:', finalMessage.content);
// Handle completed message
},
onError: (error) => {
console.error('Streaming error:', error);
// Handle error
}
});
// Can cancel the stream if needed
setTimeout(() => {
streamResponse.cancel();
}, 10000); // Cancel after 10 seconds
// Wait for completion
try {
const newCandidate = await apiClient.createCandidate({
email: 'candidate@example.com',
status: 'active',
firstName: 'John',
lastName: 'Doe',
skills: [],
experience: [],
education: [],
preferredJobTypes: ['full-time'],
location: {
city: 'San Francisco',
country: 'USA'
},
languages: [],
certifications: []
});
console.log('Created candidate:', newCandidate);
const finalMessage = await streamResponse.promise;
console.log('Stream completed:', finalMessage);
} catch (error) {
console.error('Failed to create candidate:', error);
console.error('Stream failed:', error);
}
// Search for jobs
try {
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);
}
// Auto-detection: streaming if callbacks provided, standard otherwise
await apiClient.sendMessageAuto(sessionId, 'Quick question', {
onPartialMessage: (content) => console.log('Streaming:', content)
}); // Will use streaming
// Get paginated candidates
try {
const candidates = await apiClient.getCandidates({
page: 1,
limit: 10,
sortBy: 'createdAt',
sortOrder: 'desc',
filters: {
status: 'active',
skills: ['javascript', 'react']
}
});
console.log(`Page ${candidates.page} of ${candidates.totalPages}`);
console.log(`${candidates.data.length} candidates on this page`);
} catch (error) {
console.error('Failed to get candidates:', error);
}
// Start a chat session
try {
const chatSession = await apiClient.createChatSession({
type: 'job_search',
aiParameters: {
name: 'Job Search Assistant',
model: 'gpt-4',
temperature: 0.7,
maxTokens: 2000,
topP: 0.95,
frequencyPenalty: 0.0,
presencePenalty: 0.0,
isDefault: false,
createdAt: new Date(),
updatedAt: new Date()
}
});
// Send a message
const message = await apiClient.sendMessage(
chatSession.id,
'Help me find software engineering jobs in San Francisco'
);
console.log('AI Response:', message.content);
} catch (error) {
console.error('Chat session failed:', error);
}
await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard
*/
// ============================
// 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 };
export { ApiClient }
export type { StreamingOptions, StreamingResponse, ChatMessageChunk };

View File

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

View File

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