Hooking back up
This commit is contained in:
parent
f7e41c710c
commit
68a4ccb6d3
@ -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}
|
||||
|
@ -95,8 +95,8 @@ const backstoryTheme = createTheme({
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: '2rem',
|
||||
borderRadius: '8px',
|
||||
// padding: '0.5rem',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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 }}>
|
||||
|
@ -5,7 +5,6 @@ import { ChatSubmitQueryInterface } from './ChatQuery';
|
||||
import { SetSnackType } from './Snack';
|
||||
|
||||
interface BackstoryElementProps {
|
||||
sessionId: string,
|
||||
setSnack: SetSnackType,
|
||||
submitQuery: ChatSubmitQueryInterface,
|
||||
sx?: SxProps<Theme>,
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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>("");
|
||||
|
@ -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" }}}/>
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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 || ""}`}
|
||||
|
@ -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
|
||||
};
|
@ -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',
|
||||
|
@ -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>}
|
||||
</>
|
||||
|
@ -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 />} />);
|
||||
|
@ -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"
|
||||
|
42
frontend/src/hooks/useUser.tsx
Normal file
42
frontend/src/hooks/useUser.tsx
Normal 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
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
54
frontend/src/routes/CandidateRoute.tsx
Normal file
54
frontend/src/routes/CandidateRoute.tsx
Normal 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 };
|
@ -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 };
|
@ -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 };
|
@ -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 };
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user