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 { SeverityType } from 'components/Snack';
|
||||||
import { Query } from 'types/types';
|
import { Query } from 'types/types';
|
||||||
import { ConversationHandle } from 'components/Conversation';
|
import { ConversationHandle } from 'components/Conversation';
|
||||||
import { UserProvider } from 'components/UserContext';
|
import { UserProvider } from 'hooks/useUser';
|
||||||
import { UserRoute } from 'routes/UserRoute';
|
import { CandidateRoute } from 'routes/CandidateRoute';
|
||||||
import { BackstoryLayout } from 'components/layout/BackstoryLayout';
|
import { BackstoryLayout } from 'components/layout/BackstoryLayout';
|
||||||
|
|
||||||
import './BackstoryApp.css';
|
import './BackstoryApp.css';
|
||||||
@ -17,27 +17,17 @@ import '@fontsource/roboto/400.css';
|
|||||||
import '@fontsource/roboto/500.css';
|
import '@fontsource/roboto/500.css';
|
||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
import { connectionBase } from './utils/Global';
|
import { debugConversion } from 'types/conversion';
|
||||||
|
import { User, Guest, Candidate } from 'types/types';
|
||||||
// Cookie handling functions
|
|
||||||
const getCookie = (name: string) => {
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) return parts.pop()?.split(';').shift();
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCookie = (name: string, value: string, days = 7) => {
|
|
||||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
||||||
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BackstoryApp = () => {
|
const BackstoryApp = () => {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [guest, setGuest] = useState<Guest | null>(null);
|
||||||
|
const [candidate, setCandidate] = useState<Candidate | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const snackRef = useRef<any>(null);
|
const snackRef = useRef<any>(null);
|
||||||
const chatRef = useRef<ConversationHandle>(null);
|
const chatRef = useRef<ConversationHandle>(null);
|
||||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
|
||||||
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
||||||
snackRef.current?.setSnack(message, severity);
|
snackRef.current?.setSnack(message, severity);
|
||||||
}, [snackRef]);
|
}, [snackRef]);
|
||||||
@ -48,72 +38,50 @@ const BackstoryApp = () => {
|
|||||||
};
|
};
|
||||||
const [page, setPage] = useState<string>("");
|
const [page, setPage] = useState<string>("");
|
||||||
|
|
||||||
// Extract session ID from URL query parameter or cookie
|
const createGuestSession = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
console.log("TODO: Convert this to query the server for the session instead of generating it.");
|
||||||
const urlSessionId = urlParams.get('id');
|
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
const cookieSessionId = getCookie('session_id');
|
const guest: Guest = {
|
||||||
|
sessionId,
|
||||||
// Fetch or join session on mount
|
createdAt: new Date(),
|
||||||
useEffect(() => {
|
lastActivity: new Date(),
|
||||||
const fetchSession = async () => {
|
ipAddress: 'unknown',
|
||||||
try {
|
userAgent: navigator.userAgent
|
||||||
let response;
|
|
||||||
let newSessionId;
|
|
||||||
let action = ""
|
|
||||||
if (urlSessionId) {
|
|
||||||
// Attempt to join session from URL
|
|
||||||
response = await fetch(`${connectionBase}/api/join-session/${urlSessionId}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Session not found');
|
|
||||||
}
|
|
||||||
newSessionId = (await response.json()).id;
|
|
||||||
action = "Joined";
|
|
||||||
} else if (cookieSessionId) {
|
|
||||||
// Attempt to join session from cookie
|
|
||||||
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
// Cookie session invalid, create new session
|
|
||||||
response = await fetch(`${connectionBase}/api/create-session`, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to create session');
|
|
||||||
}
|
|
||||||
action = "Created new";
|
|
||||||
} else {
|
|
||||||
action = "Joined";
|
|
||||||
}
|
|
||||||
newSessionId = (await response.json()).id;
|
|
||||||
} else {
|
|
||||||
// Create a new session
|
|
||||||
response = await fetch(`${connectionBase}/api/create-session`, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to create session');
|
|
||||||
}
|
|
||||||
action = "Created new";
|
|
||||||
newSessionId = (await response.json()).id;
|
|
||||||
}
|
|
||||||
setSessionId(newSessionId);
|
|
||||||
setSnack(`${action} session ${newSessionId}`);
|
|
||||||
|
|
||||||
// Store in cookie if user opts in
|
|
||||||
setCookie('session_id', newSessionId);
|
|
||||||
// Clear all query parameters, preserve the current path
|
|
||||||
navigate(location.pathname, { replace: true });
|
|
||||||
} catch (err) {
|
|
||||||
setSnack("" + err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
fetchSession();
|
setGuest(guest);
|
||||||
}, [cookieSessionId, setSnack, urlSessionId, location.pathname, navigate]);
|
debugConversion(guest, 'Guest Session');
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkExistingAuth = () => {
|
||||||
|
const token = localStorage.getItem('accessToken');
|
||||||
|
const userData = localStorage.getItem('userData');
|
||||||
|
if (token && userData) {
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userData);
|
||||||
|
// Convert dates back to Date objects if they're stored as strings
|
||||||
|
if (user.createdAt && typeof user.createdAt === 'string') {
|
||||||
|
user.createdAt = new Date(user.createdAt);
|
||||||
|
}
|
||||||
|
if (user.updatedAt && typeof user.updatedAt === 'string') {
|
||||||
|
user.updatedAt = new Date(user.updatedAt);
|
||||||
|
}
|
||||||
|
if (user.lastLogin && typeof user.lastLogin === 'string') {
|
||||||
|
user.lastLogin = new Date(user.lastLogin);
|
||||||
|
}
|
||||||
|
setUser(user);
|
||||||
|
} catch (e) {
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
localStorage.removeItem('userData');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create guest session on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
createGuestSession();
|
||||||
|
checkExistingAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
||||||
@ -123,15 +91,14 @@ const BackstoryApp = () => {
|
|||||||
// Render appropriate routes based on user type
|
// Render appropriate routes based on user type
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={backstoryTheme}>
|
<ThemeProvider theme={backstoryTheme}>
|
||||||
<UserProvider sessionId={sessionId} setSnack={setSnack}>
|
<UserProvider {...{ guest, user, candidate, setSnack }}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/u/:username" element={<UserRoute sessionId={sessionId} setSnack={setSnack} />} />
|
<Route path="/u/:username" element={<CandidateRoute {...{ guest, candidate, setCandidate, setSnack }} />} />
|
||||||
{/* Static/shared routes */}
|
{/* Static/shared routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/*"
|
path="/*"
|
||||||
element={
|
element={
|
||||||
<BackstoryLayout
|
<BackstoryLayout
|
||||||
sessionId={sessionId}
|
|
||||||
setSnack={setSnack}
|
setSnack={setSnack}
|
||||||
page={page}
|
page={page}
|
||||||
chatRef={chatRef}
|
chatRef={chatRef}
|
||||||
|
@ -95,8 +95,8 @@ const backstoryTheme = createTheme({
|
|||||||
MuiPaper: {
|
MuiPaper: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
padding: '2rem',
|
// padding: '0.5rem',
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -38,7 +38,7 @@ import {
|
|||||||
} from './types/conversion';
|
} from './types/conversion';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthResponse, BaseUser, Guest, Candidate
|
AuthResponse, User, Guest, Candidate
|
||||||
} from './types/types'
|
} from './types/types'
|
||||||
|
|
||||||
interface LoginRequest {
|
interface LoginRequest {
|
||||||
@ -57,13 +57,14 @@ interface RegisterRequest {
|
|||||||
|
|
||||||
const BackstoryTestApp: React.FC = () => {
|
const BackstoryTestApp: React.FC = () => {
|
||||||
const apiClient = new ApiClient();
|
const apiClient = new ApiClient();
|
||||||
const [currentUser, setCurrentUser] = useState<BaseUser | null>(null);
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
const [guestSession, setGuestSession] = useState<Guest | null>(null);
|
const [guestSession, setGuestSession] = useState<Guest | null>(null);
|
||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [phone, setPhone] = useState<E164Number | null>(null);
|
const [phone, setPhone] = useState<E164Number | null>(null);
|
||||||
|
const name = (currentUser?.userType === 'candidate' ? (currentUser as Candidate).username : currentUser?.email) || '';
|
||||||
|
|
||||||
// Login form state
|
// Login form state
|
||||||
const [loginForm, setLoginForm] = useState<LoginRequest>({
|
const [loginForm, setLoginForm] = useState<LoginRequest>({
|
||||||
@ -259,7 +260,7 @@ const BackstoryTestApp: React.FC = () => {
|
|||||||
<Toolbar>
|
<Toolbar>
|
||||||
<AccountCircle sx={{ mr: 2 }} />
|
<AccountCircle sx={{ mr: 2 }} />
|
||||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
Welcome, {currentUser.username}
|
Welcome, {name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@ -288,7 +289,7 @@ const BackstoryTestApp: React.FC = () => {
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<Typography variant="body1" sx={{ mb: 1 }}>
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||||
<strong>Username:</strong> {currentUser.username}
|
<strong>Username:</strong> {name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
@ -5,7 +5,6 @@ import { ChatSubmitQueryInterface } from './ChatQuery';
|
|||||||
import { SetSnackType } from './Snack';
|
import { SetSnackType } from './Snack';
|
||||||
|
|
||||||
interface BackstoryElementProps {
|
interface BackstoryElementProps {
|
||||||
sessionId: string,
|
|
||||||
setSnack: SetSnackType,
|
setSnack: SetSnackType,
|
||||||
submitQuery: ChatSubmitQueryInterface,
|
submitQuery: ChatSubmitQueryInterface,
|
||||||
sx?: SxProps<Theme>,
|
sx?: SxProps<Theme>,
|
||||||
|
@ -7,32 +7,28 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
import { useUser } from "./UserContext";
|
import { useUser } from "../hooks/useUser";
|
||||||
import { Candidate } from '../types/types';
|
import { Candidate } from '../types/types';
|
||||||
import { CopyBubble } from "./CopyBubble";
|
import { CopyBubble } from "./CopyBubble";
|
||||||
|
|
||||||
interface CandidateInfoProps {
|
interface CandidateInfoProps {
|
||||||
sessionId: string;
|
candidate: Candidate;
|
||||||
user?: Candidate;
|
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
action?: string;
|
action?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
|
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
|
||||||
const { user } = useUser();
|
const { candidate } = props;
|
||||||
const {
|
const {
|
||||||
sx,
|
sx,
|
||||||
action = '',
|
action = '',
|
||||||
sessionId,
|
|
||||||
} = props;
|
} = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const candidate: Candidate | null = props.user || (user as Candidate);
|
if (!candidate) {
|
||||||
|
return <Box>No user loaded.</Box>;
|
||||||
if (!candidate) {
|
}
|
||||||
return <Box>No user loaded.</Box>;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
elevation={1}
|
elevation={1}
|
||||||
@ -58,7 +54,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
|||||||
maxWidth: "80px"
|
maxWidth: "80px"
|
||||||
}}>
|
}}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={candidate.hasProfile ? `/api/u/${candidate.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''}
|
src={candidate.hasProfile ? `/api/u/${candidate.username}/profile?timestamp=${Date.now()}` : ''}
|
||||||
alt={`${candidate.fullName}'s profile`}
|
alt={`${candidate.fullName}'s profile`}
|
||||||
sx={{
|
sx={{
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
|
@ -8,18 +8,23 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
|||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||||
|
|
||||||
import { Message, MessageList, BackstoryMessage, MessageRoles } from './Message';
|
import { Message, MessageRoles } from './Message';
|
||||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||||
import { Query } from 'types/types';
|
|
||||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
import { connectionBase } from 'utils/Global';
|
import { connectionBase } from 'utils/Global';
|
||||||
import { useUser } from "components/UserContext";
|
import { useUser } from "hooks/useUser";
|
||||||
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
|
import { ApiClient, StreamingResponse } from 'types/api-client';
|
||||||
|
import { ChatMessage, ChatContext, ChatSession, AIParameters, Query } from 'types/types';
|
||||||
|
import { PaginatedResponse } from 'types/conversion';
|
||||||
|
|
||||||
import './Conversation.css';
|
import './Conversation.css';
|
||||||
|
|
||||||
const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establishing connection with server..." };
|
const defaultMessage: ChatMessage = {
|
||||||
|
status: "thinking", sender: "system", sessionId: "", timestamp: new Date(), content: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadingMessage: ChatMessage = { ...defaultMessage, content: "Establishing connection with server..." };
|
||||||
|
|
||||||
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona';
|
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona';
|
||||||
|
|
||||||
@ -37,18 +42,17 @@ interface ConversationProps extends BackstoryElementProps {
|
|||||||
resetLabel?: string, // Label to put on Reset button
|
resetLabel?: string, // Label to put on Reset button
|
||||||
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
|
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
|
||||||
defaultQuery?: string, // Default text to populate the TextField input
|
defaultQuery?: string, // Default text to populate the TextField input
|
||||||
preamble?: MessageList, // Messages to display at start of Conversation until Action has been invoked
|
preamble?: ChatMessage[], // Messages to display at start of Conversation until Action has been invoked
|
||||||
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked
|
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked
|
||||||
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked
|
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked
|
||||||
messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation
|
messageFilter?: ((messages: ChatMessage[]) => ChatMessage[]) | undefined, // Filter callback to determine which Messages to display in Conversation
|
||||||
messages?: MessageList, //
|
messages?: ChatMessage[], //
|
||||||
sx?: SxProps<Theme>,
|
sx?: SxProps<Theme>,
|
||||||
onResponse?: ((message: BackstoryMessage) => void) | undefined, // Event called when a query completes (provides messages)
|
onResponse?: ((message: ChatMessage) => void) | undefined, // Event called when a query completes (provides messages)
|
||||||
};
|
};
|
||||||
|
|
||||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
|
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
|
||||||
const {
|
const {
|
||||||
sessionId,
|
|
||||||
actionLabel,
|
actionLabel,
|
||||||
defaultPrompts,
|
defaultPrompts,
|
||||||
hideDefaultPrompts,
|
hideDefaultPrompts,
|
||||||
@ -65,20 +69,22 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
sx,
|
sx,
|
||||||
type,
|
type,
|
||||||
} = props;
|
} = props;
|
||||||
const { user } = useUser()
|
const apiClient = new ApiClient();
|
||||||
|
const { candidate } = useUser()
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [countdown, setCountdown] = useState<number>(0);
|
const [countdown, setCountdown] = useState<number>(0);
|
||||||
const [conversation, setConversation] = useState<MessageList>([]);
|
const [conversation, setConversation] = useState<ChatMessage[]>([]);
|
||||||
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
|
const conversationRef = useRef<ChatMessage[]>([]);
|
||||||
const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined);
|
const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]);
|
||||||
const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | undefined>(undefined);
|
const [processingMessage, setProcessingMessage] = useState<ChatMessage | undefined>(undefined);
|
||||||
|
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | undefined>(undefined);
|
||||||
const timerRef = useRef<any>(null);
|
const timerRef = useRef<any>(null);
|
||||||
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||||
const conversationRef = useRef<MessageList>([]);
|
|
||||||
const viewableElementRef = useRef<HTMLDivElement>(null);
|
const viewableElementRef = useRef<HTMLDivElement>(null);
|
||||||
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||||
const stopRef = useRef(false);
|
const stopRef = useRef(false);
|
||||||
const controllerRef = useRef<StreamQueryController>(null);
|
const controllerRef = useRef<StreamingResponse>(null);
|
||||||
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
|
|
||||||
// Keep the ref updated whenever items changes
|
// Keep the ref updated whenever items changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -113,72 +119,76 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
};
|
};
|
||||||
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
|
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
|
||||||
|
|
||||||
const fetchHistory = useCallback(async () => {
|
useEffect(() => {
|
||||||
let retries = 5;
|
if (chatSession) {
|
||||||
while (--retries > 0) {
|
return;
|
||||||
|
}
|
||||||
|
const createChatSession = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/history/${sessionId}/${type}`, {
|
const aiParameters: AIParameters = {
|
||||||
method: 'GET',
|
name: '',
|
||||||
headers: {
|
model: 'custom',
|
||||||
'Content-Type': 'application/json',
|
temperature: 0.7,
|
||||||
},
|
maxTokens: -1,
|
||||||
});
|
topP: 1,
|
||||||
|
frequencyPenalty: 0,
|
||||||
|
presencePenalty: 0,
|
||||||
|
isDefault: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.ok) {
|
const chatContext: ChatContext = {
|
||||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
type: "general",
|
||||||
}
|
aiParameters
|
||||||
|
};
|
||||||
const { messages } = await response.json();
|
const response: ChatSession = await apiClient.createChatSession(chatContext);
|
||||||
|
setChatSession(response);
|
||||||
if (messages === undefined || messages.length === 0) {
|
} catch (e) {
|
||||||
console.log(`History returned for ${type} from server with 0 entries`)
|
console.error(e);
|
||||||
setConversation([])
|
setSnack("Unable to create chat session.", "error");
|
||||||
setNoInteractions(true);
|
|
||||||
} else {
|
|
||||||
console.log(`History returned for ${type} from server with ${messages.length} entries:`, messages)
|
|
||||||
|
|
||||||
const backstoryMessages: BackstoryMessage[] = messages;
|
|
||||||
|
|
||||||
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => {
|
|
||||||
if (backstoryMessage.status === "partial") {
|
|
||||||
return [{
|
|
||||||
...backstoryMessage,
|
|
||||||
role: "assistant",
|
|
||||||
content: backstoryMessage.response || "",
|
|
||||||
expanded: false,
|
|
||||||
expandable: true,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
return [{
|
|
||||||
role: 'user',
|
|
||||||
content: backstoryMessage.prompt || "",
|
|
||||||
}, {
|
|
||||||
...backstoryMessage,
|
|
||||||
role: ['done'].includes(backstoryMessage.status || "") ? "assistant" : backstoryMessage.status,
|
|
||||||
content: backstoryMessage.response || "",
|
|
||||||
}] as MessageList;
|
|
||||||
}));
|
|
||||||
setNoInteractions(false);
|
|
||||||
}
|
|
||||||
setProcessingMessage(undefined);
|
|
||||||
setStreamingMessage(undefined);
|
|
||||||
return;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating session ID:', error);
|
|
||||||
setProcessingMessage({ role: "error", content: `Unable to obtain history from server. Retrying in 3 seconds (${retries} remain.)` });
|
|
||||||
setTimeout(() => {
|
|
||||||
setProcessingMessage(undefined);
|
|
||||||
}, 3000);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
||||||
setSnack("Unable to obtain chat history.", "error");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [setConversation,setSnack, type, sessionId]);
|
|
||||||
|
createChatSession();
|
||||||
|
|
||||||
|
}, [chatSession, setChatSession]);
|
||||||
|
|
||||||
|
const getChatMessages = useCallback(async () => {
|
||||||
|
if (!chatSession || !chatSession.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(chatSession.id);
|
||||||
|
const messages: ChatMessage[] = response.data;
|
||||||
|
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
setStreamingMessage(undefined);
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
console.log(`History returned with 0 entries`)
|
||||||
|
setConversation([])
|
||||||
|
setNoInteractions(true);
|
||||||
|
} else {
|
||||||
|
console.log(`History returned with ${messages.length} entries:`, messages)
|
||||||
|
setConversation(messages);
|
||||||
|
setNoInteractions(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to obtain chat history', error);
|
||||||
|
setProcessingMessage({ ...defaultMessage, status: "error", content: `Unable to obtain history from server.` });
|
||||||
|
setTimeout(() => {
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
setNoInteractions(true);
|
||||||
|
}, 3000);
|
||||||
|
setSnack("Unable to obtain chat history.", "error");
|
||||||
|
}
|
||||||
|
}, [chatSession]);
|
||||||
|
|
||||||
|
|
||||||
// Set the initial chat history to "loading" or the welcome message if loaded.
|
// Set the initial chat history to "loading" or the welcome message if loaded.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionId === undefined) {
|
if (!chatSession) {
|
||||||
setProcessingMessage(loadingMessage);
|
setProcessingMessage(loadingMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -188,33 +198,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
setConversation([]);
|
setConversation([]);
|
||||||
setNoInteractions(true);
|
setNoInteractions(true);
|
||||||
|
|
||||||
if (user) {
|
getChatMessages();
|
||||||
fetchHistory();
|
|
||||||
}
|
|
||||||
}, [fetchHistory, sessionId, setProcessing, user]);
|
|
||||||
|
|
||||||
const startCountdown = (seconds: number) => {
|
}, [chatSession]);
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
|
||||||
setCountdown(seconds);
|
|
||||||
timerRef.current = setInterval(() => {
|
|
||||||
setCountdown((prev) => {
|
|
||||||
if (prev <= 1) {
|
|
||||||
clearInterval(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return prev - 1;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopCountdown = () => {
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearInterval(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
setCountdown(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnter = (value: string) => {
|
const handleEnter = (value: string) => {
|
||||||
const query: Query = {
|
const query: Query = {
|
||||||
@ -227,76 +213,69 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
submitQuery: (query: Query) => {
|
submitQuery: (query: Query) => {
|
||||||
processQuery(query);
|
processQuery(query);
|
||||||
},
|
},
|
||||||
fetchHistory: () => { return fetchHistory(); }
|
fetchHistory: () => { getChatMessages(); }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
const reset = async () => {
|
// const reset = async () => {
|
||||||
try {
|
// try {
|
||||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
|
// const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
|
||||||
method: 'PUT',
|
// method: 'PUT',
|
||||||
headers: {
|
// headers: {
|
||||||
'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
// 'Accept': 'application/json',
|
||||||
},
|
// },
|
||||||
body: JSON.stringify({ reset: ['history'] })
|
// body: JSON.stringify({ reset: ['history'] })
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!response.ok) {
|
// if (!response.ok) {
|
||||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
// throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!response.body) {
|
// if (!response.body) {
|
||||||
throw new Error('Response body is null');
|
// throw new Error('Response body is null');
|
||||||
}
|
// }
|
||||||
|
|
||||||
setProcessingMessage(undefined);
|
// setProcessingMessage(undefined);
|
||||||
setStreamingMessage(undefined);
|
// setStreamingMessage(undefined);
|
||||||
setConversation([]);
|
// setConversation([]);
|
||||||
setNoInteractions(true);
|
// setNoInteractions(true);
|
||||||
|
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
setSnack("Error resetting history", "error")
|
// setSnack("Error resetting history", "error")
|
||||||
console.error('Error resetting history:', e);
|
// console.error('Error resetting history:', e);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const cancelQuery = () => {
|
const cancelQuery = () => {
|
||||||
console.log("Stop query");
|
console.log("Stop query");
|
||||||
if (controllerRef.current) {
|
if (controllerRef.current) {
|
||||||
controllerRef.current.abort();
|
controllerRef.current.cancel();
|
||||||
}
|
}
|
||||||
controllerRef.current = null;
|
controllerRef.current = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const processQuery = (query: Query) => {
|
const processQuery = (query: Query) => {
|
||||||
if (controllerRef.current) {
|
if (controllerRef.current || !chatSession || !chatSession.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const sessionId: string = chatSession.id;
|
||||||
|
|
||||||
setNoInteractions(false);
|
setNoInteractions(false);
|
||||||
|
|
||||||
setConversation([
|
setConversation([
|
||||||
...conversationRef.current,
|
...conversationRef.current,
|
||||||
{
|
{
|
||||||
role: 'user',
|
...defaultMessage,
|
||||||
origin: type,
|
sender: 'user',
|
||||||
content: query.prompt,
|
content: query.prompt,
|
||||||
disableCopy: true
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
|
||||||
setProcessingMessage(
|
setProcessingMessage(
|
||||||
{ role: 'status', content: 'Submitting request...', disableCopy: true }
|
{ ...defaultMessage, content: 'Submitting request...' }
|
||||||
);
|
);
|
||||||
|
|
||||||
controllerRef.current = streamQueryResponse({
|
controllerRef.current = apiClient.sendMessageStream(sessionId, query, {
|
||||||
query,
|
|
||||||
type,
|
|
||||||
sessionId,
|
|
||||||
connectionBase,
|
|
||||||
onComplete: (msg) => {
|
onComplete: (msg) => {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
switch (msg.status) {
|
switch (msg.status) {
|
||||||
@ -307,14 +286,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
...msg,
|
...msg,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
origin: type,
|
origin: type,
|
||||||
prompt: ['done', 'partial'].includes(msg.status || "") ? msg.prompt : '',
|
}] as ChatMessage[]);
|
||||||
content: msg.response || "",
|
|
||||||
expanded: msg.status === "done" ? true : false,
|
|
||||||
expandable: msg.status === "done" ? false : true,
|
|
||||||
}] as MessageList);
|
|
||||||
startCountdown(Math.ceil(msg.remaining_time || 0));
|
|
||||||
if (msg.status === "done") {
|
if (msg.status === "done") {
|
||||||
stopCountdown();
|
|
||||||
setStreamingMessage(undefined);
|
setStreamingMessage(undefined);
|
||||||
setProcessingMessage(undefined);
|
setProcessingMessage(undefined);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@ -327,28 +300,27 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
case "error":
|
case "error":
|
||||||
// Show error
|
// Show error
|
||||||
setConversation([
|
setConversation([
|
||||||
...conversationRef.current, {
|
...conversationRef.current,
|
||||||
...msg,
|
msg
|
||||||
role: 'error',
|
]);
|
||||||
origin: type,
|
|
||||||
content: msg.response || "",
|
|
||||||
}] as MessageList);
|
|
||||||
setProcessingMessage(msg);
|
setProcessingMessage(msg);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
stopCountdown();
|
|
||||||
controllerRef.current = null;
|
controllerRef.current = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setProcessingMessage({ role: (msg.status || "error") as MessageRoles, content: msg.response || "", disableCopy: true });
|
setProcessingMessage(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStreaming: (chunk) => {
|
onPartialMessage: (chunk) => {
|
||||||
setStreamingMessage({ role: "streaming", content: chunk, disableCopy: true });
|
setStreamingMessage({ ...defaultMessage, status: "streaming", content: chunk });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!chatSession) {
|
||||||
|
return (<></>);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
// <Scrollable
|
// <Scrollable
|
||||||
// className={`${className || ""} Conversation`}
|
// className={`${className || ""} Conversation`}
|
||||||
@ -365,16 +337,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
<Box sx={{ p: 1, mt: 0, ...sx }}>
|
<Box sx={{ p: 1, mt: 0, ...sx }}>
|
||||||
{
|
{
|
||||||
filteredConversation.map((message, index) =>
|
filteredConversation.map((message, index) =>
|
||||||
<Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery: processQuery, message, connectionBase, sessionId, setSnack, submitQuery }} />
|
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, setSnack, submitQuery }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
processingMessage !== undefined &&
|
processingMessage !== undefined &&
|
||||||
<Message {...{ sendQuery: processQuery, connectionBase, sessionId, setSnack, message: processingMessage, submitQuery }} />
|
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: processingMessage, submitQuery }} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
streamingMessage !== undefined &&
|
streamingMessage !== undefined &&
|
||||||
<Message {...{ sendQuery: processQuery, connectionBase, sessionId, setSnack, message: streamingMessage, submitQuery }} />
|
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: streamingMessage, submitQuery }} />
|
||||||
}
|
}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -415,14 +387,14 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||||
<DeleteConfirmation
|
<DeleteConfirmation
|
||||||
label={resetLabel || "all data"}
|
label={resetLabel || "all data"}
|
||||||
disabled={sessionId === undefined || processingMessage !== undefined || noInteractions}
|
disabled={!chatSession || processingMessage !== undefined || noInteractions}
|
||||||
onDelete={() => { reset(); resetAction && resetAction(); }} />
|
onDelete={() => { /*reset(); resetAction && resetAction(); */ }} />
|
||||||
<Tooltip title={actionLabel || "Send"}>
|
<Tooltip title={actionLabel || "Send"}>
|
||||||
<span style={{ display: "flex", flexGrow: 1 }}>
|
<span style={{ display: "flex", flexGrow: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={sessionId === undefined || processingMessage !== undefined}
|
disabled={!chatSession || processingMessage !== undefined}
|
||||||
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
|
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
|
||||||
{actionLabel}<SendIcon />
|
{actionLabel}<SendIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@ -436,7 +408,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||||
size="large"
|
size="large"
|
||||||
edge="start"
|
edge="start"
|
||||||
disabled={stopRef.current || sessionId === undefined || processing === false}
|
disabled={stopRef.current || !chatSession || processing === false}
|
||||||
>
|
>
|
||||||
<CancelIcon />
|
<CancelIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -7,11 +7,10 @@ interface DocumentProps extends BackstoryElementProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Document = (props: DocumentProps) => {
|
const Document = (props: DocumentProps) => {
|
||||||
const { sessionId, setSnack, submitQuery, filepath } = props;
|
const { setSnack, submitQuery, filepath } = props;
|
||||||
const backstoryProps = {
|
const backstoryProps = {
|
||||||
submitQuery,
|
submitQuery,
|
||||||
setSnack,
|
setSnack,
|
||||||
sessionId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [document, setDocument] = useState<string>("");
|
const [document, setDocument] = useState<string>("");
|
||||||
|
@ -2,24 +2,25 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||||
import { Quote } from 'components/Quote';
|
import { Quote } from 'components/Quote';
|
||||||
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
|
|
||||||
import { connectionBase } from 'utils/Global';
|
|
||||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||||
import { useUser } from 'components/UserContext';
|
import { useUser } from 'hooks/useUser';
|
||||||
|
import { Candidate, ChatSession } from 'types/types';
|
||||||
|
|
||||||
interface GenerateImageProps extends BackstoryElementProps {
|
interface GenerateImageProps extends BackstoryElementProps {
|
||||||
prompt: string
|
prompt: string;
|
||||||
|
chatSession: ChatSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GenerateImage = (props: GenerateImageProps) => {
|
const GenerateImage = (props: GenerateImageProps) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const {sessionId, setSnack, prompt} = props;
|
const { setSnack, chatSession, prompt } = props;
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [image, setImage] = useState<string>('');
|
const [image, setImage] = useState<string>('');
|
||||||
|
|
||||||
|
const name = (user?.userType === 'candidate' ? (user as Candidate).username : user?.email) || '';
|
||||||
// Only keep refs that are truly necessary
|
// Only keep refs that are truly necessary
|
||||||
const controllerRef = useRef<StreamQueryController>(null);
|
const controllerRef = useRef<string>(null);
|
||||||
|
|
||||||
// Effect to trigger profile generation when user data is ready
|
// Effect to trigger profile generation when user data is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -34,56 +35,54 @@ const GenerateImage = (props: GenerateImageProps) => {
|
|||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
controllerRef.current = streamQueryResponse({
|
// controllerRef.current = streamQueryResponse({
|
||||||
query: {
|
// query: {
|
||||||
prompt: prompt,
|
// prompt: prompt,
|
||||||
agentOptions: {
|
// agentOptions: {
|
||||||
username: user?.username,
|
// username: name,
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
type: "image",
|
// type: "image",
|
||||||
sessionId,
|
// onComplete: (msg) => {
|
||||||
connectionBase,
|
// switch (msg.status) {
|
||||||
onComplete: (msg) => {
|
// case "partial":
|
||||||
switch (msg.status) {
|
// case "done":
|
||||||
case "partial":
|
// if (msg.status === "done") {
|
||||||
case "done":
|
// if (!msg.response) {
|
||||||
if (msg.status === "done") {
|
// setSnack("Image generation failed", "error");
|
||||||
if (!msg.response) {
|
// } else {
|
||||||
setSnack("Image generation failed", "error");
|
// setImage(msg.response);
|
||||||
} else {
|
// }
|
||||||
setImage(msg.response);
|
// setProcessing(false);
|
||||||
}
|
// controllerRef.current = null;
|
||||||
setProcessing(false);
|
// }
|
||||||
controllerRef.current = null;
|
// break;
|
||||||
}
|
// case "error":
|
||||||
break;
|
// console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
|
||||||
case "error":
|
// setSnack(msg.response || "", "error");
|
||||||
console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
|
// setProcessing(false);
|
||||||
setSnack(msg.response || "", "error");
|
// controllerRef.current = null;
|
||||||
setProcessing(false);
|
// break;
|
||||||
controllerRef.current = null;
|
// default:
|
||||||
break;
|
// let data: any = {};
|
||||||
default:
|
// try {
|
||||||
let data: any = {};
|
// data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
|
||||||
try {
|
// } catch (e) {
|
||||||
data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
|
// data = { message: msg.response };
|
||||||
} catch (e) {
|
// }
|
||||||
data = { message: msg.response };
|
// if (msg.status !== "heartbeat") {
|
||||||
}
|
// console.log(data);
|
||||||
if (msg.status !== "heartbeat") {
|
// }
|
||||||
console.log(data);
|
// if (data.message) {
|
||||||
}
|
// setStatus(data.message);
|
||||||
if (data.message) {
|
// }
|
||||||
setStatus(data.message);
|
// break;
|
||||||
}
|
// }
|
||||||
break;
|
// }
|
||||||
}
|
// });
|
||||||
}
|
}, [user, prompt, setSnack]);
|
||||||
});
|
|
||||||
}, [user, prompt, sessionId, setSnack]);
|
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!chatSession) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +95,7 @@ const GenerateImage = (props: GenerateImageProps) => {
|
|||||||
maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
|
maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
|
||||||
minHeight: "max-content",
|
minHeight: "max-content",
|
||||||
}}>
|
}}>
|
||||||
{image !== '' && <img alt={prompt} src={`${image}/${sessionId}`} />}
|
{image !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />}
|
||||||
{ prompt &&
|
{ prompt &&
|
||||||
<Quote size={processing ? "normal" : "small"} quote={prompt} sx={{ "& *": { color: "#2E2E2E !important" }}}/>
|
<Quote size={processing ? "normal" : "small"} quote={prompt} sx={{ "& *": { color: "#2E2E2E !important" }}}/>
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import { SetSnackType } from './Snack';
|
|||||||
import { CopyBubble } from './CopyBubble';
|
import { CopyBubble } from './CopyBubble';
|
||||||
import { Scrollable } from './Scrollable';
|
import { Scrollable } from './Scrollable';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
|
import { ChatMessage, ChatSession } from 'types/types';
|
||||||
|
|
||||||
type MessageRoles =
|
type MessageRoles =
|
||||||
'assistant' |
|
'assistant' |
|
||||||
@ -304,14 +305,15 @@ type MessageList = BackstoryMessage[];
|
|||||||
|
|
||||||
interface MessageProps extends BackstoryElementProps {
|
interface MessageProps extends BackstoryElementProps {
|
||||||
sx?: SxProps<Theme>,
|
sx?: SxProps<Theme>,
|
||||||
message: BackstoryMessage,
|
message: ChatMessage,
|
||||||
expanded?: boolean,
|
expanded?: boolean,
|
||||||
onExpand?: (open: boolean) => void,
|
onExpand?: (open: boolean) => void,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
chatSession?: ChatSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MessageMetaProps {
|
interface MessageMetaProps {
|
||||||
metadata: MessageMetaData,
|
metadata: Record<string, any>,
|
||||||
messageProps: MessageProps
|
messageProps: MessageProps
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -446,12 +448,11 @@ const MessageMeta = (props: MessageMetaProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Message = (props: MessageProps) => {
|
const Message = (props: MessageProps) => {
|
||||||
const { message, submitQuery, sx, className, onExpand, setSnack, sessionId, expanded } = props;
|
const { message, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded } = props;
|
||||||
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
||||||
const textFieldRef = useRef(null);
|
const textFieldRef = useRef(null);
|
||||||
const backstoryProps = {
|
const backstoryProps = {
|
||||||
submitQuery,
|
submitQuery,
|
||||||
sessionId,
|
|
||||||
setSnack
|
setSnack
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -475,7 +476,8 @@ const Message = (props: MessageProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatBubble
|
<ChatBubble
|
||||||
className={`${className || ""} Message Message-${message.role}`}
|
role='assistant'
|
||||||
|
className={`${className || ""} Message Message-${message.sender}`}
|
||||||
{...message}
|
{...message}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
@ -503,11 +505,11 @@ const Message = (props: MessageProps) => {
|
|||||||
overflow: "auto", /* Handles scrolling for the div */
|
overflow: "auto", /* Handles scrolling for the div */
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledMarkdown streaming={message.role === "streaming"} content={formattedContent} {...backstoryProps} />
|
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={formattedContent} {...backstoryProps} />
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}>
|
<CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}>
|
||||||
{(message.disableCopy === undefined || message.disableCopy === false) && <CopyBubble content={message.content} />}
|
{/*(message.disableCopy === undefined || message.disableCopy === false) &&*/ <CopyBubble content={message.content} />}
|
||||||
{message.metadata && (
|
{message.metadata && (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
|
<Button variant="text" onClick={handleMetaExpandClick} sx={{ color: "darkgrey", p: 0 }}>
|
||||||
@ -516,7 +518,7 @@ const Message = (props: MessageProps) => {
|
|||||||
<ExpandMore
|
<ExpandMore
|
||||||
expand={metaExpanded}
|
expand={metaExpanded}
|
||||||
onClick={handleMetaExpandClick}
|
onClick={handleMetaExpandClick}
|
||||||
aria-expanded={message.expanded}
|
aria-expanded={true /*message.expanded*/}
|
||||||
aria-label="show more"
|
aria-label="show more"
|
||||||
>
|
>
|
||||||
<ExpandMoreIcon />
|
<ExpandMoreIcon />
|
||||||
|
@ -13,15 +13,17 @@ import { GenerateImage } from './GenerateImage';
|
|||||||
|
|
||||||
import './StyledMarkdown.css';
|
import './StyledMarkdown.css';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
|
import { ChatSession } from 'types/types';
|
||||||
|
|
||||||
interface StyledMarkdownProps extends BackstoryElementProps {
|
interface StyledMarkdownProps extends BackstoryElementProps {
|
||||||
className?: string,
|
className?: string,
|
||||||
content: string,
|
content: string,
|
||||||
streaming?: boolean,
|
streaming?: boolean,
|
||||||
|
chatSession?: ChatSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
|
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
|
||||||
const { className, sessionId, content, submitQuery, sx, streaming, setSnack } = props;
|
const { className, content, chatSession, submitQuery, sx, streaming, setSnack } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const overrides: any = {
|
const overrides: any = {
|
||||||
@ -77,7 +79,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
|||||||
props: {
|
props: {
|
||||||
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
|
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
const href = event.currentTarget.getAttribute('href');
|
const href = event.currentTarget.getAttribute('href');
|
||||||
console.log("StyledMarkdown onClick:", href, sessionId);
|
console.log("StyledMarkdown onClick:", href);
|
||||||
if (href) {
|
if (href) {
|
||||||
if (href.match(/^\//)) {
|
if (href.match(/^\//)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -108,19 +110,22 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
|||||||
return props.query;
|
return props.query;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
GenerateImage: {
|
};
|
||||||
|
|
||||||
|
if (chatSession) {
|
||||||
|
overrides.GenerateImage = {
|
||||||
component: (props: { prompt: string }) => {
|
component: (props: { prompt: string }) => {
|
||||||
const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
|
const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
|
||||||
try {
|
try {
|
||||||
return <GenerateImage prompt={prompt} {...{sessionId, submitQuery, setSnack}}/>
|
return <GenerateImage {...{ chatSession, prompt, submitQuery, setSnack }} />
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("StyledMarkdown error:", prompt, e);
|
console.log("StyledMarkdown error:", prompt, e);
|
||||||
return props.prompt;
|
return props.prompt;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return <Box
|
return <Box
|
||||||
className={`MuiMarkdown ${className || ""}`}
|
className={`MuiMarkdown ${className || ""}`}
|
||||||
|
@ -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 VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
|
||||||
const { sessionId, setSnack, rag, inline, sx } = props;
|
const { setSnack, rag, inline, sx } = props;
|
||||||
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
||||||
const [newQuery, setNewQuery] = useState<string>('');
|
const [newQuery, setNewQuery] = useState<string>('');
|
||||||
const [querySet, setQuerySet] = useState<QuerySet>(rag || emptyQuerySet);
|
const [querySet, setQuerySet] = useState<QuerySet>(rag || emptyQuerySet);
|
||||||
@ -225,12 +225,12 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
|||||||
|
|
||||||
// Get the collection to visualize
|
// Get the collection to visualize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2)) || sessionId === undefined) {
|
if ((result !== undefined && result.dimensions !== (view2D ? 3 : 2))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fetchCollection = async () => {
|
const fetchCollection = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/umap/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/umap/`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -247,7 +247,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchCollection();
|
fetchCollection();
|
||||||
}, [result, setSnack, sessionId, view2D])
|
}, [result, setSnack, view2D])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!result || !result.embeddings) return;
|
if (!result || !result.embeddings) return;
|
||||||
@ -389,7 +389,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
|||||||
if (!query.trim()) return;
|
if (!query.trim()) return;
|
||||||
setNewQuery('');
|
setNewQuery('');
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${connectionBase}/api/similarity/${sessionId}`, {
|
const response = await fetch(`${connectionBase}/api/similarity/`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -407,7 +407,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!plotData || sessionId === undefined) return (
|
if (!plotData) return (
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<div>Loading visualization...</div>
|
<div>Loading visualization...</div>
|
||||||
</Box>
|
</Box>
|
||||||
@ -415,7 +415,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
|||||||
|
|
||||||
const fetchRAGMeta = async (node: Node) => {
|
const fetchRAGMeta = async (node: Node) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/umap/entry/${node.id}/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/umap/entry/${node.id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -15,7 +15,7 @@ import { Header } from 'components/layout/Header';
|
|||||||
import { Scrollable } from 'components/Scrollable';
|
import { Scrollable } from 'components/Scrollable';
|
||||||
import { Footer } from 'components/layout/Footer';
|
import { Footer } from 'components/layout/Footer';
|
||||||
import { Snack, SetSnackType } from 'components/Snack';
|
import { Snack, SetSnackType } from 'components/Snack';
|
||||||
import { useUser } from 'components/UserContext';
|
import { useUser } from 'hooks/useUser';
|
||||||
import { User } from 'types/types';
|
import { User } from 'types/types';
|
||||||
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
|
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
|
||||||
import { LoadingComponent } from "components/LoadingComponent";
|
import { LoadingComponent } from "components/LoadingComponent";
|
||||||
@ -122,16 +122,15 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BackstoryLayout: React.FC<{
|
const BackstoryLayout: React.FC<{
|
||||||
sessionId: string | undefined;
|
|
||||||
setSnack: SetSnackType;
|
setSnack: SetSnackType;
|
||||||
page: string;
|
page: string;
|
||||||
chatRef: React.Ref<any>;
|
chatRef: React.Ref<any>;
|
||||||
snackRef: React.Ref<any>;
|
snackRef: React.Ref<any>;
|
||||||
submitQuery: any;
|
submitQuery: any;
|
||||||
}> = ({ sessionId, setSnack, page, chatRef, snackRef, submitQuery }) => {
|
}> = ({ setSnack, page, chatRef, snackRef, submitQuery }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user } = useUser();
|
const { user, guest, candidate } = useUser();
|
||||||
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -139,18 +138,18 @@ const BackstoryLayout: React.FC<{
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
let dynamicRoutes;
|
let dynamicRoutes;
|
||||||
if (sessionId) {
|
if (guest) {
|
||||||
dynamicRoutes = getBackstoryDynamicRoutes({
|
dynamicRoutes = getBackstoryDynamicRoutes({
|
||||||
sessionId,
|
user,
|
||||||
setSnack,
|
setSnack,
|
||||||
submitQuery,
|
submitQuery,
|
||||||
chatRef
|
chatRef
|
||||||
}, user);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}>
|
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}>
|
||||||
<Header {...{ setSnack, sessionId, user, currentPath: page, navigate, navigationLinks }} />
|
<Header {...{ setSnack, guest, user, candidate, currentPath: page, navigate, navigationLinks }} />
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -178,7 +177,7 @@ const BackstoryLayout: React.FC<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackstoryPageContainer>
|
<BackstoryPageContainer>
|
||||||
{!sessionId &&
|
{!guest &&
|
||||||
<Box>
|
<Box>
|
||||||
<LoadingComponent
|
<LoadingComponent
|
||||||
loadingText="Creating session..."
|
loadingText="Creating session..."
|
||||||
@ -187,7 +186,7 @@ const BackstoryLayout: React.FC<{
|
|||||||
fadeDuration={1200} />
|
fadeDuration={1200} />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
{sessionId && <>
|
{guest && <>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
|
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
|
||||||
</>
|
</>
|
||||||
|
@ -33,25 +33,26 @@ const LoginPage = () => (<BetaPage><Typography variant="h4">Login page...</Typog
|
|||||||
// const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
|
// const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
|
||||||
|
|
||||||
interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
|
interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
|
||||||
chatRef: Ref<ConversationHandle>
|
chatRef: Ref<ConversationHandle>;
|
||||||
|
user?: User | null;
|
||||||
}
|
}
|
||||||
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: User | null): ReactNode => {
|
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
|
||||||
const { sessionId, setSnack, submitQuery, chatRef } = props;
|
const { user, setSnack, submitQuery, chatRef } = props;
|
||||||
let index=0
|
let index=0
|
||||||
const routes = [
|
const routes = [
|
||||||
<Route key={`${index++}`} path="/" element={<HomePage/>} />,
|
<Route key={`${index++}`} path="/" element={<HomePage/>} />,
|
||||||
<Route key={`${index++}`} path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
|
<Route key={`${index++}`} path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} submitQuery={submitQuery} />} />,
|
||||||
<Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
|
<Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} submitQuery={submitQuery} />} />,
|
||||||
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
|
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} submitQuery={submitQuery} />} />,
|
||||||
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
|
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} submitQuery={submitQuery} />} />,
|
||||||
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
|
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} submitQuery={submitQuery} />} />,
|
||||||
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{sessionId, setSnack, submitQuery}} />} />,
|
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{ setSnack, submitQuery }} />} />,
|
||||||
<Route key={`${index++}`} path="/job-analysis" element={<JobAnalysisPage />} />,
|
<Route key={`${index++}`} path="/job-analysis" element={<JobAnalysisPage />} />,
|
||||||
<Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...{ sessionId, setSnack, submitQuery }} />} />,
|
<Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...{ setSnack, submitQuery }} />} />,
|
||||||
<Route key={`${index++}`} path="/settings" element={<ControlsPage {...{ sessionId, setSnack, submitQuery }} />} />,
|
<Route key={`${index++}`} path="/settings" element={<ControlsPage {...{ setSnack, submitQuery }} />} />,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (user === undefined || user === null) {
|
if (!user) {
|
||||||
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
|
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
|
||||||
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage />} />);
|
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage />} />);
|
||||||
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
||||||
|
@ -32,12 +32,13 @@ import {
|
|||||||
|
|
||||||
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
|
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
|
||||||
import { Beta } from 'components/Beta';
|
import { Beta } from 'components/Beta';
|
||||||
import 'components/layout/Header.css';
|
import { useUser } from 'hooks/useUser';
|
||||||
import { useUser } from 'components/UserContext';
|
import { Candidate, Employer } from 'types/types';
|
||||||
// import { Candidate, Employer } from '../types/types';
|
|
||||||
import { SetSnackType } from 'components/Snack';
|
import { SetSnackType } from 'components/Snack';
|
||||||
import { CopyBubble } from 'components/CopyBubble';
|
import { CopyBubble } from 'components/CopyBubble';
|
||||||
|
|
||||||
|
import 'components/layout/Header.css';
|
||||||
|
|
||||||
// Styled components
|
// Styled components
|
||||||
const StyledAppBar = styled(AppBar, {
|
const StyledAppBar = styled(AppBar, {
|
||||||
shouldForwardProp: (prop) => prop !== 'transparent',
|
shouldForwardProp: (prop) => prop !== 'transparent',
|
||||||
@ -45,6 +46,8 @@ const StyledAppBar = styled(AppBar, {
|
|||||||
backgroundColor: transparent ? 'transparent' : theme.palette.primary.main,
|
backgroundColor: transparent ? 'transparent' : theme.palette.primary.main,
|
||||||
boxShadow: transparent ? 'none' : '',
|
boxShadow: transparent ? 'none' : '',
|
||||||
transition: 'background-color 0.3s ease',
|
transition: 'background-color 0.3s ease',
|
||||||
|
borderRadius: 0,
|
||||||
|
padding: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const NavLinksContainer = styled(Box)(({ theme }) => ({
|
const NavLinksContainer = styled(Box)(({ theme }) => ({
|
||||||
@ -96,8 +99,8 @@ interface HeaderProps {
|
|||||||
|
|
||||||
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
// const candidate: Candidate | null = (user && user.userType === "UserType.CANDIDATE") ? user as Candidate : null;
|
const candidate: Candidate | null = (user && user.userType === "candidate") ? user as Candidate : null;
|
||||||
// const employer: Employer | null = (user && user.userType === "UserType.EMPLOYER") ? user as Employer : null;
|
const employer: Employer | null = (user && user.userType === "employer") ? user as Employer : null;
|
||||||
const {
|
const {
|
||||||
transparent = false,
|
transparent = false,
|
||||||
className,
|
className,
|
||||||
@ -111,6 +114,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const name = (candidate ? candidate.username : user?.email) || '';
|
||||||
|
|
||||||
const BackstoryLogo = () => {
|
const BackstoryLogo = () => {
|
||||||
return <Typography
|
return <Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
@ -299,10 +304,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
height: 32,
|
height: 32,
|
||||||
bgcolor: theme.palette.secondary.main,
|
bgcolor: theme.palette.secondary.main,
|
||||||
}}>
|
}}>
|
||||||
{user?.username.charAt(0).toUpperCase()}
|
{name.charAt(0).toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||||
{user?.username}
|
{name}
|
||||||
</Box>
|
</Box>
|
||||||
<ExpandMore fontSize="small" />
|
<ExpandMore fontSize="small" />
|
||||||
</UserButton>
|
</UserButton>
|
||||||
@ -376,7 +381,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
position="fixed"
|
position="fixed"
|
||||||
transparent={transparent}
|
transparent={transparent}
|
||||||
className={className}
|
className={className}
|
||||||
sx={{ overflow: "hidden" }}
|
sx={{ overflow: "hidden" }}
|
||||||
>
|
>
|
||||||
<Container maxWidth="xl">
|
<Container maxWidth="xl">
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
@ -392,17 +397,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
{renderUserSection()}
|
{renderUserSection()}
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<Tooltip title="Open Menu">
|
<Tooltip title="Open Menu">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="open drawer"
|
aria-label="open drawer"
|
||||||
edge="end"
|
edge="end"
|
||||||
onClick={handleDrawerToggle}
|
onClick={handleDrawerToggle}
|
||||||
sx={{ display: { md: 'none' } }}
|
sx={{ display: { md: 'none' } }}
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{sessionId && <CopyBubble
|
{sessionId && <CopyBubble
|
||||||
tooltip="Copy link"
|
tooltip="Copy link"
|
||||||
|
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 { backstoryTheme } from './BackstoryTheme';
|
||||||
import { BrowserRouter as Router } from "react-router-dom";
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
import { BackstoryApp } from './BackstoryApp';
|
import { BackstoryApp } from './BackstoryApp';
|
||||||
import { BackstoryTestApp } from 'TestApp';
|
// import { BackstoryTestApp } from 'TestApp';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@ -16,8 +16,8 @@ root.render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider theme={backstoryTheme}>
|
<ThemeProvider theme={backstoryTheme}>
|
||||||
<Router>
|
<Router>
|
||||||
{/* <BackstoryApp /> */}
|
<BackstoryApp />
|
||||||
<BackstoryTestApp />
|
{/* <BackstoryTestApp /> */}
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
@ -10,7 +10,7 @@ import { Candidate } from "../types/types";
|
|||||||
|
|
||||||
const CandidateListingPage = (props: BackstoryPageProps) => {
|
const CandidateListingPage = (props: BackstoryPageProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { sessionId, setSnack } = props;
|
const { setSnack } = props;
|
||||||
const [candidates, setCandidates] = useState<Candidate[] | undefined>(undefined);
|
const [candidates, setCandidates] = useState<Candidate[] | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -20,7 +20,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
|
|||||||
const fetchCandidates = async () => {
|
const fetchCandidates = async () => {
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
response = await fetch(`${connectionBase}/api/u/${sessionId}`, {
|
response = await fetch(`${connectionBase}/api/u`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -45,7 +45,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchCandidates();
|
fetchCandidates();
|
||||||
}, [candidates, sessionId, setSnack]);
|
}, [candidates, setSnack]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{display: "flex", flexDirection: "column"}}>
|
<Box sx={{display: "flex", flexDirection: "column"}}>
|
||||||
@ -66,7 +66,7 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
|
|||||||
}}
|
}}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<CandidateInfo sessionId={sessionId} sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} user={u} />
|
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} candidate={u} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -8,11 +8,11 @@ import { BackstoryPageProps } from '../components/BackstoryTab';
|
|||||||
import { Conversation, ConversationHandle } from '../components/Conversation';
|
import { Conversation, ConversationHandle } from '../components/Conversation';
|
||||||
import { ChatQuery } from '../components/ChatQuery';
|
import { ChatQuery } from '../components/ChatQuery';
|
||||||
import { CandidateInfo } from 'components/CandidateInfo';
|
import { CandidateInfo } from 'components/CandidateInfo';
|
||||||
import { useUser } from "../components/UserContext";
|
import { useUser } from "../hooks/useUser";
|
||||||
import { Candidate } from "../types/types";
|
import { Candidate } from "../types/types";
|
||||||
|
|
||||||
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||||
const { sessionId, setSnack, submitQuery } = props;
|
const { setSnack, submitQuery } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
||||||
@ -42,15 +42,14 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<CandidateInfo sessionId={sessionId} action="Chat with Backstory AI about " />
|
<CandidateInfo candidate={candidate} action="Chat with Backstory AI about " />
|
||||||
<Conversation
|
<Conversation
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...{
|
{...{
|
||||||
multiline: true,
|
multiline: true,
|
||||||
type: "chat",
|
type: "chat",
|
||||||
placeholder: `What would you like to know about ${candidate?.firstName}?`,
|
placeholder: `What would you like to know about ${candidate?.firstName}?`,
|
||||||
resetLabel: "chat",
|
resetLabel: "chat",
|
||||||
sessionId,
|
|
||||||
setSnack,
|
setSnack,
|
||||||
defaultPrompts: questions,
|
defaultPrompts: questions,
|
||||||
submitQuery,
|
submitQuery,
|
||||||
|
@ -85,7 +85,7 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ControlsPage = (props: BackstoryPageProps) => {
|
const ControlsPage = (props: BackstoryPageProps) => {
|
||||||
const { setSnack, sessionId } = props;
|
const { setSnack } = props;
|
||||||
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
||||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
||||||
const [tools, setTools] = useState<Tool[]>([]);
|
const [tools, setTools] = useState<Tool[]>([]);
|
||||||
@ -95,12 +95,12 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
const [serverTunables, setServerTunables] = useState<ServerTunables | undefined>(undefined);
|
const [serverTunables, setServerTunables] = useState<ServerTunables | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serverTunables === undefined || systemPrompt === serverTunables.system_prompt || !systemPrompt.trim() || sessionId === undefined) {
|
if (serverTunables === undefined || systemPrompt === serverTunables.system_prompt || !systemPrompt.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sendSystemPrompt = async (prompt: string) => {
|
const sendSystemPrompt = async (prompt: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -122,11 +122,11 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
|
|
||||||
sendSystemPrompt(systemPrompt);
|
sendSystemPrompt(systemPrompt);
|
||||||
|
|
||||||
}, [systemPrompt, sessionId, setSnack, serverTunables]);
|
}, [systemPrompt, setSnack, serverTunables]);
|
||||||
|
|
||||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt")[], message: string = "Update successful.") => {
|
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt")[], message: string = "Update successful.") => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/reset/`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -173,12 +173,12 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
|
|
||||||
// Get the system information
|
// Get the system information
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (systemInfo !== undefined || sessionId === undefined) {
|
if (systemInfo !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fetchSystemInfo = async () => {
|
const fetchSystemInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/system-info/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/system-info`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -207,7 +207,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
|
|
||||||
fetchSystemInfo();
|
fetchSystemInfo();
|
||||||
|
|
||||||
}, [systemInfo, setSystemInfo, setSnack, sessionId])
|
}, [systemInfo, setSystemInfo, setSnack])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditSystemPrompt(systemPrompt.trim());
|
setEditSystemPrompt(systemPrompt.trim());
|
||||||
@ -216,7 +216,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
const toggleRag = async (tool: Tool) => {
|
const toggleRag = async (tool: Tool) => {
|
||||||
tool.enabled = !tool.enabled
|
tool.enabled = !tool.enabled
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -238,7 +238,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
const toggleTool = async (tool: Tool) => {
|
const toggleTool = async (tool: Tool) => {
|
||||||
tool.enabled = !tool.enabled
|
tool.enabled = !tool.enabled
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -259,13 +259,13 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
|
|
||||||
// If the systemPrompt has not been set, fetch it from the server
|
// If the systemPrompt has not been set, fetch it from the server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serverTunables !== undefined || sessionId === undefined) {
|
if (serverTunables !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fetchTunables = async () => {
|
const fetchTunables = async () => {
|
||||||
try {
|
try {
|
||||||
// Make the fetch request with proper headers
|
// Make the fetch request with proper headers
|
||||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
const response = await fetch(connectionBase + `/api/tunables`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -285,7 +285,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchTunables();
|
fetchTunables();
|
||||||
}, [sessionId, setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags, setSnack]);
|
}, [setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags, setSnack]);
|
||||||
|
|
||||||
const toggle = async (type: string, index: number) => {
|
const toggle = async (type: string, index: number) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -66,7 +66,7 @@ const Sidebar: React.FC<{
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: 'divider'
|
borderColor: 'divider',
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="h6" component="h2" fontWeight="bold">
|
<Typography variant="h6" component="h2" fontWeight="bold">
|
||||||
Documentation
|
Documentation
|
||||||
@ -169,7 +169,7 @@ const documentTitleFromRoute = (route: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DocsPage = (props: BackstoryPageProps) => {
|
const DocsPage = (props: BackstoryPageProps) => {
|
||||||
const { sessionId, submitQuery, setSnack } = props;
|
const { submitQuery, setSnack } = props;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { paramPage = '' } = useParams();
|
const { paramPage = '' } = useParams();
|
||||||
@ -244,7 +244,6 @@ const DocsPage = (props: BackstoryPageProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
{page && <Document
|
{page && <Document
|
||||||
filepath={`/docs/${page}.md`}
|
filepath={`/docs/${page}.md`}
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
submitQuery={submitQuery}
|
||||||
setSnack={setSnack}
|
setSnack={setSnack}
|
||||||
/>}
|
/>}
|
||||||
@ -272,7 +271,7 @@ const DocsPage = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
// Document grid for landing page
|
// Document grid for landing page
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 3 }} elevation={1}>
|
<Paper sx={{ p: 5, border: "3px solid orange" }} elevation={1}>
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
Documentation
|
Documentation
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -280,20 +279,18 @@ const DocsPage = (props: BackstoryPageProps) => {
|
|||||||
Select a document from the sidebar to view detailed technical information about the application.
|
Select a document from the sidebar to view detailed technical information about the application.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={1}>
|
||||||
{documents.map((doc, index) => {
|
{documents.map((doc, index) => {
|
||||||
if (doc.route === null) return (<></>);
|
if (doc.route === null) return (<></>);
|
||||||
return (<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
return (<Grid sx={{ minWidth: "164px" }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||||
<Card>
|
<Card sx={{ minHeight: "180px" }}>
|
||||||
<CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}>
|
<CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}>
|
||||||
<CardContent>
|
<CardContent sx={{ display: "flex", flexDirection: "column", m: 0, p: 1 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: "row", gap: 1, verticalAlign: 'top' }}>
|
||||||
<Box sx={{ color: 'primary.main', mr: 1.5 }}>
|
{getDocumentIcon(doc.title)}
|
||||||
{getDocumentIcon(doc.title)}
|
<Typography variant="h3" sx={{ m: "0 !important" }}>{doc.title}</Typography>
|
||||||
</Box>
|
|
||||||
<Typography variant="h6">{doc.title}</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
|
<Typography variant="body2" color="text.secondary">
|
||||||
{doc.description}
|
{doc.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -14,14 +14,13 @@ import { jsonrepair } from 'jsonrepair';
|
|||||||
import { CandidateInfo } from '../components/CandidateInfo';
|
import { CandidateInfo } from '../components/CandidateInfo';
|
||||||
import { Query } from '../types/types'
|
import { Query } from '../types/types'
|
||||||
import { Quote } from 'components/Quote';
|
import { Quote } from 'components/Quote';
|
||||||
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
|
|
||||||
import { connectionBase } from 'utils/Global';
|
|
||||||
import { Candidate } from '../types/types';
|
import { Candidate } from '../types/types';
|
||||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||||
import { Scrollable } from '../components/Scrollable';
|
import { Scrollable } from '../components/Scrollable';
|
||||||
import { Pulse } from 'components/Pulse';
|
import { Pulse } from 'components/Pulse';
|
||||||
|
import { StreamingResponse } from 'types/api-client';
|
||||||
|
|
||||||
const emptyUser: Candidate = {
|
const emptyUser: Candidate = {
|
||||||
description: "[blank]",
|
description: "[blank]",
|
||||||
@ -47,7 +46,7 @@ const emptyUser: Candidate = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GenerateCandidate = (props: BackstoryElementProps) => {
|
const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||||
const {sessionId, setSnack, submitQuery} = props;
|
const { setSnack, submitQuery } = props;
|
||||||
const [streaming, setStreaming] = useState<string>('');
|
const [streaming, setStreaming] = useState<string>('');
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [user, setUser] = useState<Candidate | null>(null);
|
const [user, setUser] = useState<Candidate | null>(null);
|
||||||
@ -60,7 +59,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false);
|
const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false);
|
||||||
|
|
||||||
// Only keep refs that are truly necessary
|
// Only keep refs that are truly necessary
|
||||||
const controllerRef = useRef<StreamQueryController>(null);
|
const controllerRef = useRef<StreamingResponse>(null);
|
||||||
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||||
|
|
||||||
const generatePersona = useCallback((query: Query) => {
|
const generatePersona = useCallback((query: Query) => {
|
||||||
@ -77,69 +76,68 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
setCanGenImage(false);
|
setCanGenImage(false);
|
||||||
setShouldGenerateProfile(false); // Reset the flag
|
setShouldGenerateProfile(false); // Reset the flag
|
||||||
|
|
||||||
controllerRef.current = streamQueryResponse({
|
// controllerRef.current = streamQueryResponse({
|
||||||
query,
|
// query,
|
||||||
type: "persona",
|
// type: "persona",
|
||||||
sessionId,
|
// connectionBase,
|
||||||
connectionBase,
|
// onComplete: (msg) => {
|
||||||
onComplete: (msg) => {
|
// switch (msg.status) {
|
||||||
switch (msg.status) {
|
// case "partial":
|
||||||
case "partial":
|
// case "done":
|
||||||
case "done":
|
// setState(currentState => {
|
||||||
setState(currentState => {
|
// switch (currentState) {
|
||||||
switch (currentState) {
|
// case 0: /* Generating persona */
|
||||||
case 0: /* Generating persona */
|
// let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
|
||||||
let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
|
// if (!partialUser.fullName) {
|
||||||
if (!partialUser.fullName) {
|
// partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`;
|
||||||
partialUser.fullName = `${partialUser.firstName} ${partialUser.lastName}`;
|
// }
|
||||||
}
|
// console.log("Setting final user data:", partialUser);
|
||||||
console.log("Setting final user data:", partialUser);
|
// setUser({ ...partialUser });
|
||||||
setUser({ ...partialUser });
|
// return 1; /* Generating resume */
|
||||||
return 1; /* Generating resume */
|
// case 1: /* Generating resume */
|
||||||
case 1: /* Generating resume */
|
// setResume(msg.response || '');
|
||||||
setResume(msg.response || '');
|
// return 2; /* RAG generation */
|
||||||
return 2; /* RAG generation */
|
// case 2: /* RAG generation */
|
||||||
case 2: /* RAG generation */
|
// return 3; /* Image generation */
|
||||||
return 3; /* Image generation */
|
// default:
|
||||||
default:
|
// return currentState;
|
||||||
return currentState;
|
// }
|
||||||
}
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
if (msg.status === "done") {
|
// if (msg.status === "done") {
|
||||||
setProcessing(false);
|
// setProcessing(false);
|
||||||
setCanGenImage(true);
|
// setCanGenImage(true);
|
||||||
setStatus('');
|
// setStatus('');
|
||||||
controllerRef.current = null;
|
// controllerRef.current = null;
|
||||||
setState(0);
|
// setState(0);
|
||||||
// Set flag to trigger profile generation after user state updates
|
// // Set flag to trigger profile generation after user state updates
|
||||||
console.log("Persona generation complete, setting shouldGenerateProfile flag");
|
// console.log("Persona generation complete, setting shouldGenerateProfile flag");
|
||||||
setShouldGenerateProfile(true);
|
// setShouldGenerateProfile(true);
|
||||||
}
|
// }
|
||||||
break;
|
// break;
|
||||||
case "thinking":
|
// case "thinking":
|
||||||
setStatus(msg.response || '');
|
// setStatus(msg.response || '');
|
||||||
break;
|
// break;
|
||||||
|
|
||||||
case "error":
|
// case "error":
|
||||||
console.log(`Error generating persona: ${msg.response}`);
|
// console.log(`Error generating persona: ${msg.response}`);
|
||||||
setSnack(msg.response || "", "error");
|
// setSnack(msg.response || "", "error");
|
||||||
setProcessing(false);
|
// setProcessing(false);
|
||||||
setUser(emptyUser);
|
// setUser(emptyUser);
|
||||||
controllerRef.current = null;
|
// controllerRef.current = null;
|
||||||
setState(0);
|
// setState(0);
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
onStreaming: (chunk) => {
|
// onStreaming: (chunk) => {
|
||||||
setStreaming(chunk);
|
// setStreaming(chunk);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}, [sessionId, setSnack]);
|
}, [setSnack]);
|
||||||
|
|
||||||
const cancelQuery = useCallback(() => {
|
const cancelQuery = useCallback(() => {
|
||||||
if (controllerRef.current) {
|
if (controllerRef.current) {
|
||||||
controllerRef.current.abort();
|
controllerRef.current.cancel();
|
||||||
controllerRef.current = null;
|
controllerRef.current = null;
|
||||||
setState(0);
|
setState(0);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@ -184,67 +182,67 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
setState(3);
|
setState(3);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
controllerRef.current = streamQueryResponse({
|
// controllerRef.current = streamQueryResponse({
|
||||||
query: {
|
// query: {
|
||||||
prompt: imagePrompt,
|
// prompt: imagePrompt,
|
||||||
agentOptions: {
|
// agentOptions: {
|
||||||
username: user?.username,
|
// username: user?.username,
|
||||||
filename: "profile.png"
|
// filename: "profile.png"
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
type: "image",
|
// type: "image",
|
||||||
sessionId,
|
// sessionId,
|
||||||
connectionBase,
|
// connectionBase,
|
||||||
onComplete: (msg) => {
|
// onComplete: (msg) => {
|
||||||
// console.log("Profile generation response:", msg);
|
// // console.log("Profile generation response:", msg);
|
||||||
switch (msg.status) {
|
// switch (msg.status) {
|
||||||
case "partial":
|
// case "partial":
|
||||||
case "done":
|
// case "done":
|
||||||
if (msg.status === "done") {
|
// if (msg.status === "done") {
|
||||||
setProcessing(false);
|
// setProcessing(false);
|
||||||
controllerRef.current = null;
|
// controllerRef.current = null;
|
||||||
setState(0);
|
// setState(0);
|
||||||
setCanGenImage(true);
|
// setCanGenImage(true);
|
||||||
setShouldGenerateProfile(false);
|
// setShouldGenerateProfile(false);
|
||||||
setUser({
|
// setUser({
|
||||||
...(user ? user : emptyUser),
|
// ...(user ? user : emptyUser),
|
||||||
hasProfile: true
|
// hasProfile: true
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
break;
|
// break;
|
||||||
case "error":
|
// case "error":
|
||||||
console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
|
// console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
|
||||||
setSnack(msg.response || "", "error");
|
// setSnack(msg.response || "", "error");
|
||||||
setProcessing(false);
|
// setProcessing(false);
|
||||||
controllerRef.current = null;
|
// controllerRef.current = null;
|
||||||
setState(0);
|
// setState(0);
|
||||||
setCanGenImage(true);
|
// setCanGenImage(true);
|
||||||
setShouldGenerateProfile(false);
|
// setShouldGenerateProfile(false);
|
||||||
break;
|
// break;
|
||||||
default:
|
// default:
|
||||||
let data: any = {};
|
// let data: any = {};
|
||||||
try {
|
// try {
|
||||||
data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
|
// data = typeof msg.response === 'string' ? JSON.parse(msg.response) : msg.response;
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
data = { message: msg.response };
|
// data = { message: msg.response };
|
||||||
}
|
// }
|
||||||
if (msg.status !== "heartbeat") {
|
// if (msg.status !== "heartbeat") {
|
||||||
console.log(data);
|
// console.log(data);
|
||||||
}
|
// }
|
||||||
if (data.timestamp) {
|
// if (data.timestamp) {
|
||||||
setTimestamp(data.timestamp);
|
// setTimestamp(data.timestamp);
|
||||||
} else {
|
// } else {
|
||||||
setTimestamp(Date.now())
|
// setTimestamp(Date.now())
|
||||||
}
|
// }
|
||||||
if (data.message) {
|
// if (data.message) {
|
||||||
setStatus(data.message);
|
// setStatus(data.message);
|
||||||
}
|
// }
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
}, [shouldGenerateProfile, user, prompt, sessionId, setSnack]);
|
}, [shouldGenerateProfile, user, prompt, setSnack]);
|
||||||
|
|
||||||
// Handle streaming updates based on current state
|
// Handle streaming updates based on current state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -274,10 +272,6 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
}
|
}
|
||||||
}, [streaming, state]);
|
}, [streaming, state]);
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="GenerateCandidate" sx={{
|
<Box className="GenerateCandidate" sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -287,8 +281,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
|
maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
|
||||||
}}>
|
}}>
|
||||||
{user && <CandidateInfo
|
{user && <CandidateInfo
|
||||||
sessionId={sessionId}
|
candidate={user}
|
||||||
user={user}
|
|
||||||
sx={{flexShrink: 1}}/>
|
sx={{flexShrink: 1}}/>
|
||||||
}
|
}
|
||||||
{ prompt &&
|
{ prompt &&
|
||||||
@ -322,7 +315,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
}}>
|
}}>
|
||||||
<Box sx={{ display: "flex", position: "relative", width: "min-content", height: "min-content" }}>
|
<Box sx={{ display: "flex", position: "relative", width: "min-content", height: "min-content" }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={user?.hasProfile ? `/api/u/${user.username}/profile/${sessionId}` : ''}
|
src={user?.hasProfile ? `/api/u/${user.username}/profile` : ''}
|
||||||
alt={`${user?.fullName}'s profile`}
|
alt={`${user?.fullName}'s profile`}
|
||||||
sx={{
|
sx={{
|
||||||
width: 80,
|
width: 80,
|
||||||
@ -339,7 +332,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
sx={{ m: 1, gap: 1, justifySelf: "flex-start", alignSelf: "center", flexGrow: 0, maxHeight: "min-content" }}
|
sx={{ m: 1, gap: 1, justifySelf: "flex-start", alignSelf: "center", flexGrow: 0, maxHeight: "min-content" }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={
|
disabled={
|
||||||
sessionId === undefined || processing || !canGenImage
|
processing || !canGenImage
|
||||||
}
|
}
|
||||||
onClick={() => { setShouldGenerateProfile(true); }}>
|
onClick={() => { setShouldGenerateProfile(true); }}>
|
||||||
{user?.hasProfile ? 'Re-' : ''}Generate Picture<SendIcon />
|
{user?.hasProfile ? 'Re-' : ''}Generate Picture<SendIcon />
|
||||||
@ -351,7 +344,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
{ resume !== '' &&
|
{ resume !== '' &&
|
||||||
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
|
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
|
||||||
<Scrollable sx={{flexGrow: 1}}>
|
<Scrollable sx={{flexGrow: 1}}>
|
||||||
<StyledMarkdown {...{content: resume, setSnack, sessionId, submitQuery}}/>
|
<StyledMarkdown {...{ content: resume, setSnack, submitQuery }} />
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
</Paper> }
|
</Paper> }
|
||||||
<BackstoryTextField
|
<BackstoryTextField
|
||||||
@ -367,7 +360,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
<Button
|
<Button
|
||||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={sessionId === undefined || processing}
|
disabled={processing}
|
||||||
onClick={handleSendClick}>
|
onClick={handleSendClick}>
|
||||||
Generate New Persona<SendIcon />
|
Generate New Persona<SendIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@ -381,7 +374,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||||
size="large"
|
size="large"
|
||||||
edge="start"
|
edge="start"
|
||||||
disabled={controllerRef.current === null || !sessionId || processing === false}
|
disabled={controllerRef.current === null || processing === false}
|
||||||
>
|
>
|
||||||
<CancelIcon />
|
<CancelIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { BackstoryPageProps } from '../components/BackstoryTab';
|
import { BackstoryPageProps } from '../components/BackstoryTab';
|
||||||
import { BackstoryMessage, Message } from '../components/Message';
|
import { Message } from '../components/Message';
|
||||||
|
import { ChatMessage } from 'types/types';
|
||||||
|
|
||||||
const LoadingPage = (props: BackstoryPageProps) => {
|
const LoadingPage = (props: BackstoryPageProps) => {
|
||||||
const backstoryPreamble: BackstoryMessage = {
|
const preamble: ChatMessage = {
|
||||||
role: 'info',
|
sender: 'system',
|
||||||
title: 'Please wait while connecting to Backstory...',
|
status: 'done',
|
||||||
disableCopy: true,
|
sessionId: '',
|
||||||
content: '...',
|
content: 'Please wait while connecting to Backstory...',
|
||||||
expandable: false,
|
timestamp: new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box sx={{display: "flex", flexGrow: 1, maxWidth: "1024px", margin: "0 auto"}}>
|
return <Box sx={{display: "flex", flexGrow: 1, maxWidth: "1024px", margin: "0 auto"}}>
|
||||||
<Message message={backstoryPreamble} {...props} />
|
<Message message={preamble} {...props} />
|
||||||
</Box>
|
</Box>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ import './ResumeBuilderPage.css';
|
|||||||
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||||
const {
|
const {
|
||||||
sx,
|
sx,
|
||||||
sessionId,
|
|
||||||
setSnack,
|
setSnack,
|
||||||
submitQuery,
|
submitQuery,
|
||||||
} = props
|
} = props
|
||||||
@ -196,191 +195,194 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
|
|||||||
setHasFacts(false);
|
setHasFacts(false);
|
||||||
}, [setHasFacts]);
|
}, [setHasFacts]);
|
||||||
|
|
||||||
const renderJobDescriptionView = useCallback((sx?: SxProps) => {
|
return (<Box>Not re-implmented yet</Box>);
|
||||||
console.log('renderJobDescriptionView');
|
|
||||||
const jobDescriptionQuestions = [
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<ChatQuery query={{ prompt: "What are the key skills necessary for this position?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} />
|
|
||||||
<ChatQuery query={{ prompt: "How much should this position pay (accounting for inflation)?", tunables: { enableTools: false } }} submitQuery={handleJobQuery} />
|
|
||||||
</Box>,
|
|
||||||
];
|
|
||||||
|
|
||||||
const jobDescriptionPreamble: MessageList = [{
|
|
||||||
role: 'info',
|
|
||||||
content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
|
|
||||||
|
|
||||||
1. **Job Analysis**: LLM extracts requirements from '\`Job Description\`' to generate a list of desired '\`Skills\`'.
|
// const renderJobDescriptionView = useCallback((sx?: SxProps) => {
|
||||||
2. **Candidate Analysis**: LLM determines candidate qualifications by performing skill assessments.
|
// 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\`'.
|
// 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\`'.
|
// 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.
|
// 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.
|
// See [About > Resume Generation Architecture](/about/resume-generation) for more details.
|
||||||
`,
|
// `,
|
||||||
disableCopy: true
|
// disableCopy: true
|
||||||
}];
|
// }];
|
||||||
|
|
||||||
|
|
||||||
if (!hasJobDescription) {
|
// if (!hasJobDescription) {
|
||||||
return <Conversation
|
// return <Conversation
|
||||||
ref={jobConversationRef}
|
// ref={jobConversationRef}
|
||||||
{...{
|
// {...{
|
||||||
type: "job_description",
|
// type: "job_description",
|
||||||
actionLabel: "Generate Resume",
|
// actionLabel: "Generate Resume",
|
||||||
preamble: jobDescriptionPreamble,
|
// preamble: jobDescriptionPreamble,
|
||||||
hidePreamble: true,
|
// hidePreamble: true,
|
||||||
placeholder: "Paste a job description, then click Generate...",
|
// placeholder: "Paste a job description, then click Generate...",
|
||||||
multiline: true,
|
// multiline: true,
|
||||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||||
messageFilter: filterJobDescriptionMessages,
|
// messageFilter: filterJobDescriptionMessages,
|
||||||
resetAction: resetJobDescription,
|
// resetAction: resetJobDescription,
|
||||||
onResponse: jobResponse,
|
// onResponse: jobResponse,
|
||||||
sessionId,
|
// sessionId,
|
||||||
setSnack,
|
// setSnack,
|
||||||
submitQuery,
|
// submitQuery,
|
||||||
sx,
|
// sx,
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
|
|
||||||
} else {
|
// } else {
|
||||||
return <Conversation
|
// return <Conversation
|
||||||
ref={jobConversationRef}
|
// ref={jobConversationRef}
|
||||||
{...{
|
// {...{
|
||||||
type: "job_description",
|
// type: "job_description",
|
||||||
actionLabel: "Send",
|
// actionLabel: "Send",
|
||||||
placeholder: "Ask a question about this job description...",
|
// placeholder: "Ask a question about this job description...",
|
||||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||||
messageFilter: filterJobDescriptionMessages,
|
// messageFilter: filterJobDescriptionMessages,
|
||||||
defaultPrompts: jobDescriptionQuestions,
|
// defaultPrompts: jobDescriptionQuestions,
|
||||||
resetAction: resetJobDescription,
|
// resetAction: resetJobDescription,
|
||||||
onResponse: jobResponse,
|
// onResponse: jobResponse,
|
||||||
sessionId,
|
// sessionId,
|
||||||
setSnack,
|
// setSnack,
|
||||||
submitQuery,
|
// submitQuery,
|
||||||
sx,
|
// sx,
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
}
|
// }
|
||||||
}, [filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume, submitQuery]);
|
// }, [filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume, submitQuery]);
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Renders the resume view with loading indicator
|
// * Renders the resume view with loading indicator
|
||||||
*/
|
// */
|
||||||
const renderResumeView = useCallback((sx?: SxProps) => {
|
// const renderResumeView = useCallback((sx?: SxProps) => {
|
||||||
const resumeQuestions = [
|
// const resumeQuestions = [
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
// <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: "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} />
|
// <ChatQuery query={{ prompt: "Provide a more concise resume.", tunables: { enableTools: false } }} submitQuery={handleResumeQuery} />
|
||||||
</Box>,
|
// </Box>,
|
||||||
];
|
// ];
|
||||||
|
|
||||||
if (!hasFacts) {
|
// if (!hasFacts) {
|
||||||
return <Conversation
|
// return <Conversation
|
||||||
ref={resumeConversationRef}
|
// ref={resumeConversationRef}
|
||||||
{...{
|
// {...{
|
||||||
type: "resume",
|
// type: "resume",
|
||||||
actionLabel: "Fact Check",
|
// actionLabel: "Fact Check",
|
||||||
defaultQuery: "Fact check the resume.",
|
// defaultQuery: "Fact check the resume.",
|
||||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||||
messageFilter: filterResumeMessages,
|
// messageFilter: filterResumeMessages,
|
||||||
onResponse: resumeResponse,
|
// onResponse: resumeResponse,
|
||||||
resetAction: resetResume,
|
// resetAction: resetResume,
|
||||||
sessionId,
|
// sessionId,
|
||||||
setSnack,
|
// setSnack,
|
||||||
submitQuery,
|
// submitQuery,
|
||||||
sx,
|
// sx,
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
} else {
|
// } else {
|
||||||
return <Conversation
|
// return <Conversation
|
||||||
ref={resumeConversationRef}
|
// ref={resumeConversationRef}
|
||||||
{...{
|
// {...{
|
||||||
type: "resume",
|
// type: "resume",
|
||||||
actionLabel: "Send",
|
// actionLabel: "Send",
|
||||||
placeholder: "Ask a question about this job resume...",
|
// placeholder: "Ask a question about this job resume...",
|
||||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||||
messageFilter: filterResumeMessages,
|
// messageFilter: filterResumeMessages,
|
||||||
onResponse: resumeResponse,
|
// onResponse: resumeResponse,
|
||||||
resetAction: resetResume,
|
// resetAction: resetResume,
|
||||||
sessionId,
|
// sessionId,
|
||||||
setSnack,
|
// setSnack,
|
||||||
defaultPrompts: resumeQuestions,
|
// defaultPrompts: resumeQuestions,
|
||||||
submitQuery,
|
// submitQuery,
|
||||||
sx,
|
// sx,
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
}
|
// }
|
||||||
}, [filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume, submitQuery]);
|
// }, [filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume, submitQuery]);
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Renders the fact check view
|
// * Renders the fact check view
|
||||||
*/
|
// */
|
||||||
const renderFactCheckView = useCallback((sx?: SxProps) => {
|
// const renderFactCheckView = useCallback((sx?: SxProps) => {
|
||||||
const factsQuestions = [
|
// const factsQuestions = [
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
// <Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
<ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enableTools: false } }} submitQuery={handleFactsQuery} />
|
// <ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enableTools: false } }} submitQuery={handleFactsQuery} />
|
||||||
</Box>,
|
// </Box>,
|
||||||
];
|
// ];
|
||||||
|
|
||||||
return <Conversation
|
// return <Conversation
|
||||||
ref={factsConversationRef}
|
// ref={factsConversationRef}
|
||||||
{...{
|
// {...{
|
||||||
type: "fact_check",
|
// type: "fact_check",
|
||||||
actionLabel: "Send",
|
// actionLabel: "Send",
|
||||||
placeholder: "Ask a question about any discrepencies...",
|
// placeholder: "Ask a question about any discrepencies...",
|
||||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
// resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||||
messageFilter: filterFactsMessages,
|
// messageFilter: filterFactsMessages,
|
||||||
defaultPrompts: factsQuestions,
|
// defaultPrompts: factsQuestions,
|
||||||
resetAction: resetFacts,
|
// resetAction: resetFacts,
|
||||||
onResponse: factsResponse,
|
// onResponse: factsResponse,
|
||||||
sessionId,
|
// sessionId,
|
||||||
submitQuery,
|
// submitQuery,
|
||||||
setSnack,
|
// setSnack,
|
||||||
sx,
|
// sx,
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
}, [ sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts, submitQuery]);
|
// }, [ sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts, submitQuery]);
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<Box className="ResumeBuilder"
|
// <Box className="ResumeBuilder"
|
||||||
sx={{
|
// sx={{
|
||||||
p: 0,
|
// p: 0,
|
||||||
m: 0,
|
// m: 0,
|
||||||
display: "flex",
|
// display: "flex",
|
||||||
flexGrow: 1,
|
// flexGrow: 1,
|
||||||
margin: "0 auto",
|
// margin: "0 auto",
|
||||||
overflow: "hidden",
|
// overflow: "hidden",
|
||||||
backgroundColor: "#F5F5F5",
|
// backgroundColor: "#F5F5F5",
|
||||||
flexDirection: "column",
|
// flexDirection: "column",
|
||||||
maxWidth: "1024px",
|
// maxWidth: "1024px",
|
||||||
}}
|
// }}
|
||||||
>
|
// >
|
||||||
{/* Tabs */}
|
// {/* Tabs */}
|
||||||
<Tabs
|
// <Tabs
|
||||||
value={activeTab}
|
// value={activeTab}
|
||||||
onChange={handleTabChange}
|
// onChange={handleTabChange}
|
||||||
variant="fullWidth"
|
// variant="fullWidth"
|
||||||
sx={{ bgcolor: 'background.paper' }}
|
// sx={{ bgcolor: 'background.paper' }}
|
||||||
>
|
// >
|
||||||
<Tab value={0} label="Job Description" />
|
// <Tab value={0} label="Job Description" />
|
||||||
{hasResume && <Tab value={1} label="Resume" />}
|
// {hasResume && <Tab value={1} label="Resume" />}
|
||||||
{hasFacts && <Tab value={2} label="Fact Check" />}
|
// {hasFacts && <Tab value={2} label="Fact Check" />}
|
||||||
</Tabs>
|
// </Tabs>
|
||||||
|
|
||||||
{/* Document display area */}
|
// {/* Document display area */}
|
||||||
<Box sx={{
|
// <Box sx={{
|
||||||
display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
|
// display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
|
||||||
overflow: "hidden"
|
// overflow: "hidden"
|
||||||
}}>
|
// }}>
|
||||||
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
|
// <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 === 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 sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(/*{ height: "calc(100% - 72px - 48px)" }*/)}</Box>
|
||||||
</Box>
|
// </Box>
|
||||||
</Box>
|
// </Box>
|
||||||
);
|
// );
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
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
|
* This demonstrates how to use the generated types with the conversion utilities
|
||||||
* for seamless frontend-backend communication.
|
* for seamless frontend-backend communication, including streaming responses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import generated types (from running generate_types.py)
|
// Import generated types (from running generate_types.py)
|
||||||
@ -21,6 +21,40 @@ import {
|
|||||||
PaginatedRequest
|
PaginatedRequest
|
||||||
} from './conversion';
|
} from './conversion';
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Streaming Types and Interfaces
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
interface StreamingOptions {
|
||||||
|
onMessage?: (message: Types.ChatMessage) => void;
|
||||||
|
onPartialMessage?: (partialContent: string, messageId?: string) => void;
|
||||||
|
onComplete?: (finalMessage: Types.ChatMessage) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
onStatusChange?: (status: Types.ChatStatusType) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamingResponse {
|
||||||
|
messageId: string;
|
||||||
|
cancel: () => void;
|
||||||
|
promise: Promise<Types.ChatMessage>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessageChunk {
|
||||||
|
id?: string;
|
||||||
|
sessionId: string;
|
||||||
|
status: Types.ChatStatusType;
|
||||||
|
sender: Types.ChatSenderType;
|
||||||
|
content: string;
|
||||||
|
isPartial?: boolean;
|
||||||
|
timestamp: Date;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Enhanced API Client Class
|
||||||
|
// ============================
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private defaultHeaders: Record<string, string>;
|
private defaultHeaders: Record<string, string>;
|
||||||
@ -257,7 +291,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Chat Methods
|
// Chat Methods (Enhanced with Streaming)
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> {
|
async createChatSession(context: Types.ChatContext): Promise<Types.ChatSession> {
|
||||||
@ -278,16 +312,186 @@ class ApiClient {
|
|||||||
return handleApiResponse<Types.ChatSession>(response);
|
return handleApiResponse<Types.ChatSession>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(sessionId: string, content: string): Promise<Types.ChatMessage> {
|
/**
|
||||||
|
* Send message with standard response (non-streaming)
|
||||||
|
*/
|
||||||
|
async sendMessage(sessionId: string, query: Types.Query): Promise<Types.ChatMessage> {
|
||||||
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages`, {
|
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.defaultHeaders,
|
headers: this.defaultHeaders,
|
||||||
body: JSON.stringify(formatApiRequest({ content }))
|
body: JSON.stringify(formatApiRequest({ query }))
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleApiResponse<Types.ChatMessage>(response);
|
return handleApiResponse<Types.ChatMessage>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message with streaming response support
|
||||||
|
*/
|
||||||
|
sendMessageStream(
|
||||||
|
sessionId: string,
|
||||||
|
query: Types.Query,
|
||||||
|
options: StreamingOptions = {}
|
||||||
|
): StreamingResponse {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const signal = options.signal || abortController.signal;
|
||||||
|
|
||||||
|
let messageId = '';
|
||||||
|
let accumulatedContent = '';
|
||||||
|
let currentMessage: Partial<Types.ChatMessage> = {};
|
||||||
|
|
||||||
|
const promise = new Promise<Types.ChatMessage>(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages/stream`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.defaultHeaders,
|
||||||
|
'Accept': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formatApiRequest({ query })),
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('Response body is not readable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === '') continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle Server-Sent Events format
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
|
||||||
|
if (data === '[DONE]') {
|
||||||
|
// Stream completed
|
||||||
|
const finalMessage: Types.ChatMessage = {
|
||||||
|
id: messageId,
|
||||||
|
sessionId,
|
||||||
|
status: 'done',
|
||||||
|
sender: currentMessage.sender || 'ai',
|
||||||
|
content: accumulatedContent,
|
||||||
|
timestamp: currentMessage.timestamp || new Date(),
|
||||||
|
...currentMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
options.onComplete?.(finalMessage);
|
||||||
|
resolve(finalMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageChunk: ChatMessageChunk = JSON.parse(data);
|
||||||
|
|
||||||
|
// Update accumulated state
|
||||||
|
if (messageChunk.id) messageId = messageChunk.id;
|
||||||
|
if (messageChunk.content) {
|
||||||
|
accumulatedContent += messageChunk.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current message properties
|
||||||
|
Object.assign(currentMessage, {
|
||||||
|
...messageChunk,
|
||||||
|
content: accumulatedContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger callbacks
|
||||||
|
if (messageChunk.status) {
|
||||||
|
options.onStatusChange?.(messageChunk.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageChunk.isPartial) {
|
||||||
|
options.onPartialMessage?.(messageChunk.content, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentCompleteMessage: Types.ChatMessage = {
|
||||||
|
id: messageId,
|
||||||
|
sessionId,
|
||||||
|
status: messageChunk.status,
|
||||||
|
sender: messageChunk.sender,
|
||||||
|
content: accumulatedContent,
|
||||||
|
timestamp: messageChunk.timestamp,
|
||||||
|
...currentMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
options.onMessage?.(currentCompleteMessage);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('Failed to parse SSE chunk:', parseError);
|
||||||
|
// Continue processing other lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here without a [DONE] signal, create final message
|
||||||
|
const finalMessage: Types.ChatMessage = {
|
||||||
|
id: messageId || `msg_${Date.now()}`,
|
||||||
|
sessionId,
|
||||||
|
status: 'done',
|
||||||
|
sender: currentMessage.sender || 'ai',
|
||||||
|
content: accumulatedContent,
|
||||||
|
timestamp: currentMessage.timestamp || new Date(),
|
||||||
|
...currentMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
options.onComplete?.(finalMessage);
|
||||||
|
resolve(finalMessage);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
reject(new Error('Request was aborted'));
|
||||||
|
} else {
|
||||||
|
options.onError?.(error as Error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId,
|
||||||
|
cancel: () => abortController.abort(),
|
||||||
|
promise
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message with automatic streaming detection
|
||||||
|
*/
|
||||||
|
async sendMessageAuto(
|
||||||
|
sessionId: string,
|
||||||
|
query: Types.Query,
|
||||||
|
options?: StreamingOptions
|
||||||
|
): Promise<Types.ChatMessage> {
|
||||||
|
// If streaming options are provided, use streaming
|
||||||
|
if (options && (options.onMessage || options.onPartialMessage || options.onStatusChange)) {
|
||||||
|
const streamResponse = this.sendMessageStream(sessionId, query, options);
|
||||||
|
return streamResponse.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use standard response
|
||||||
|
return this.sendMessage(sessionId, query);
|
||||||
|
}
|
||||||
|
|
||||||
async getChatMessages(sessionId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.ChatMessage>> {
|
async getChatMessages(sessionId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.ChatMessage>> {
|
||||||
const paginatedRequest = createPaginatedRequest(request);
|
const paginatedRequest = createPaginatedRequest(request);
|
||||||
const params = toUrlParams(formatApiRequest(paginatedRequest));
|
const params = toUrlParams(formatApiRequest(paginatedRequest));
|
||||||
@ -333,16 +537,11 @@ class ApiClient {
|
|||||||
// Error Handling Helper
|
// Error Handling Helper
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
// ============================
|
|
||||||
// Error Handling Helper
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> {
|
async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const response = await requestFn();
|
const response = await requestFn();
|
||||||
return await handleApiResponse<T>(response);
|
return await handleApiResponse<T>(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error for debugging
|
|
||||||
console.error('API request failed:', error);
|
console.error('API request failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -352,28 +551,153 @@ class ApiClient {
|
|||||||
// Utility Methods
|
// Utility Methods
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
/**
|
|
||||||
* Update authorization token for future requests
|
|
||||||
*/
|
|
||||||
setAuthToken(token: string): void {
|
setAuthToken(token: string): void {
|
||||||
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
|
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove authorization token
|
|
||||||
*/
|
|
||||||
clearAuthToken(): void {
|
clearAuthToken(): void {
|
||||||
delete this.defaultHeaders['Authorization'];
|
delete this.defaultHeaders['Authorization'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current base URL
|
|
||||||
*/
|
|
||||||
getBaseUrl(): string {
|
getBaseUrl(): string {
|
||||||
return this.baseUrl;
|
return this.baseUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// React Hooks for Streaming
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
/* React Hook Examples for Streaming Chat
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
export function useStreamingChat(sessionId: string) {
|
||||||
|
const [messages, setMessages] = useState<Types.ChatMessage[]>([]);
|
||||||
|
const [currentMessage, setCurrentMessage] = useState<Types.ChatMessage | null>(null);
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const apiClient = useApiClient();
|
||||||
|
const streamingRef = useRef<StreamingResponse | null>(null);
|
||||||
|
|
||||||
|
const sendMessage = useCallback(async (query: Types.Query) => {
|
||||||
|
setError(null);
|
||||||
|
setIsStreaming(true);
|
||||||
|
setCurrentMessage(null);
|
||||||
|
|
||||||
|
const streamingOptions: StreamingOptions = {
|
||||||
|
onMessage: (message) => {
|
||||||
|
setCurrentMessage(message);
|
||||||
|
},
|
||||||
|
onPartialMessage: (content, messageId) => {
|
||||||
|
setCurrentMessage(prev => prev ?
|
||||||
|
{ ...prev, content: prev.content + content } :
|
||||||
|
{
|
||||||
|
id: messageId || '',
|
||||||
|
sessionId,
|
||||||
|
status: 'streaming',
|
||||||
|
sender: 'ai',
|
||||||
|
content,
|
||||||
|
timestamp: new Date()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onStatusChange: (status) => {
|
||||||
|
setCurrentMessage(prev => prev ? { ...prev, status } : null);
|
||||||
|
},
|
||||||
|
onComplete: (finalMessage) => {
|
||||||
|
setMessages(prev => [...prev, finalMessage]);
|
||||||
|
setCurrentMessage(null);
|
||||||
|
setIsStreaming(false);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err.message);
|
||||||
|
setIsStreaming(false);
|
||||||
|
setCurrentMessage(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
streamingRef.current = apiClient.sendMessageStream(sessionId, query, streamingOptions);
|
||||||
|
await streamingRef.current.promise;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to send message');
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
}, [sessionId, apiClient]);
|
||||||
|
|
||||||
|
const cancelStreaming = useCallback(() => {
|
||||||
|
if (streamingRef.current) {
|
||||||
|
streamingRef.current.cancel();
|
||||||
|
setIsStreaming(false);
|
||||||
|
setCurrentMessage(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
currentMessage,
|
||||||
|
isStreaming,
|
||||||
|
error,
|
||||||
|
sendMessage,
|
||||||
|
cancelStreaming
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in React component:
|
||||||
|
function ChatInterface({ sessionId }: { sessionId: string }) {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
currentMessage,
|
||||||
|
isStreaming,
|
||||||
|
error,
|
||||||
|
sendMessage,
|
||||||
|
cancelStreaming
|
||||||
|
} = useStreamingChat(sessionId);
|
||||||
|
|
||||||
|
const handleSendMessage = (text: string) => {
|
||||||
|
sendMessage(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="messages">
|
||||||
|
{messages.map(message => (
|
||||||
|
<div key={message.id}>
|
||||||
|
<strong>{message.sender}:</strong> {message.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentMessage && (
|
||||||
|
<div className="current-message">
|
||||||
|
<strong>{currentMessage.sender}:</strong> {currentMessage.content}
|
||||||
|
{isStreaming && <span className="streaming-indicator">...</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="input-area">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSendMessage(e.currentTarget.value);
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
{isStreaming && (
|
||||||
|
<button onClick={cancelStreaming}>Cancel</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Usage Examples
|
// Usage Examples
|
||||||
// ============================
|
// ============================
|
||||||
@ -382,193 +706,54 @@ class ApiClient {
|
|||||||
// Initialize API client
|
// Initialize API client
|
||||||
const apiClient = new ApiClient();
|
const apiClient = new ApiClient();
|
||||||
|
|
||||||
// Login and set auth token
|
// Standard message sending (non-streaming)
|
||||||
try {
|
try {
|
||||||
const authResponse = await apiClient.login('user@example.com', 'password');
|
const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?');
|
||||||
apiClient.setAuthToken(authResponse.accessToken);
|
console.log('Response:', message.content);
|
||||||
console.log('Logged in as:', authResponse.user);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error);
|
console.error('Failed to send message:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new candidate
|
// Streaming message with callbacks
|
||||||
|
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', {
|
||||||
|
onPartialMessage: (content, messageId) => {
|
||||||
|
console.log('Partial content:', content);
|
||||||
|
// Update UI with partial content
|
||||||
|
},
|
||||||
|
onStatusChange: (status) => {
|
||||||
|
console.log('Status changed:', status);
|
||||||
|
// Update UI status indicator
|
||||||
|
},
|
||||||
|
onComplete: (finalMessage) => {
|
||||||
|
console.log('Final message:', finalMessage.content);
|
||||||
|
// Handle completed message
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Streaming error:', error);
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can cancel the stream if needed
|
||||||
|
setTimeout(() => {
|
||||||
|
streamResponse.cancel();
|
||||||
|
}, 10000); // Cancel after 10 seconds
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
try {
|
try {
|
||||||
const newCandidate = await apiClient.createCandidate({
|
const finalMessage = await streamResponse.promise;
|
||||||
email: 'candidate@example.com',
|
console.log('Stream completed:', finalMessage);
|
||||||
status: 'active',
|
|
||||||
firstName: 'John',
|
|
||||||
lastName: 'Doe',
|
|
||||||
skills: [],
|
|
||||||
experience: [],
|
|
||||||
education: [],
|
|
||||||
preferredJobTypes: ['full-time'],
|
|
||||||
location: {
|
|
||||||
city: 'San Francisco',
|
|
||||||
country: 'USA'
|
|
||||||
},
|
|
||||||
languages: [],
|
|
||||||
certifications: []
|
|
||||||
});
|
|
||||||
console.log('Created candidate:', newCandidate);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create candidate:', error);
|
console.error('Stream failed:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for jobs
|
// Auto-detection: streaming if callbacks provided, standard otherwise
|
||||||
try {
|
await apiClient.sendMessageAuto(sessionId, 'Quick question', {
|
||||||
const jobResults = await apiClient.searchJobs('software engineer', {
|
onPartialMessage: (content) => console.log('Streaming:', content)
|
||||||
location: 'San Francisco',
|
}); // Will use streaming
|
||||||
experienceLevel: 'senior'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Found ${jobResults.total} jobs:`);
|
|
||||||
jobResults.data.forEach(job => {
|
|
||||||
console.log(`- ${job.title} at ${job.location.city}`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Job search failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get paginated candidates
|
await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard
|
||||||
try {
|
|
||||||
const candidates = await apiClient.getCandidates({
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
sortBy: 'createdAt',
|
|
||||||
sortOrder: 'desc',
|
|
||||||
filters: {
|
|
||||||
status: 'active',
|
|
||||||
skills: ['javascript', 'react']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Page ${candidates.page} of ${candidates.totalPages}`);
|
|
||||||
console.log(`${candidates.data.length} candidates on this page`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get candidates:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a chat session
|
|
||||||
try {
|
|
||||||
const chatSession = await apiClient.createChatSession({
|
|
||||||
type: 'job_search',
|
|
||||||
aiParameters: {
|
|
||||||
name: 'Job Search Assistant',
|
|
||||||
model: 'gpt-4',
|
|
||||||
temperature: 0.7,
|
|
||||||
maxTokens: 2000,
|
|
||||||
topP: 0.95,
|
|
||||||
frequencyPenalty: 0.0,
|
|
||||||
presencePenalty: 0.0,
|
|
||||||
isDefault: false,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send a message
|
|
||||||
const message = await apiClient.sendMessage(
|
|
||||||
chatSession.id,
|
|
||||||
'Help me find software engineering jobs in San Francisco'
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('AI Response:', message.content);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Chat session failed:', error);
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============================
|
export { ApiClient }
|
||||||
// React Hook Examples
|
export type { StreamingOptions, StreamingResponse, ChatMessageChunk };
|
||||||
// ============================
|
|
||||||
|
|
||||||
/*
|
|
||||||
// Custom hooks for React applications
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export function useApiClient() {
|
|
||||||
const [client] = useState(() => new ApiClient(process.env.REACT_APP_API_URL || ''));
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCandidates(request?: Partial<PaginatedRequest>) {
|
|
||||||
const [data, setData] = useState<PaginatedResponse<Types.Candidate> | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const apiClient = useApiClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchCandidates() {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
const result = await apiClient.getCandidates(request);
|
|
||||||
setData(result);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch candidates');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchCandidates();
|
|
||||||
}, [request]);
|
|
||||||
|
|
||||||
return { data, loading, error, refetch: () => fetchCandidates() };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useJobs(request?: Partial<PaginatedRequest>) {
|
|
||||||
const [data, setData] = useState<PaginatedResponse<Types.Job> | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const apiClient = useApiClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchJobs() {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
const result = await apiClient.getJobs(request);
|
|
||||||
setData(result);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch jobs');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchJobs();
|
|
||||||
}, [request]);
|
|
||||||
|
|
||||||
return { data, loading, error, refetch: () => fetchJobs() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in React component:
|
|
||||||
function CandidateList() {
|
|
||||||
const { data: candidates, loading, error } = useCandidates({
|
|
||||||
limit: 10,
|
|
||||||
sortBy: 'createdAt'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) return <div>Loading candidates...</div>;
|
|
||||||
if (error) return <div>Error: {error}</div>;
|
|
||||||
if (!candidates) return <div>No candidates found</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Candidates ({candidates.total})</h2>
|
|
||||||
{candidates.data.map(candidate => (
|
|
||||||
<div key={candidate.id}>
|
|
||||||
{candidate.firstName} {candidate.lastName} - {candidate.email}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{candidates.hasMore && (
|
|
||||||
<button>Load More</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { ApiClient };
|
|
@ -1,6 +1,6 @@
|
|||||||
// Generated TypeScript types from Pydantic models
|
// Generated TypeScript types from Pydantic models
|
||||||
// Source: src/models.py
|
// Source: src/backend/models.py
|
||||||
// Generated on: 2025-05-28T20:34:39.642452
|
// Generated on: 2025-05-28T21:47:08.590102
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -17,6 +17,8 @@ export type ChatContextType = "job_search" | "candidate_screening" | "interview_
|
|||||||
|
|
||||||
export type ChatSenderType = "user" | "ai" | "system";
|
export type ChatSenderType = "user" | "ai" | "system";
|
||||||
|
|
||||||
|
export type ChatStatusType = "partial" | "done" | "streaming" | "thinking" | "error";
|
||||||
|
|
||||||
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none";
|
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none";
|
||||||
|
|
||||||
export type DataSourceType = "document" | "website" | "api" | "database" | "internal";
|
export type DataSourceType = "document" | "website" | "api" | "database" | "internal";
|
||||||
@ -127,7 +129,7 @@ export interface Attachment {
|
|||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
user: BaseUser;
|
user: any;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +150,6 @@ export interface Authentication {
|
|||||||
|
|
||||||
export interface BaseUser {
|
export interface BaseUser {
|
||||||
id?: string;
|
id?: string;
|
||||||
username: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@ -160,7 +161,6 @@ export interface BaseUser {
|
|||||||
|
|
||||||
export interface BaseUserWithType {
|
export interface BaseUserWithType {
|
||||||
id?: string;
|
id?: string;
|
||||||
username: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@ -173,7 +173,6 @@ export interface BaseUserWithType {
|
|||||||
|
|
||||||
export interface Candidate {
|
export interface Candidate {
|
||||||
id?: string;
|
id?: string;
|
||||||
username: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@ -182,6 +181,7 @@ export interface Candidate {
|
|||||||
profileImage?: string;
|
profileImage?: string;
|
||||||
status: "active" | "inactive" | "pending" | "banned";
|
status: "active" | "inactive" | "pending" | "banned";
|
||||||
userType?: "candidate";
|
userType?: "candidate";
|
||||||
|
username: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
@ -250,6 +250,7 @@ export interface ChatContext {
|
|||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id?: string;
|
id?: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
status: "partial" | "done" | "streaming" | "thinking" | "error";
|
||||||
sender: "user" | "ai" | "system";
|
sender: "user" | "ai" | "system";
|
||||||
senderId?: string;
|
senderId?: string;
|
||||||
content: string;
|
content: string;
|
||||||
@ -320,7 +321,6 @@ export interface Education {
|
|||||||
|
|
||||||
export interface Employer {
|
export interface Employer {
|
||||||
id?: string;
|
id?: string;
|
||||||
username: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
@ -67,6 +67,13 @@ class ChatSenderType(str, Enum):
|
|||||||
AI = "ai"
|
AI = "ai"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
||||||
|
class ChatStatusType(str, Enum):
|
||||||
|
PARTIAL = "partial"
|
||||||
|
DONE = "done"
|
||||||
|
STREAMING = "streaming"
|
||||||
|
THINKING = "thinking"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
class ChatContextType(str, Enum):
|
class ChatContextType(str, Enum):
|
||||||
JOB_SEARCH = "job_search"
|
JOB_SEARCH = "job_search"
|
||||||
CANDIDATE_SCREENING = "candidate_screening"
|
CANDIDATE_SCREENING = "candidate_screening"
|
||||||
@ -544,6 +551,7 @@ class ChatContext(BaseModel):
|
|||||||
class ChatMessage(BaseModel):
|
class ChatMessage(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
session_id: str = Field(..., alias="sessionId")
|
session_id: str = Field(..., alias="sessionId")
|
||||||
|
status: ChatStatusType
|
||||||
sender: ChatSenderType
|
sender: ChatSenderType
|
||||||
sender_id: Optional[str] = Field(None, alias="senderId")
|
sender_id: Optional[str] = Field(None, alias="senderId")
|
||||||
content: str
|
content: str
|
||||||
|
Loading…
x
Reference in New Issue
Block a user