Compare commits
3 Commits
957ef07467
...
889a3f9e3b
Author | SHA1 | Date | |
---|---|---|---|
889a3f9e3b | |||
52ec9fdcf0 | |||
0173657477 |
@ -1,3 +1,7 @@
|
|||||||
|
.App {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@ -114,18 +118,6 @@ button {
|
|||||||
background-color: #D3CDBF;
|
background-color: #D3CDBF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Conversation {
|
|
||||||
display: flex;
|
|
||||||
background-color: #F5F5F5;
|
|
||||||
border: 1px solid #E0E0E0;
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 10px;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message.MuiCard-root {
|
.user-message.MuiCard-root {
|
||||||
background-color: #DCF8C6;
|
background-color: #DCF8C6;
|
||||||
border: 1px solid #B2E0A7;
|
border: 1px solid #B2E0A7;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
@ -23,8 +23,10 @@ import { Snack, SeverityType } from './Snack';
|
|||||||
import { VectorVisualizer } from './VectorVisualizer';
|
import { VectorVisualizer } from './VectorVisualizer';
|
||||||
import { Controls } from './Controls';
|
import { Controls } from './Controls';
|
||||||
import { Conversation, ConversationHandle } from './Conversation';
|
import { Conversation, ConversationHandle } from './Conversation';
|
||||||
|
import { Scrollable } from './AutoScroll';
|
||||||
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
import './Conversation.css';
|
||||||
|
|
||||||
import '@fontsource/roboto/300.css';
|
import '@fontsource/roboto/300.css';
|
||||||
import '@fontsource/roboto/400.css';
|
import '@fontsource/roboto/400.css';
|
||||||
@ -33,7 +35,6 @@ import '@fontsource/roboto/700.css';
|
|||||||
|
|
||||||
import MuiMarkdown from 'mui-markdown';
|
import MuiMarkdown from 'mui-markdown';
|
||||||
|
|
||||||
|
|
||||||
const getConnectionBase = (loc: any): string => {
|
const getConnectionBase = (loc: any): string => {
|
||||||
if (!loc.host.match(/.*battle-linux.*/)) {
|
if (!loc.host.match(/.*battle-linux.*/)) {
|
||||||
return loc.protocol + "//" + loc.host;
|
return loc.protocol + "//" + loc.host;
|
||||||
@ -61,7 +62,7 @@ function CustomTabPanel(props: TabPanelProps) {
|
|||||||
aria-labelledby={`tab-${index}`}
|
aria-labelledby={`tab-${index}`}
|
||||||
{...other}
|
{...other}
|
||||||
>
|
>
|
||||||
{tab === index && children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -69,9 +70,10 @@ function CustomTabPanel(props: TabPanelProps) {
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||||
const [connectionBase,] = useState<string>(getConnectionBase(window.location))
|
const [connectionBase,] = useState<string>(getConnectionBase(window.location))
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string>("");
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [isMenuClosing, setIsMenuClosing] = useState(false);
|
const [isMenuClosing, setIsMenuClosing] = useState(false);
|
||||||
const [tab, setTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [about, setAbout] = useState<string>("");
|
const [about, setAbout] = useState<string>("");
|
||||||
const isDesktop = useMediaQuery('(min-width:650px)');
|
const isDesktop = useMediaQuery('(min-width:650px)');
|
||||||
const prevIsDesktopRef = useRef<boolean>(isDesktop);
|
const prevIsDesktopRef = useRef<boolean>(isDesktop);
|
||||||
@ -126,46 +128,19 @@ const App = () => {
|
|||||||
const handleSubmitChatQuery = (query: string) => {
|
const handleSubmitChatQuery = (query: string) => {
|
||||||
console.log(`handleSubmitChatQuery: ${query} -- `, chatRef.current ? ' sending' : 'no handler');
|
console.log(`handleSubmitChatQuery: ${query} -- `, chatRef.current ? ' sending' : 'no handler');
|
||||||
chatRef.current?.submitQuery(query);
|
chatRef.current?.submitQuery(query);
|
||||||
|
setActiveTab(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatPreamble: MessageList = [
|
|
||||||
{
|
|
||||||
role: 'content',
|
|
||||||
title: 'Welcome to Backstory',
|
|
||||||
content: `
|
|
||||||
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
|
|
||||||
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
|
|
||||||
It was written by James Ketrenos in order to provide answers to
|
|
||||||
questions potential employers may have about his work history.
|
|
||||||
|
|
||||||
What would you like to know about James?
|
|
||||||
`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const chatQuestions = [
|
|
||||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
|
||||||
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} />
|
|
||||||
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} />
|
|
||||||
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} />
|
|
||||||
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} />
|
|
||||||
</Box>,
|
|
||||||
<Box sx={{ p: 1 }}>
|
|
||||||
<MuiMarkdown>
|
|
||||||
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
|
|
||||||
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
|
|
||||||
</MuiMarkdown>
|
|
||||||
</Box>
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
// Extract the sessionId from the URL if present, otherwise
|
// Extract the sessionId from the URL if present, otherwise
|
||||||
// request a sessionId from the server.
|
// request a sessionId from the server.
|
||||||
|
const validPaths = useMemo(() => ['chat', 'notes', 'tasks'], []); // allowed paths
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
|
||||||
|
|
||||||
const fetchSession = async () => {
|
const fetchSession = async (pathOverride?: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(connectionBase + `/api/context`, {
|
const response = await fetch(connectionBase + `/api/context`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -179,20 +154,31 @@ What would you like to know about James?
|
|||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setSessionId(data.id);
|
setSessionId(data.id);
|
||||||
window.history.replaceState({}, '', `/${data.id}`);
|
|
||||||
|
const newPath = pathOverride || 'chat'; // default fallback
|
||||||
|
window.history.replaceState({}, '', `/${newPath}/${data.id}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSnack("Server is temporarily down", "error");
|
setSnack("Server is temporarily down", "error");
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!pathParts.length) {
|
if (pathParts.length < 2) {
|
||||||
console.log("No session id -- creating a new session")
|
console.log("No session id or path -- creating new session");
|
||||||
fetchSession();
|
fetchSession();
|
||||||
} else {
|
} else {
|
||||||
console.log(`Session id: ${pathParts[0]} -- existing session`)
|
const currentPath = pathParts[0];
|
||||||
setSessionId(pathParts[0]);
|
const session = pathParts[1];
|
||||||
|
|
||||||
|
if (!validPaths.includes(currentPath)) {
|
||||||
|
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
|
||||||
|
fetchSession(); // or you could window.location.replace if you want
|
||||||
|
} else {
|
||||||
|
console.log(`Path: ${currentPath}, Session id: ${session}`);
|
||||||
|
setSessionId(session);
|
||||||
|
setSelectedPath(currentPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [setSessionId, connectionBase, setSnack]);
|
}, [setSessionId, setSelectedPath, connectionBase, setSnack, validPaths]);
|
||||||
|
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setIsMenuClosing(true);
|
setIsMenuClosing(true);
|
||||||
@ -209,24 +195,42 @@ What would you like to know about James?
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const settingsPanel = (
|
|
||||||
<>
|
|
||||||
{sessionId !== undefined &&
|
|
||||||
<Controls {...{ sessionId, setSnack, connectionBase }} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
setTab(newValue);
|
setActiveTab(newValue);
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTabSelect = (newPath: string) => {
|
||||||
|
if (!sessionId) return; // safety
|
||||||
|
setSelectedPath(newPath);
|
||||||
|
window.history.pushState({}, '', `/${newPath}/${sessionId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
if (pathParts.length >= 2) {
|
||||||
|
const path = pathParts[0];
|
||||||
|
const session = pathParts[1];
|
||||||
|
|
||||||
|
if (validPaths.includes(path)) {
|
||||||
|
setSelectedPath(path);
|
||||||
|
setSessionId(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => window.removeEventListener('popstate', handlePopState);
|
||||||
|
}, [setSelectedPath, setSessionId, validPaths]);
|
||||||
|
|
||||||
const menuDrawer = (
|
const menuDrawer = (
|
||||||
<Card className="MenuCard">
|
<Card className="MenuCard">
|
||||||
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
value={tab}
|
value={activeTab}
|
||||||
indicatorColor="secondary"
|
indicatorColor="secondary"
|
||||||
textColor="inherit"
|
textColor="inherit"
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
@ -265,11 +269,93 @@ What would you like to know about James?
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
/* toolbar height is 56px + 8px margin-top */
|
const tabs = useMemo(() => {
|
||||||
const Offset = styled('div')(({ theme }) => ({ ...theme.mixins.toolbar, minHeight: '64px', height: '64px' }));
|
const chatPreamble: MessageList = [
|
||||||
|
{
|
||||||
|
role: 'content',
|
||||||
|
title: 'Welcome to Backstory',
|
||||||
|
content: `
|
||||||
|
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
|
||||||
|
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
|
||||||
|
It was written by James Ketrenos in order to provide answers to
|
||||||
|
questions potential employers may have about his work history.
|
||||||
|
|
||||||
|
What would you like to know about James?
|
||||||
|
`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const chatQuestions = [
|
||||||
|
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||||
|
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} />
|
||||||
|
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} />
|
||||||
|
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} />
|
||||||
|
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} />
|
||||||
|
</Box>,
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<MuiMarkdown>
|
||||||
|
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
|
||||||
|
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
|
||||||
|
</MuiMarkdown>
|
||||||
|
</Box>
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
maxWidth: "1024px",
|
||||||
|
height: "calc(100vh - 72px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Conversation
|
||||||
|
ref={chatRef}
|
||||||
|
{...{
|
||||||
|
type: "chat",
|
||||||
|
prompt: "What would you like to know about James?",
|
||||||
|
sessionId,
|
||||||
|
connectionBase,
|
||||||
|
setSnack,
|
||||||
|
preamble: chatPreamble,
|
||||||
|
defaultPrompts: chatQuestions
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Scrollable>,
|
||||||
|
<ResumeBuilder sx={{
|
||||||
|
margin: "0 auto",
|
||||||
|
height: "calc(100vh - 72px)",
|
||||||
|
overflow: "auto",
|
||||||
|
backgroundColor: "#F5F5F5",
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1
|
||||||
|
}} {...{ setSnack, connectionBase, sessionId }} />,
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
maxWidth: "1024px",
|
||||||
|
height: "calc(100vh - 72px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VectorVisualizer {...{ connectionBase, sessionId, setSnack }} />
|
||||||
|
</Scrollable>,
|
||||||
|
<Box className="ChatBox">
|
||||||
|
<Box className="Conversation">
|
||||||
|
<Message {...{ message: { role: 'content', content: about }, submitQuery: handleSubmitChatQuery, connectionBase, sessionId, setSnack }} />
|
||||||
|
</Box>
|
||||||
|
</Box>,
|
||||||
|
<Box className="ChatBox">
|
||||||
|
{sessionId !== undefined &&
|
||||||
|
<Controls {...{ sessionId, setSnack, connectionBase }} />
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
];
|
||||||
|
}, [about, connectionBase, sessionId, setSnack, isMobile]);
|
||||||
|
|
||||||
|
|
||||||
|
/* toolbar height is 64px + 8px margin-top */
|
||||||
|
const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="App" sx={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}>
|
<Box className="App"
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
@ -296,7 +382,7 @@ What would you like to know about James?
|
|||||||
<Tooltip title="Backstory">
|
<Tooltip title="Backstory">
|
||||||
<Box
|
<Box
|
||||||
sx={{ m: 1, gap: 1, display: "flex", flexDirection: "row", alignItems: "center", fontWeight: "bold", fontSize: "1.0rem", cursor: "pointer" }}
|
sx={{ m: 1, gap: 1, display: "flex", flexDirection: "row", alignItems: "center", fontWeight: "bold", fontSize: "1.0rem", cursor: "pointer" }}
|
||||||
onClick={() => { setTab(0); setMenuOpen(false); }}
|
onClick={() => { setActiveTab(0); setMenuOpen(false); }}
|
||||||
>
|
>
|
||||||
<Avatar sx={{
|
<Avatar sx={{
|
||||||
width: 24,
|
width: 24,
|
||||||
@ -313,7 +399,7 @@ What would you like to know about James?
|
|||||||
|
|
||||||
{menuOpen === false && isDesktop &&
|
{menuOpen === false && isDesktop &&
|
||||||
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
||||||
value={tab}
|
value={activeTab}
|
||||||
indicatorColor="secondary"
|
indicatorColor="secondary"
|
||||||
textColor="inherit"
|
textColor="inherit"
|
||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
@ -357,7 +443,9 @@ What would you like to know about James?
|
|||||||
|
|
||||||
<Offset />
|
<Offset />
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }}>
|
<Box
|
||||||
|
sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
component="nav"
|
component="nav"
|
||||||
aria-label="mailbox folders"
|
aria-label="mailbox folders"
|
||||||
@ -382,52 +470,11 @@ What would you like to know about James?
|
|||||||
{menuDrawer}
|
{menuDrawer}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</Box>
|
</Box>
|
||||||
|
{
|
||||||
<CustomTabPanel tab={tab} index={0}>
|
tabs.map((tab: any, i: number) =>
|
||||||
<Box component="main" sx={{ flexGrow: 1, overflow: 'auto' }} className="ChatBox">
|
<CustomTabPanel key={i} tab={activeTab} index={i}>{tab}</CustomTabPanel>
|
||||||
<Conversation
|
)
|
||||||
ref={chatRef}
|
}
|
||||||
{...{
|
|
||||||
type: "chat",
|
|
||||||
prompt: "What would you like to know about James?",
|
|
||||||
sessionId,
|
|
||||||
connectionBase,
|
|
||||||
setSnack,
|
|
||||||
preamble: chatPreamble,
|
|
||||||
defaultPrompts: chatQuestions
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</CustomTabPanel>
|
|
||||||
|
|
||||||
<CustomTabPanel tab={tab} index={1}>
|
|
||||||
<ResumeBuilder {...{ setSnack, connectionBase, sessionId }} />
|
|
||||||
</CustomTabPanel>
|
|
||||||
|
|
||||||
<CustomTabPanel tab={tab} index={2}>
|
|
||||||
<Box className="ChatBox">
|
|
||||||
<Box className="Conversation">
|
|
||||||
<VectorVisualizer {...{ connectionBase, sessionId, setSnack }} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</CustomTabPanel>
|
|
||||||
|
|
||||||
<CustomTabPanel tab={tab} index={3}>
|
|
||||||
<Box className="ChatBox">
|
|
||||||
<Box className="Conversation">
|
|
||||||
<Message {...{ message: { role: 'assistant', content: about }, submitQuery: handleSubmitChatQuery, connectionBase, sessionId, setSnack }} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</CustomTabPanel>
|
|
||||||
|
|
||||||
<CustomTabPanel tab={tab} index={4}>
|
|
||||||
<Box className="ChatBox">
|
|
||||||
<Box className="Conversation">
|
|
||||||
{ settingsPanel }
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</CustomTabPanel>
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Snack
|
<Snack
|
||||||
|
154
frontend/src/AutoScroll.tsx
Normal file
154
frontend/src/AutoScroll.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { useEffect, useRef, useState, RefObject } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that automatically scrolls a container to the bottom when content changes
|
||||||
|
* or when the container is resized, but only if the user is already near the bottom.
|
||||||
|
*
|
||||||
|
* @param threshold - Distance from bottom (px) to consider "near bottom" (default: 100)
|
||||||
|
* @param smooth - Whether to use smooth scrolling (default: true)
|
||||||
|
* @returns Ref to attach to the scrollable container
|
||||||
|
*/
|
||||||
|
const useAutoScrollToBottom = (
|
||||||
|
threshold: number = 0.33, // Percentage of viewport to trigger threshold
|
||||||
|
smooth: boolean = true
|
||||||
|
): RefObject<HTMLDivElement> => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lastScrollTop = useRef<number>(0);
|
||||||
|
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
// console.log("No ref");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastScrollHeight = container.scrollHeight;
|
||||||
|
var isUserScrollingUp = false;
|
||||||
|
|
||||||
|
// Function to check if we should scroll to bottom
|
||||||
|
const checkAndScrollToBottom = (priorScrollHeight?: number | undefined): void => {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const scrollHeight = (priorScrollHeight !== undefined) ? priorScrollHeight : container.scrollHeight;
|
||||||
|
|
||||||
|
// Only auto-scroll if the user is near the bottom and not actively scrolling up
|
||||||
|
const isNearBottom: boolean =
|
||||||
|
scrollHeight - container.scrollTop - container.clientHeight <= container.clientHeight * threshold;
|
||||||
|
|
||||||
|
if (isNearBottom && !isUserScrollingUp) {
|
||||||
|
// console.log('Scrolling', {
|
||||||
|
// isNearBottom,
|
||||||
|
// isUserScrollingUp,
|
||||||
|
// scrollHeightToUser: scrollHeight,
|
||||||
|
// scrollHeight: container.scrollHeight,
|
||||||
|
// scrollTop: container.scrollTop,
|
||||||
|
// clientHeight: container.clientHeight,
|
||||||
|
// threshold,
|
||||||
|
// delta: container.scrollHeight - container.scrollTop - container.clientHeight
|
||||||
|
// });
|
||||||
|
container.scrollTo({
|
||||||
|
top: container.scrollHeight,
|
||||||
|
behavior: smooth ? 'smooth' : 'auto'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// console.log('Not scrolling', {
|
||||||
|
// isNearBottom,
|
||||||
|
// isUserScrollingUp,
|
||||||
|
// scrollHeight: container.scrollHeight,
|
||||||
|
// scrollTop: container.scrollTop,
|
||||||
|
// clientHeight: container.clientHeight,
|
||||||
|
// threshold,
|
||||||
|
// delta: container.scrollHeight - container.scrollTop - container.clientHeight
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up ResizeObserver to detect content size changes
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAndScrollToBottom(lastScrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe the container and its children
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
Array.from(container.children).forEach((child) => {
|
||||||
|
resizeObserver.observe(child);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track user scrolling behavior
|
||||||
|
const handleScroll = (): void => {
|
||||||
|
if (!container) {
|
||||||
|
// console.log("No ref in handleScroll");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing timeout
|
||||||
|
if (scrollTimeout.current) {
|
||||||
|
clearTimeout(scrollTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine scroll direction
|
||||||
|
const currentScrollTop = container.scrollTop;
|
||||||
|
isUserScrollingUp = currentScrollTop < lastScrollTop.current;
|
||||||
|
lastScrollTop.current = currentScrollTop;
|
||||||
|
|
||||||
|
// Reset the scrolling flag after user stops scrolling
|
||||||
|
scrollTimeout.current = setTimeout(() => {
|
||||||
|
//setIsUserScrollingUp(false);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add scroll event listener
|
||||||
|
container.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
// Run initial check
|
||||||
|
checkAndScrollToBottom();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
if (container) {
|
||||||
|
container.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
if (scrollTimeout.current) {
|
||||||
|
clearTimeout(scrollTimeout.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [smooth, threshold]); // Re-run when dependencies change or scrolling state changes
|
||||||
|
|
||||||
|
return containerRef as RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ScrollableProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
sx?: SxProps<Theme>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Scrollable = ({ sx, children }: ScrollableProps) => {
|
||||||
|
const scrollRef = useAutoScrollToBottom();
|
||||||
|
return <Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
margin: "0 auto",
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
backgroundColor: "#F5F5F5",
|
||||||
|
...sx
|
||||||
|
}}
|
||||||
|
ref={scrollRef}
|
||||||
|
>{children}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useAutoScrollToBottom,
|
||||||
|
Scrollable,
|
||||||
|
};
|
||||||
|
|
@ -79,7 +79,7 @@ function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatB
|
|||||||
...defaultStyle,
|
...defaultStyle,
|
||||||
backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal
|
backgroundColor: 'rgba(74, 122, 125, 0.15)', // Translucent dusty teal
|
||||||
border: `1px solid ${theme.palette.secondary.light}`, // Lighter dusty teal
|
border: `1px solid ${theme.palette.secondary.light}`, // Lighter dusty teal
|
||||||
borderRadius: defaultRadius,
|
borderRadius: '4px',
|
||||||
maxWidth: isFullWidth ? '100%' : '75%',
|
maxWidth: isFullWidth ? '100%' : '75%',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
color: theme.palette.secondary.dark, // Darker dusty teal for text
|
color: theme.palette.secondary.dark, // Darker dusty teal for text
|
||||||
|
13
frontend/src/Conversation.css
Normal file
13
frontend/src/Conversation.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
.Conversation {
|
||||||
|
display: flex;
|
||||||
|
background-color: #F5F5F5;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
@ -13,6 +13,9 @@ import PropagateLoader from "react-spinners/PropagateLoader";
|
|||||||
import { Message, MessageList, MessageData } from './Message';
|
import { Message, MessageList, MessageData } from './Message';
|
||||||
import { SetSnackType } from './Snack';
|
import { SetSnackType } from './Snack';
|
||||||
import { ContextStatus } from './ContextStatus';
|
import { ContextStatus } from './ContextStatus';
|
||||||
|
import { useAutoScrollToBottom } from './AutoScroll';
|
||||||
|
|
||||||
|
import './Conversation.css';
|
||||||
|
|
||||||
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." };
|
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." };
|
||||||
|
|
||||||
@ -79,6 +82,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||||
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||||
const conversationRef = useRef<MessageList>([]);
|
const conversationRef = useRef<MessageList>([]);
|
||||||
|
const scrollRef = useAutoScrollToBottom();
|
||||||
|
|
||||||
// Keep the ref updated whenever items changes
|
// Keep the ref updated whenever items changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -182,27 +186,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
fetchHistory();
|
fetchHistory();
|
||||||
}, [setConversation, setFilteredConversation, updateContextStatus, connectionBase, setSnack, type, sessionId]);
|
}, [setConversation, setFilteredConversation, updateContextStatus, connectionBase, setSnack, type, sessionId]);
|
||||||
|
|
||||||
const isScrolledToBottom = useCallback(()=> {
|
|
||||||
// Current vertical scroll position
|
|
||||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
||||||
|
|
||||||
// Total height of the page content
|
|
||||||
const scrollHeight = document.documentElement.scrollHeight;
|
|
||||||
|
|
||||||
// Height of the visible window
|
|
||||||
const clientHeight = document.documentElement.clientHeight;
|
|
||||||
|
|
||||||
// If we're at the bottom (allowing a small buffer of 16px)
|
|
||||||
return scrollTop + clientHeight >= scrollHeight - 16;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
|
||||||
console.log("Scroll to bottom");
|
|
||||||
window.scrollTo({
|
|
||||||
top: document.body.scrollHeight,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const startCountdown = (seconds: number) => {
|
const startCountdown = (seconds: number) => {
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
setCountdown(seconds);
|
setCountdown(seconds);
|
||||||
@ -211,11 +194,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
if (prev <= 1) {
|
if (prev <= 1) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
if (isScrolledToBottom()) {
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return prev - 1;
|
return prev - 1;
|
||||||
@ -317,14 +295,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
console.log(conversation);
|
console.log(conversation);
|
||||||
|
|
||||||
let scrolledToBottom;
|
|
||||||
|
|
||||||
scrollToBottom();
|
|
||||||
// Clear input
|
// Clear input
|
||||||
setQuery('');
|
setQuery('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
// Create a unique ID for the processing message
|
// Create a unique ID for the processing message
|
||||||
const processingId = Date.now().toString();
|
const processingId = Date.now().toString();
|
||||||
@ -337,10 +311,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
// Add a small delay to ensure React has time to update the UI
|
// Add a small delay to ensure React has time to update the UI
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the fetch request with proper headers
|
// Make the fetch request with proper headers
|
||||||
const response = await fetch(connectionBase + `/api/chat/${sessionId}/${type}`, {
|
const response = await fetch(connectionBase + `/api/chat/${sessionId}/${type}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -355,12 +325,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
const token_guess = 500;
|
const token_guess = 500;
|
||||||
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
|
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
|
||||||
|
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
setSnack(`Query sent. Response estimated in ${estimate}s.`, "info");
|
setSnack(`Query sent. Response estimated in ${estimate}s.`, "info");
|
||||||
startCountdown(Math.round(estimate));
|
startCountdown(Math.round(estimate));
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
||||||
@ -395,17 +361,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
|
|
||||||
// Force an immediate state update based on the message type
|
// Force an immediate state update based on the message type
|
||||||
if (update.status === 'processing') {
|
if (update.status === 'processing') {
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
// Update processing message with immediate re-render
|
// Update processing message with immediate re-render
|
||||||
setProcessingMessage({ role: 'status', content: update.message });
|
setProcessingMessage({ role: 'status', content: update.message });
|
||||||
// Add a small delay to ensure React has time to update the UI
|
// Add a small delay to ensure React has time to update the UI
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
} else if (update.status === 'done') {
|
} else if (update.status === 'done') {
|
||||||
// Replace processing message with final result
|
// Replace processing message with final result
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
if (onResponse) {
|
if (onResponse) {
|
||||||
update.message = onResponse(update.message);
|
update.message = onResponse(update.message);
|
||||||
}
|
}
|
||||||
@ -425,12 +386,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
setLastPromptTPS(promptTPS ? promptTPS : 35);
|
setLastPromptTPS(promptTPS ? promptTPS : 35);
|
||||||
updateContextStatus();
|
updateContextStatus();
|
||||||
}
|
}
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
} else if (update.status === 'error') {
|
} else if (update.status === 'error') {
|
||||||
// Show error
|
// Show error
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
setProcessingMessage({ role: 'error', content: update.message });
|
setProcessingMessage({ role: 'error', content: update.message });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setProcessingMessage(undefined);
|
setProcessingMessage(undefined);
|
||||||
@ -438,9 +395,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
|
|
||||||
// Add a small delay to ensure React has time to update the UI
|
// Add a small delay to ensure React has time to update the UI
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSnack("Error processing query", "error")
|
setSnack("Error processing query", "error")
|
||||||
@ -455,7 +409,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
const update = JSON.parse(buffer);
|
const update = JSON.parse(buffer);
|
||||||
|
|
||||||
if (update.status === 'done') {
|
if (update.status === 'done') {
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
if (onResponse) {
|
if (onResponse) {
|
||||||
update.message = onResponse(update.message);
|
update.message = onResponse(update.message);
|
||||||
}
|
}
|
||||||
@ -464,25 +417,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
...conversationRef.current,
|
...conversationRef.current,
|
||||||
update.message
|
update.message
|
||||||
]);
|
]);
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSnack("Error processing query", "error")
|
setSnack("Error processing query", "error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
stopCountdown();
|
stopCountdown();
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
console.error('Fetch error:', error);
|
||||||
setSnack("Unable to process query", "error");
|
setSnack("Unable to process query", "error");
|
||||||
scrolledToBottom = isScrolledToBottom();
|
|
||||||
setProcessingMessage({ role: 'error', content: "Unable to process query" });
|
setProcessingMessage({ role: 'error', content: "Unable to process query" });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setProcessingMessage(undefined);
|
setProcessingMessage(undefined);
|
||||||
@ -490,19 +435,18 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
|||||||
|
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
stopCountdown();
|
stopCountdown();
|
||||||
if (scrolledToBottom) {
|
|
||||||
setTimeout(() => { scrollToBottom() }, 50);
|
|
||||||
}
|
|
||||||
// Add a small delay to ensure React has time to update the UI
|
// Add a small delay to ensure React has time to update the UI
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={className || "Conversation"} sx={{
|
<Box className={className || "Conversation"}
|
||||||
display: "flex", flexDirection: "column", flexGrow: 1, p: 1, mt: 1,
|
ref={scrollRef}
|
||||||
...sx
|
sx={{
|
||||||
}}>
|
display: "flex", flexDirection: "column", flexGrow: 1, p: 1, mt: 0,
|
||||||
|
...sx
|
||||||
|
}}>
|
||||||
{
|
{
|
||||||
filteredConversation.map((message, index) =>
|
filteredConversation.map((message, index) =>
|
||||||
<Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack }} />
|
<Message key={index} {...{ sendQuery, message, connectionBase, sessionId, setSnack }} />
|
||||||
|
@ -248,7 +248,7 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne
|
|||||||
m: 0,
|
m: 0,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
mt: 1,
|
mt: 1,
|
||||||
overflowX: "auto"
|
// overflowX: "auto"
|
||||||
}}>
|
}}>
|
||||||
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0 }}>
|
<CardContent ref={textFieldRef} sx={{ position: "relative", display: "flex", flexDirection: "column", overflowX: "auto", m: 0, p: 0 }}>
|
||||||
<CopyBubble content={message?.content} />
|
<CopyBubble content={message?.content} />
|
||||||
|
@ -18,38 +18,31 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
import { SeverityType } from './Snack';
|
|
||||||
|
|
||||||
import { ChatQuery } from './Message';
|
import { ChatQuery } from './Message';
|
||||||
import { MessageList, MessageData } from './Message';
|
import { MessageList, MessageData } from './Message';
|
||||||
import { SetSnackType } from './Snack';
|
import { SetSnackType } from './Snack';
|
||||||
import { Conversation } from './Conversation';
|
import { Conversation } from './Conversation';
|
||||||
|
|
||||||
/**
|
interface ResumeBuilderProps {
|
||||||
* Props for the DocumentViewer component
|
connectionBase: string,
|
||||||
* @interface DocumentViewerProps
|
sessionId: string | undefined,
|
||||||
* @property {SxProps<Theme>} [sx] - Optional styling properties
|
setSnack: SetSnackType,
|
||||||
* @property {string} [connectionBase] - Base URL for fetch calls
|
|
||||||
* @property {string} [sessionId] - Session ID
|
|
||||||
* @property {SetSnackType} - setSnack UI callback
|
|
||||||
*/
|
|
||||||
export interface DocumentViewerProps {
|
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
connectionBase: string;
|
};
|
||||||
sessionId: string;
|
|
||||||
setSnack: SetSnackType;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* DocumentViewer component
|
* ResumeBuilder component
|
||||||
*
|
*
|
||||||
* A responsive component that displays job descriptions, generated resumes and fact checks
|
* A responsive component that displays job descriptions, generated resumes and fact checks
|
||||||
* with different layouts for mobile and desktop views.
|
* with different layouts for mobile and desktop views.
|
||||||
*/
|
*/
|
||||||
const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||||
sx,
|
sx,
|
||||||
connectionBase,
|
connectionBase,
|
||||||
sessionId,
|
sessionId,
|
||||||
setSnack
|
setSnack
|
||||||
|
|
||||||
}) => {
|
}) => {
|
||||||
// State for editing job description
|
// State for editing job description
|
||||||
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
||||||
@ -229,6 +222,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderJobDescriptionView = useCallback((small: boolean) => {
|
const renderJobDescriptionView = useCallback((small: boolean) => {
|
||||||
|
console.log('renderJobDescriptionView');
|
||||||
const jobDescriptionQuestions = [
|
const jobDescriptionQuestions = [
|
||||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||||
<ChatQuery text="What are the key skills necessary for this position?" submitQuery={handleJobQuery} />
|
<ChatQuery text="What are the key skills necessary for this position?" submitQuery={handleJobQuery} />
|
||||||
@ -339,26 +333,43 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
/>
|
/>
|
||||||
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages]);
|
}, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages]);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the appropriate content based on active state for Desktop
|
* Gets the appropriate content based on active state for Desktop
|
||||||
*/
|
*/
|
||||||
const getActiveDesktopContent = useCallback(() => {
|
const getActiveDesktopContent = useCallback(() => {
|
||||||
/* Left panel - Job Description */
|
const hasSlider = hasResume || hasFacts;
|
||||||
const showResume = hasResume
|
|
||||||
const showFactCheck = hasFacts
|
|
||||||
const ratio = 75 + 25 * splitRatio / 100;
|
const ratio = 75 + 25 * splitRatio / 100;
|
||||||
const otherRatio = showResume ? ratio / (hasFacts ? 3 : 2) : 100;
|
const otherRatio = hasResume ? ratio / (hasFacts ? 3 : 2) : 100;
|
||||||
const resumeRatio = 100 - otherRatio * (hasFacts ? 2 : 1);
|
const resumeRatio = 100 - otherRatio * (hasFacts ? 2 : 1);
|
||||||
const children = [];
|
const children = [];
|
||||||
children.push(
|
children.push(
|
||||||
<Box key="JobDescription" className="ChatBox" sx={{ display: 'flex', flexDirection: 'column', minWidth: `${otherRatio}%`, width: `${otherRatio}%`, maxWidth: `${otherRatio}%`, p: 0, flexGrow: 1, overflowY: 'auto' }}>
|
<Box key="JobDescription" className="ChatBox" sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minWidth: `${otherRatio}%`,
|
||||||
|
width: `${otherRatio}%`,
|
||||||
|
maxWidth: `${otherRatio}%`,
|
||||||
|
p: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
}}>
|
||||||
{renderJobDescriptionView(false)}
|
{renderJobDescriptionView(false)}
|
||||||
</Box>);
|
</Box>);
|
||||||
|
|
||||||
/* Resume panel - conditionally rendered if resume defined, or processing is in progress */
|
/* Resume panel - conditionally rendered if resume defined, or processing is in progress */
|
||||||
if (showResume) {
|
if (hasResume) {
|
||||||
children.push(
|
children.push(
|
||||||
<Box key="ResumeView" className="ChatBox" sx={{ display: 'flex', flexDirection: 'column', minWidth: `${resumeRatio}%`, width: `${resumeRatio}%`, maxWidth: `${resumeRatio}%`, p: 0, flexGrow: 1, overflowY: 'auto' }}>
|
<Box key="ResumeView"
|
||||||
|
className="ChatBox"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minWidth: `${resumeRatio}%`,
|
||||||
|
width: `${resumeRatio}%`,
|
||||||
|
maxWidth: `${resumeRatio}%`,
|
||||||
|
p: 0,
|
||||||
|
flexGrow: 1
|
||||||
|
}}>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
{renderResumeView(false)}
|
{renderResumeView(false)}
|
||||||
</Box>
|
</Box>
|
||||||
@ -366,9 +377,19 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */
|
/* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */
|
||||||
if (showFactCheck) {
|
if (hasFacts) {
|
||||||
children.push(
|
children.push(
|
||||||
<Box key="FactCheckView" className="ChatBox" sx={{ display: 'flex', flexDirection: 'column', minWidth: `${otherRatio}%`, width: `${otherRatio}%`, maxWidth: `${otherRatio}%`, p: 0, flexGrow: 1, overflowY: 'auto' }}>
|
<Box key="FactCheckView"
|
||||||
|
className="ChatBox"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minWidth: `${otherRatio}%`,
|
||||||
|
width: `${otherRatio}%`,
|
||||||
|
maxWidth: `${otherRatio}%`,
|
||||||
|
p: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
}}>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
{renderFactCheckView(false)}
|
{renderFactCheckView(false)}
|
||||||
</Box>
|
</Box>
|
||||||
@ -377,7 +398,7 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
|
|
||||||
/* Split control panel - conditionally rendered if either facts or resume is set */
|
/* Split control panel - conditionally rendered if either facts or resume is set */
|
||||||
let slider = <Box key="slider"></Box>;
|
let slider = <Box key="slider"></Box>;
|
||||||
if (showResume || showFactCheck) {
|
if (hasSlider) {
|
||||||
slider = (
|
slider = (
|
||||||
<Paper key="slider" sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Paper key="slider" sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Stack direction="row" spacing={2} alignItems="center" sx={{ width: '60%' }}>
|
<Stack direction="row" spacing={2} alignItems="center" sx={{ width: '60%' }}>
|
||||||
@ -406,35 +427,49 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ ...sx, display: 'flex', flexGrow: 1, flexDirection: 'column', p: 0 }}>
|
<Box sx={{
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1, flexDirection: 'row', overflow: 'hidden', p: 0 }}>
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
overflow: 'hidden',
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
margin: "0 auto",
|
||||||
|
maxWidth: hasSlider ? "100%" : "1024px",
|
||||||
|
width: hasSlider ? "100%" : "1024px",
|
||||||
|
height: `calc(100vh - ${hasSlider ? 144 : 72}px)`,
|
||||||
|
backgroundColor: "#F5F5F5",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
{slider}
|
{slider}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, sx, hasFacts, hasResume]);
|
}, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, hasFacts, hasResume]);
|
||||||
|
|
||||||
// Render mobile view
|
// Render mobile view
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
/**
|
|
||||||
* Gets the appropriate content based on active tab
|
|
||||||
*/
|
|
||||||
const getActiveMobileContent = () => {
|
|
||||||
switch (activeTab) {
|
|
||||||
case 0:
|
|
||||||
return renderJobDescriptionView(true);
|
|
||||||
case 1:
|
|
||||||
return renderResumeView(true);
|
|
||||||
case 2:
|
|
||||||
return renderFactCheckView(true);
|
|
||||||
default:
|
|
||||||
return renderJobDescriptionView(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, ...sx }}>
|
<Box sx={{
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
margin: "0 auto",
|
||||||
|
overflow: "hidden",
|
||||||
|
height: "calc(100vh - 72px)",
|
||||||
|
backgroundColor: "#F5F5F5",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
@ -449,47 +484,17 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
|||||||
|
|
||||||
{/* Document display area */}
|
{/* Document display area */}
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx }}>
|
||||||
{getActiveMobileContent()}
|
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(true)}</Box>
|
||||||
|
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView(true)}</Box>
|
||||||
|
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(true)}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return getActiveDesktopContent();
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, width: "100%", ...sx }}>
|
|
||||||
{getActiveDesktopContent()}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ResumeBuilderProps {
|
|
||||||
connectionBase: string,
|
|
||||||
sessionId: string | undefined,
|
|
||||||
setSnack: (message: string, severity?: SeverityType) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResumeBuilder = ({ connectionBase, sessionId, setSnack }: ResumeBuilderProps) => {
|
|
||||||
if (sessionId === undefined) {
|
|
||||||
return (<></>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className="DocBox">
|
|
||||||
<Box className="Conversation" sx={{ p: 0, pt: 1 }}>
|
|
||||||
<DocumentViewer sx={{
|
|
||||||
p: 0,
|
|
||||||
m: 0,
|
|
||||||
display: "flex",
|
|
||||||
flexGrow: 1,
|
|
||||||
overflowY: "auto",
|
|
||||||
flexDirection: "column",
|
|
||||||
height: "calc(0vh - 0px)", // Hack to make the height work
|
|
||||||
}} {...{ setSnack, connectionBase, sessionId }} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ResumeBuilderProps
|
ResumeBuilderProps
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,8 @@ import Switch from '@mui/material/Switch';
|
|||||||
|
|
||||||
import { SetSnackType } from './Snack';
|
import { SetSnackType } from './Snack';
|
||||||
|
|
||||||
|
import './VectorVisualizer.css';
|
||||||
|
|
||||||
interface Metadata {
|
interface Metadata {
|
||||||
doc_type?: string;
|
doc_type?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -299,16 +301,30 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inli
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="VectorVisualizer" sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
|
<Box className="VectorVisualizer"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1
|
||||||
|
}}>
|
||||||
{
|
{
|
||||||
!inline &&
|
!inline &&
|
||||||
<Card sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', mb: 1, pt: 0 }}>
|
<Card sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, minHeight: '2.5rem', justifyContent: 'center', alignItems: 'center', m: 0, p: 0, mb: 1 }}>
|
||||||
<Typography variant="h6" sx={{ p: 1, pt: 0 }}>
|
<Typography variant="h6" sx={{ p: 1, pt: 0 }}>
|
||||||
Similarity Visualization via Uniform Manifold Approximation and Projection (UMAP)
|
Similarity Visualization via Uniform Manifold Approximation and Projection (UMAP)
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
<FormControlLabel sx={{ display: "inline-flex", width: "fit-content", mb: '-2.5rem', zIndex: 100, ml: 1, flexBasis: 0, flexGrow: 0 }} control={<Switch checked={!view2D} />} onChange={() => setView2D(!view2D)} label="3D" />
|
<FormControlLabel sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
width: "fit-content",
|
||||||
|
mb: '-2.5rem',
|
||||||
|
zIndex: 100,
|
||||||
|
ml: 1,
|
||||||
|
flexBasis: 0,
|
||||||
|
flexGrow: 0
|
||||||
|
}}
|
||||||
|
control={<Switch checked={!view2D} />} onChange={() => setView2D(!view2D)} label="3D" />
|
||||||
<Plot
|
<Plot
|
||||||
onClick={(event: any) => {
|
onClick={(event: any) => {
|
||||||
const point = event.points[0];
|
const point = event.points[0];
|
||||||
@ -323,7 +339,6 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inli
|
|||||||
content: `${emoji} ${type.toUpperCase()}\n${text}`,
|
content: `${emoji} ${type.toUpperCase()}\n${text}`,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
data={[plotData.data]}
|
data={[plotData.data]}
|
||||||
useResizeHandler={true}
|
useResizeHandler={true}
|
||||||
config={{
|
config={{
|
||||||
@ -333,7 +348,17 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inli
|
|||||||
showSendToCloud: false,
|
showSendToCloud: false,
|
||||||
staticPlot: false,
|
staticPlot: false,
|
||||||
}}
|
}}
|
||||||
style={{ display: "flex", flexGrow: 1, justifyContent: 'center', alignItems: 'center', minHeight: '30vh', height: '30vh', padding: 0, margin: 0 }}
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '240px',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
layout={plotData.layout}
|
layout={plotData.layout}
|
||||||
/>
|
/>
|
||||||
{!inline &&
|
{!inline &&
|
||||||
@ -389,7 +414,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = ({ setSnack, rag, inli
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { VectorVisualizerProps, ResultData, Metadata };
|
export type { VectorVisualizerProps, ResultData };
|
||||||
|
|
||||||
export {
|
export {
|
||||||
VectorVisualizer
|
VectorVisualizer
|
||||||
|
@ -7,6 +7,7 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@ -161,6 +161,8 @@ When answering queries, follow these steps:
|
|||||||
4. When both [INFO] and tool outputs are relevant, synthesize information from both sources to provide the most complete answer
|
4. When both [INFO] and tool outputs are relevant, synthesize information from both sources to provide the most complete answer
|
||||||
5. Always prioritize the most up-to-date and relevant information, whether it comes from [INFO] or tools
|
5. Always prioritize the most up-to-date and relevant information, whether it comes from [INFO] or tools
|
||||||
6. If [INFO] and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
|
6. If [INFO] and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
|
||||||
|
7. If there is information in the [INFO], [JOB DESCRIPTION], or [WORK HISTORY] sections to enhance the answer, incorporate it seamlessly and refer to it as 'the latest information' or 'recent data' instead of mentioning '[INFO]' (etc.) or quoting it directly.
|
||||||
|
8. Avoid phrases like 'According to the [INFO]' or similar references to the [INFO], [JOB DESCRIPTION], or [WORK HISTORY] tags.
|
||||||
|
|
||||||
Always use tools and [INFO] when possible. Be concise, and never make up information. If you do not know the answer, say so.
|
Always use tools and [INFO] when possible. Be concise, and never make up information. If you do not know the answer, say so.
|
||||||
""".strip()
|
""".strip()
|
||||||
@ -182,6 +184,8 @@ When answering queries, follow these steps:
|
|||||||
8. Use the [INTRO] to highlight the use of AI in generating this resume.
|
8. Use the [INTRO] to highlight the use of AI in generating this resume.
|
||||||
9. Use the [WORK HISTORY] to create a polished, professional resume.
|
9. Use the [WORK HISTORY] to create a polished, professional resume.
|
||||||
10. Do not list any locations or mailing addresses in the resume.
|
10. Do not list any locations or mailing addresses in the resume.
|
||||||
|
11. If there is information in the [INFO], [JOB DESCRIPTION], [WORK HISTORY], or [RESUME] sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '[JOB DESCRIPTION]' (etc.) or quoting it directly.
|
||||||
|
12. Avoid phrases like 'According to the [INFO]' or similar references to the [INFO], [JOB DESCRIPTION], or [WORK HISTORY] tags.
|
||||||
|
|
||||||
Structure the resume professionally with the following sections where applicable:
|
Structure the resume professionally with the following sections where applicable:
|
||||||
|
|
||||||
@ -205,6 +209,8 @@ If there are inaccuracies, list them in a bullet point format.
|
|||||||
When answering queries, follow these steps:
|
When answering queries, follow these steps:
|
||||||
1. You must not invent or assume any information not explicitly present in the [WORK HISTORY].
|
1. You must not invent or assume any information not explicitly present in the [WORK HISTORY].
|
||||||
2. Analyze the [RESUME] to identify any discrepancies or inaccuracies based on the [WORK HISTORY].
|
2. Analyze the [RESUME] to identify any discrepancies or inaccuracies based on the [WORK HISTORY].
|
||||||
|
3. If there is information in the [INFO], [JOB DESCRIPTION], [WORK HISTORY], or [RESUME] sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '[JOB DESCRIPTION]' (etc.) or quoting it directly.
|
||||||
|
4. Avoid phrases like 'According to the [INFO]' or similar references to the [INFO], [JOB DESCRIPTION], [RESUME], or [WORK HISTORY] tags.
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
system_job_description = f"""
|
system_job_description = f"""
|
||||||
@ -215,6 +221,8 @@ You are a hiring and job placing specialist. Your task is to answers about a job
|
|||||||
When answering queries, follow these steps:
|
When answering queries, follow these steps:
|
||||||
1. Analyze the [JOB DESCRIPTION] to provide insights for the asked question.
|
1. Analyze the [JOB DESCRIPTION] to provide insights for the asked question.
|
||||||
2. If any financial information is requested, be sure to account for inflation.
|
2. If any financial information is requested, be sure to account for inflation.
|
||||||
|
3. If there is information in the [INFO], [JOB DESCRIPTION], [WORK HISTORY], or [RESUME] sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '[JOB DESCRIPTION]' (etc.) or quoting it directly.
|
||||||
|
4. Avoid phrases like 'According to the [INFO]' or similar references to the [INFO], [JOB DESCRIPTION], [RESUME], or [WORK HISTORY] tags.
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
def create_system_message(prompt):
|
def create_system_message(prompt):
|
||||||
@ -1057,14 +1065,15 @@ class WebServer:
|
|||||||
if rag_context:
|
if rag_context:
|
||||||
preamble = f"""
|
preamble = f"""
|
||||||
1. Respond to this query: {content}
|
1. Respond to this query: {content}
|
||||||
2. If there is information in the [INFO] section to enhance the answer, incorporate it seamlessly and refer to it as 'the latest information' or 'recent data' instead of mentioning '[INFO]' or quoting it directly.
|
2. If there is information in the [INFO], [JOB DESCRIPTION], [WORK HISTORY], or [RESUME] sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '[JOB DESCRIPTION]' (etc.) or quoting it directly.
|
||||||
3. Avoid phrases like 'According to the [INFO]' or similar references to the [INFO] tag.
|
3. Avoid phrases like 'According to the [INFO]' or similar references to the [INFO], [JOB DESCRIPTION], or [WORK HISTORY] tags.
|
||||||
[INFO]
|
[INFO]
|
||||||
{rag_context}
|
{rag_context}
|
||||||
[/INFO]
|
[/INFO]
|
||||||
Use that information to respond to:"""
|
Use that information to respond to:"""
|
||||||
|
|
||||||
system_prompt = context["sessions"]["chat"]["system_prompt"]
|
# Use the mode specific system_prompt instead of 'chat'
|
||||||
|
system_prompt = context["sessions"][type]["system_prompt"]
|
||||||
|
|
||||||
# On first entry, a single job_description is provided ("user")
|
# On first entry, a single job_description is provided ("user")
|
||||||
# Generate a resume to append to RESUME history
|
# Generate a resume to append to RESUME history
|
||||||
|
Loading…
x
Reference in New Issue
Block a user