All UI seems to work
This commit is contained in:
parent
97425a6aad
commit
b6dd4878c8
86
frontend/src/AboutPage.tsx
Normal file
86
frontend/src/AboutPage.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { Document } from './Document';
|
||||
|
||||
const AboutPage = (props: BackstoryPageProps) => {
|
||||
const { sessionId, submitQuery, setSnack, route, setRoute } = props;
|
||||
const [ page, setPage ] = useState<string>("");
|
||||
const [ subRoute, setSubRoute] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`AboutPage: ${page}`);
|
||||
}, [page]);
|
||||
useEffect(() => {
|
||||
console.log(`AboutPage: ${page} - subRoute: ${subRoute}`);
|
||||
}, [subRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
if (route === undefined) { return; }
|
||||
const parts = route.split("/");
|
||||
if (parts.length === 0) { return; }
|
||||
setPage(parts[0]);
|
||||
if (parts.length > 1) {
|
||||
parts.shift();
|
||||
setSubRoute(parts.join("/"));
|
||||
}
|
||||
}, [route, setPage, setSubRoute]);
|
||||
|
||||
const onDocumentExpand = (document: string, open: boolean) => {
|
||||
console.log("Document expanded:", document, open);
|
||||
if (open) {
|
||||
setPage(document);
|
||||
} else {
|
||||
setPage("");
|
||||
}
|
||||
/* This is just to quiet warnings for now...*/
|
||||
if (route === "never" && subRoute && setRoute) {
|
||||
setRoute(document);
|
||||
setSubRoute(document);
|
||||
}
|
||||
}
|
||||
|
||||
return <Scrollable
|
||||
autoscroll={false}
|
||||
sx={{
|
||||
maxWidth: "1024px",
|
||||
height: "calc(100vh - 72px)",
|
||||
flexDirection: "column",
|
||||
margin: "0 auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Document {...{
|
||||
title: "About",
|
||||
filepath: "/docs/about.md",
|
||||
onExpand: (open: boolean) => { onDocumentExpand('about', open); },
|
||||
expanded: page === 'about',
|
||||
sessionId,
|
||||
submitQuery: submitQuery,
|
||||
setSnack,
|
||||
}} />
|
||||
<Document {...{
|
||||
title: "Resume Generation Architecture",
|
||||
filepath: "/docs/resume-generation.md",
|
||||
onExpand: (open: boolean) => { onDocumentExpand('resume-generation', open); },
|
||||
expanded: page === 'resume-generation',
|
||||
sessionId,
|
||||
submitQuery: submitQuery,
|
||||
setSnack,
|
||||
}} />
|
||||
<Document {...{
|
||||
title: "Application Architecture",
|
||||
filepath: "/docs/about-app.md",
|
||||
onExpand: (open: boolean) => { onDocumentExpand('about-app', open); },
|
||||
expanded: page === 'about-app',
|
||||
sessionId,
|
||||
submitQuery: submitQuery,
|
||||
setSnack,
|
||||
}} />
|
||||
</Scrollable>;
|
||||
};
|
||||
|
||||
export {
|
||||
AboutPage
|
||||
};
|
434
frontend/src/ControlsPage.tsx
Normal file
434
frontend/src/ControlsPage.tsx
Normal file
@ -0,0 +1,434 @@
|
||||
import React, { useState, useEffect, ReactElement } from 'react';
|
||||
// import FormGroup from '@mui/material/FormGroup';
|
||||
// import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
// import Switch from '@mui/material/Switch';
|
||||
// import Divider from '@mui/material/Divider';
|
||||
// import TextField from '@mui/material/TextField';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionActions from '@mui/material/AccordionActions';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import Typography from '@mui/material/Typography';
|
||||
// import Button from '@mui/material/Button';
|
||||
// import Box from '@mui/material/Box';
|
||||
// import ResetIcon from '@mui/icons-material/History';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
import { connectionBase } from './Global';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { restyle } from 'plotly.js';
|
||||
|
||||
interface ServerTunables {
|
||||
system_prompt: string,
|
||||
message_history_length: number,
|
||||
tools: Tool[],
|
||||
rags: Tool[]
|
||||
};
|
||||
|
||||
type Tool = {
|
||||
type: string,
|
||||
enabled: boolean
|
||||
name: string,
|
||||
description: string,
|
||||
parameters?: any,
|
||||
returns?: any
|
||||
};
|
||||
|
||||
type GPUInfo = {
|
||||
name: string,
|
||||
memory: number,
|
||||
discrete: boolean
|
||||
}
|
||||
|
||||
type SystemInfo = {
|
||||
"Installed RAM": string,
|
||||
"Graphics Card": GPUInfo[],
|
||||
"CPU": string
|
||||
};
|
||||
|
||||
const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({ systemInfo }) => {
|
||||
const [systemElements, setSystemElements] = useState<ReactElement[]>([]);
|
||||
|
||||
const convertToSymbols = (text: string) => {
|
||||
return text
|
||||
.replace(/\(R\)/g, '®') // Replace (R) with the ® symbol
|
||||
.replace(/\(C\)/g, '©') // Replace (C) with the © symbol
|
||||
.replace(/\(TM\)/g, '™'); // Replace (TM) with the ™ symbol
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (systemInfo === undefined) {
|
||||
return;
|
||||
}
|
||||
const elements = Object.entries(systemInfo).flatMap(([k, v]) => {
|
||||
// If v is an array, repeat for each card
|
||||
if (Array.isArray(v)) {
|
||||
return v.map((card, index) => (
|
||||
<div key={index} className="SystemInfoItem">
|
||||
<div>{convertToSymbols(k)} {index}</div>
|
||||
<div>{convertToSymbols(card.name)} {card.discrete ? `w/ ${Math.round(card.memory / (1024 * 1024 * 1024))}GB RAM` : "(integrated)"}</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
// If it's not an array, handle normally
|
||||
return (
|
||||
<div key={k} className="SystemInfoItem">
|
||||
<div>{convertToSymbols(k)}</div>
|
||||
<div>{convertToSymbols(String(v))}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
setSystemElements(elements);
|
||||
}, [systemInfo]);
|
||||
|
||||
return <div className="SystemInfo">{systemElements}</div>;
|
||||
};
|
||||
|
||||
const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
|
||||
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [rags, setRags] = useState<Tool[]>([]);
|
||||
const [systemPrompt, setSystemPrompt] = useState<string>("");
|
||||
const [messageHistoryLength, setMessageHistoryLength] = useState<number>(5);
|
||||
const [serverTunables, setServerTunables] = useState<ServerTunables | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverTunables === undefined || systemPrompt === serverTunables.system_prompt || !systemPrompt.trim() || sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
const sendSystemPrompt = async (prompt: string) => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "system_prompt": prompt }),
|
||||
});
|
||||
|
||||
const tunables = await response.json();
|
||||
serverTunables.system_prompt = tunables.system_prompt;
|
||||
setSystemPrompt(tunables.system_prompt)
|
||||
setSnack("System prompt updated", "success");
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("System prompt update failed", "error");
|
||||
}
|
||||
};
|
||||
|
||||
sendSystemPrompt(systemPrompt);
|
||||
|
||||
}, [systemPrompt, sessionId, setSnack, serverTunables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverTunables === undefined || messageHistoryLength === serverTunables.message_history_length || !messageHistoryLength || sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
const sendMessageHistoryLength = async (length: number) => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "message_history_length": length }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const newLength = data["message_history_length"];
|
||||
if (newLength !== messageHistoryLength) {
|
||||
setMessageHistoryLength(newLength);
|
||||
setSnack("Message history length updated", "success");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Message history length update failed", "error");
|
||||
}
|
||||
};
|
||||
|
||||
sendMessageHistoryLength(messageHistoryLength);
|
||||
|
||||
}, [messageHistoryLength, setMessageHistoryLength, sessionId, setSnack, serverTunables]);
|
||||
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt" | "message_history_length")[], message: string = "Update successful.") => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "reset": types }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw Error()
|
||||
}
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
switch (key) {
|
||||
case "rags":
|
||||
setRags(value as Tool[]);
|
||||
break;
|
||||
case "tools":
|
||||
setTools(value as Tool[]);
|
||||
break;
|
||||
case "system_prompt":
|
||||
setSystemPrompt((value as ServerTunables)["system_prompt"].trim());
|
||||
break;
|
||||
case "history":
|
||||
console.log('TODO: handle history reset');
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSnack(message, "success");
|
||||
} else {
|
||||
throw Error(`${{ status: response.status, message: response.statusText }}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Unable to restore defaults", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Get the system information
|
||||
useEffect(() => {
|
||||
if (systemInfo !== undefined || sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
fetch(connectionBase + `/api/system-info/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setSystemInfo(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error obtaining system information:', error);
|
||||
setSnack("Unable to obtain system information.", "error");
|
||||
});
|
||||
}, [systemInfo, setSystemInfo, setSnack, sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
setEditSystemPrompt(systemPrompt.trim());
|
||||
}, [systemPrompt, setEditSystemPrompt]);
|
||||
|
||||
const toggleRag = async (tool: Tool) => {
|
||||
tool.enabled = !tool.enabled
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "rags": [{ "name": tool?.name, "enabled": tool.enabled }] }),
|
||||
});
|
||||
|
||||
const tunables: ServerTunables = await response.json();
|
||||
setRags(tunables.rags)
|
||||
setSnack(`${tool?.name} ${tool.enabled ? "enabled" : "disabled"}`);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack(`${tool?.name} ${tool.enabled ? "enabling" : "disabling"} failed.`, "error");
|
||||
tool.enabled = !tool.enabled
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTool = async (tool: Tool) => {
|
||||
tool.enabled = !tool.enabled
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "tools": [{ "name": tool.name, "enabled": tool.enabled }] }),
|
||||
});
|
||||
|
||||
const tunables: ServerTunables = await response.json();
|
||||
setTools(tunables.tools)
|
||||
setSnack(`${tool.name} ${tool.enabled ? "enabled" : "disabled"}`);
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack(`${tool.name} ${tool.enabled ? "enabling" : "disabling"} failed.`, "error");
|
||||
tool.enabled = !tool.enabled
|
||||
}
|
||||
};
|
||||
|
||||
// If the systemPrompt has not been set, fetch it from the server
|
||||
useEffect(() => {
|
||||
if (serverTunables !== undefined || sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
const fetchTunables = async () => {
|
||||
// Make the fetch request with proper headers
|
||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log("Server tunables: ", data);
|
||||
setServerTunables(data);
|
||||
setSystemPrompt(data["system_prompt"]);
|
||||
setMessageHistoryLength(data["message_history_length"]);
|
||||
setTools(data["tools"]);
|
||||
setRags(data["rags"]);
|
||||
}
|
||||
|
||||
fetchTunables();
|
||||
}, [sessionId, setServerTunables, setSystemPrompt, setMessageHistoryLength, serverTunables, setTools, setRags]);
|
||||
|
||||
const toggle = async (type: string, index: number) => {
|
||||
switch (type) {
|
||||
case "rag":
|
||||
if (rags === undefined) {
|
||||
return;
|
||||
}
|
||||
toggleRag(rags[index])
|
||||
break;
|
||||
case "tool":
|
||||
if (tools === undefined) {
|
||||
return;
|
||||
}
|
||||
toggleTool(tools[index]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: any) => {
|
||||
if (event.key === 'Enter' && event.ctrlKey) {
|
||||
setSystemPrompt(editSystemPrompt);
|
||||
}
|
||||
};
|
||||
|
||||
return (<div className="Controls">
|
||||
{/* <Typography component="span" sx={{ mb: 1 }}>
|
||||
You can change the information available to the LLM by adjusting the following settings:
|
||||
</Typography>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">System Prompt</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionActions style={{ display: "flex", flexDirection: "column" }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
slotProps={{
|
||||
htmlInput: { style: { fontSize: "0.85rem", lineHeight: "1.25rem" } }
|
||||
}}
|
||||
type="text"
|
||||
value={editSystemPrompt}
|
||||
onChange={(e) => setEditSystemPrompt(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Enter the new system prompt.."
|
||||
/>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
||||
<Button variant="contained" disabled={editSystemPrompt.trim() === systemPrompt.trim()} onClick={() => { setSystemPrompt(editSystemPrompt.trim()); }}>Set</Button>
|
||||
<Button variant="outlined" onClick={() => { reset(["system_prompt"], "System prompt reset."); }} color="error">Reset</Button>
|
||||
</Box>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">Tunables</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionActions style={{ flexDirection: "column" }}>
|
||||
<TextField
|
||||
id="outlined-number"
|
||||
label="Message history"
|
||||
type="number"
|
||||
helperText="Only use this many messages as context. 0 = All. Keeping this low will reduce context growth and improve performance."
|
||||
value={messageHistoryLength}
|
||||
onChange={(e: any) => setMessageHistoryLength(e.target.value)}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
min: 0
|
||||
},
|
||||
inputLabel: {
|
||||
shrink: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">Tools</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
These tools can be made available to the LLM for obtaining real-time information from the Internet. The description provided to the LLM is provided for reference.
|
||||
</AccordionDetails>
|
||||
<AccordionActions>
|
||||
<FormGroup sx={{ p: 1 }}>
|
||||
{
|
||||
(tools || []).map((tool, index) =>
|
||||
<Box key={index}>
|
||||
<Divider />
|
||||
<FormControlLabel control={<Switch checked={tool.enabled} />} onChange={() => toggle("tool", index)} label={tool.name} />
|
||||
<Typography sx={{ fontSize: "0.8rem", mb: 1 }}>{tool.description}</Typography>
|
||||
</Box>
|
||||
)
|
||||
}</FormGroup>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">RAG</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
These RAG databases can be enabled / disabled for adding additional context based on the chat request.
|
||||
</AccordionDetails>
|
||||
<AccordionActions>
|
||||
<FormGroup sx={{ p: 1, flexGrow: 1, justifyContent: "flex-start" }}>
|
||||
{
|
||||
(rags || []).map((rag, index) =>
|
||||
<Box key={index} sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }}>
|
||||
<Divider />
|
||||
<FormControlLabel
|
||||
control={<Switch checked={rag.enabled} />}
|
||||
onChange={() => toggle("rag", index)} label={rag.name}
|
||||
/>
|
||||
<Typography>{rag.description}</Typography>
|
||||
</Box>
|
||||
)
|
||||
}</FormGroup>
|
||||
</AccordionActions>
|
||||
</Accordion> */}
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">System Information</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
The server is running on the following hardware:
|
||||
</AccordionDetails>
|
||||
<AccordionActions>
|
||||
<SystemInfoComponent systemInfo={systemInfo} />
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
|
||||
{/* <Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Delete Backstory History</Button>
|
||||
<Button onClick={() => { reset(["rags", "tools", "system_prompt", "message_history_length"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button> */}
|
||||
</div>);
|
||||
}
|
||||
|
||||
export {
|
||||
ControlsPage
|
||||
};
|
14
frontend/src/Global.tsx
Normal file
14
frontend/src/Global.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
const getConnectionBase = (loc: any): string => {
|
||||
console.log(`getConnectionBase(${loc})`)
|
||||
if (!loc.host.match(/.*battle-linux.*/)) {
|
||||
return loc.protocol + "//" + loc.host;
|
||||
} else {
|
||||
return loc.protocol + "//battle-linux.ketrenos.com:8912";
|
||||
}
|
||||
}
|
||||
|
||||
const connectionBase = getConnectionBase(window.location);
|
||||
|
||||
export {
|
||||
connectionBase
|
||||
};
|
69
frontend/src/HomePage.tsx
Normal file
69
frontend/src/HomePage.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import MuiMarkdown from 'mui-markdown';
|
||||
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { Conversation, ConversationHandle } from './Conversation';
|
||||
import { ChatQuery } from './ChatQuery';
|
||||
import { MessageList } from './Message';
|
||||
|
||||
const HomePage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||
const { sessionId, setSnack, submitQuery } = props;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const backstoryPreamble: MessageList = [
|
||||
{
|
||||
role: 'content',
|
||||
title: 'Welcome to Backstory',
|
||||
disableCopy: true,
|
||||
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 backstoryQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
<ChatQuery prompt="What is James Ketrenos' work history?" tunables={{ enable_tools: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery prompt="What programming languages has James used?" tunables={{ enable_tools: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery prompt="What are James' professional strengths?" tunables={{ enable_tools: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery prompt="What are today's headlines on CNBC.com?" tunables={{ enable_tools: true, enable_rag: false, enable_context: false }} submitQuery={submitQuery} />
|
||||
</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 <Conversation
|
||||
sx={{
|
||||
maxWidth: "1024px",
|
||||
height: "calc(100vh - 72px)",
|
||||
}}
|
||||
ref={ref}
|
||||
{...{
|
||||
type: "chat",
|
||||
prompt: "What would you like to know about James?",
|
||||
resetLabel: "chat",
|
||||
sessionId,
|
||||
setSnack,
|
||||
preamble: backstoryPreamble,
|
||||
defaultPrompts: backstoryQuestions,
|
||||
submitQuery,
|
||||
}}
|
||||
/>;
|
||||
});
|
||||
|
||||
export {
|
||||
HomePage
|
||||
};
|
6
frontend/src/ResumeBuilderPage.css
Normal file
6
frontend/src/ResumeBuilderPage.css
Normal file
@ -0,0 +1,6 @@
|
||||
.ResumeBuilder .JsonViewScrollable {
|
||||
min-height: unset !important;
|
||||
max-height: 30rem !important;
|
||||
border: 1px solid orange;
|
||||
overflow-x: auto !important;
|
||||
}
|
367
frontend/src/ResumeBuilderPage.tsx
Normal file
367
frontend/src/ResumeBuilderPage.tsx
Normal file
@ -0,0 +1,367 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { SxProps } from '@mui/material';
|
||||
|
||||
import { ChatQuery } from './ChatQuery';
|
||||
import { MessageList, MessageData } from './Message';
|
||||
import { Conversation } from './Conversation';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
|
||||
import './ResumeBuilderPage.css';
|
||||
|
||||
/**
|
||||
* ResumeBuilder component
|
||||
*
|
||||
* A responsive component that displays job descriptions, generated resumes and fact checks
|
||||
* with different layouts for mobile and desktop views.
|
||||
*/
|
||||
const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
|
||||
sx,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
}) => {
|
||||
// State for editing job description
|
||||
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
||||
const [hasResume, setHasResume] = useState<boolean>(false);
|
||||
const [hasFacts, setHasFacts] = useState<boolean>(false);
|
||||
const jobConversationRef = useRef<any>(null);
|
||||
const resumeConversationRef = useRef<any>(null);
|
||||
const factsConversationRef = useRef<any>(null);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
|
||||
/**
|
||||
* Handle tab change for mobile view
|
||||
*/
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number): void => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
const handleJobQuery = (query: string) => {
|
||||
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler');
|
||||
jobConversationRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
const handleResumeQuery = (query: string) => {
|
||||
console.log(`handleResumeQuery: ${query} -- `, resumeConversationRef.current ? ' sending' : 'no handler');
|
||||
resumeConversationRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
const handleFactsQuery = (query: string) => {
|
||||
console.log(`handleFactsQuery: ${query} -- `, factsConversationRef.current ? ' sending' : 'no handler');
|
||||
factsConversationRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
const filterJobDescriptionMessages = useCallback((messages: MessageList): MessageList => {
|
||||
if (messages === undefined || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (messages.length > 2) {
|
||||
setHasResume(true);
|
||||
setHasFacts(true);
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
messages[0].role = 'content';
|
||||
messages[0].title = 'Job Description';
|
||||
messages[0].disableCopy = false;
|
||||
messages[0].expandable = true;
|
||||
}
|
||||
|
||||
if (messages.length > 3) {
|
||||
// messages[2] is Show job requirements
|
||||
messages[3].role = 'job-requirements';
|
||||
messages[3].title = 'Job Requirements';
|
||||
messages[3].disableCopy = false;
|
||||
messages[3].expanded = false;
|
||||
messages[3].expandable = true;
|
||||
}
|
||||
|
||||
/* Filter out the 2nd and 3rd (0-based) */
|
||||
const filtered = messages.filter((m, i) => i !== 1 && i !== 2);
|
||||
|
||||
return filtered;
|
||||
}, [setHasResume, setHasFacts]);
|
||||
|
||||
const filterResumeMessages = useCallback((messages: MessageList): MessageList => {
|
||||
if (messages === undefined || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (messages.length > 1) {
|
||||
// messages[0] is Show Qualifications
|
||||
messages[1].role = 'qualifications';
|
||||
messages[1].title = 'Candidate qualifications';
|
||||
messages[1].disableCopy = false;
|
||||
messages[1].expanded = false;
|
||||
messages[1].expandable = true;
|
||||
}
|
||||
|
||||
if (messages.length > 3) {
|
||||
// messages[2] is Show Resume
|
||||
messages[3].role = 'resume';
|
||||
messages[3].title = 'Generated Resume';
|
||||
messages[3].disableCopy = false;
|
||||
messages[3].expanded = true;
|
||||
messages[3].expandable = true;
|
||||
}
|
||||
|
||||
/* Filter out the 1st and 3rd messages (0-based) */
|
||||
const filtered = messages.filter((m, i) => i !== 0 && i !== 2);
|
||||
|
||||
return filtered;
|
||||
}, []);
|
||||
|
||||
const filterFactsMessages = useCallback((messages: MessageList): MessageList => {
|
||||
if (messages === undefined || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (messages.length > 1) {
|
||||
// messages[0] is Show verification
|
||||
messages[1].role = 'fact-check';
|
||||
messages[1].title = 'Fact Check';
|
||||
messages[1].disableCopy = false;
|
||||
messages[1].expanded = true;
|
||||
messages[1].expandable = true;
|
||||
}
|
||||
|
||||
/* Filter out the 1st (0-based) */
|
||||
const filtered = messages.filter((m, i) => i !== 0);
|
||||
|
||||
return filtered;
|
||||
}, []);
|
||||
|
||||
const jobResponse = useCallback(async (message: MessageData) => {
|
||||
console.log('onJobResponse', message);
|
||||
if (message.actions && message.actions.includes("job_description")) {
|
||||
await jobConversationRef.current.fetchHistory();
|
||||
}
|
||||
if (message.actions && message.actions.includes("resume_generated")) {
|
||||
await resumeConversationRef.current.fetchHistory();
|
||||
setHasResume(true);
|
||||
setActiveTab(1); // Switch to Resume tab
|
||||
}
|
||||
if (message.actions && message.actions.includes("facts_checked")) {
|
||||
await factsConversationRef.current.fetchHistory();
|
||||
setHasFacts(true);
|
||||
}
|
||||
}, [setHasFacts, setHasResume, setActiveTab]);
|
||||
|
||||
const resumeResponse = useCallback((message: MessageData): void => {
|
||||
console.log('onResumeResponse', message);
|
||||
setHasFacts(true);
|
||||
}, [setHasFacts]);
|
||||
|
||||
const factsResponse = useCallback((message: MessageData): void => {
|
||||
console.log('onFactsResponse', message);
|
||||
}, []);
|
||||
|
||||
const resetJobDescription = useCallback(() => {
|
||||
setHasJobDescription(false);
|
||||
setHasResume(false);
|
||||
setHasFacts(false);
|
||||
}, [setHasJobDescription, setHasResume, setHasFacts]);
|
||||
|
||||
const resetResume = useCallback(() => {
|
||||
setHasResume(false);
|
||||
setHasFacts(false);
|
||||
}, [setHasResume, setHasFacts]);
|
||||
|
||||
const resetFacts = useCallback(() => {
|
||||
setHasFacts(false);
|
||||
}, [setHasFacts]);
|
||||
|
||||
const renderJobDescriptionView = useCallback((sx: SxProps) => {
|
||||
console.log('renderJobDescriptionView');
|
||||
const jobDescriptionQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="What are the key skills necessary for this position?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
const jobDescriptionPreamble: MessageList = [{
|
||||
role: 'info',
|
||||
content: `Once you paste a job description and press **Generate Resume**, the system will perform the following actions:
|
||||
|
||||
1. **RAG**: Collects information from the RAG database relavent to the job description
|
||||
2. **Isolated Analysis**: Three sub-stages
|
||||
1. **Job Analysis**: Extracts requirements from job description only
|
||||
2. **Candidate Analysis**: Catalogs qualifications from resume/context only
|
||||
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
|
||||
3. **Resume Generation**: Uses mapping output to create a tailored resume with evidence-based content
|
||||
4. **Verification**: Performs fact-checking to catch any remaining fabrications
|
||||
1. **Re-generation**: If verification does not pass, a second attempt is made to correct any issues`
|
||||
}];
|
||||
|
||||
|
||||
if (!hasJobDescription) {
|
||||
return <Conversation
|
||||
ref={jobConversationRef}
|
||||
{...{
|
||||
type: "job_description",
|
||||
actionLabel: "Generate Resume",
|
||||
preamble: jobDescriptionPreamble,
|
||||
hidePreamble: true,
|
||||
prompt: "Paste a job description, then click Generate...",
|
||||
multiline: true,
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterJobDescriptionMessages,
|
||||
resetAction: resetJobDescription,
|
||||
onResponse: jobResponse,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
|
||||
} else {
|
||||
return <Conversation
|
||||
ref={jobConversationRef}
|
||||
{...{
|
||||
type: "job_description",
|
||||
actionLabel: "Send",
|
||||
prompt: "Ask a question about this job description...",
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterJobDescriptionMessages,
|
||||
defaultPrompts: jobDescriptionQuestions,
|
||||
resetAction: resetJobDescription,
|
||||
onResponse: jobResponse,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
}, [filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse, resetJobDescription, hasFacts, hasResume, submitQuery]);
|
||||
|
||||
/**
|
||||
* Renders the resume view with loading indicator
|
||||
*/
|
||||
const renderResumeView = useCallback((sx: SxProps) => {
|
||||
const resumeQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="Is this resume a good fit for the provided job description?" tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
if (!hasFacts) {
|
||||
return <Conversation
|
||||
ref={resumeConversationRef}
|
||||
{...{
|
||||
type: "resume",
|
||||
actionLabel: "Fact Check",
|
||||
defaultQuery: "Fact check the resume.",
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterResumeMessages,
|
||||
onResponse: resumeResponse,
|
||||
resetAction: resetResume,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
} else {
|
||||
return <Conversation
|
||||
ref={resumeConversationRef}
|
||||
{...{
|
||||
type: "resume",
|
||||
actionLabel: "Send",
|
||||
prompt: "Ask a question about this job resume...",
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterResumeMessages,
|
||||
onResponse: resumeResponse,
|
||||
resetAction: resetResume,
|
||||
sessionId,
|
||||
setSnack,
|
||||
defaultPrompts: resumeQuestions,
|
||||
submitQuery,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
}, [filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse, resetResume, hasResume, submitQuery]);
|
||||
|
||||
/**
|
||||
* Renders the fact check view
|
||||
*/
|
||||
const renderFactCheckView = useCallback((sx: SxProps) => {
|
||||
const factsQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
return <Conversation
|
||||
ref={factsConversationRef}
|
||||
{...{
|
||||
type: "fact_check",
|
||||
actionLabel: "Send",
|
||||
prompt: "Ask a question about any discrepencies...",
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterFactsMessages,
|
||||
defaultPrompts: factsQuestions,
|
||||
resetAction: resetFacts,
|
||||
onResponse: factsResponse,
|
||||
sessionId,
|
||||
submitQuery,
|
||||
setSnack,
|
||||
sx,
|
||||
}}
|
||||
/>
|
||||
}, [ sessionId, setSnack, factsResponse, filterFactsMessages, resetFacts, hasResume, hasFacts, submitQuery]);
|
||||
|
||||
return (
|
||||
<Box className="ResumeBuilder"
|
||||
sx={{
|
||||
p: 0,
|
||||
m: 0,
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
margin: "0 auto",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#F5F5F5",
|
||||
flexDirection: "column",
|
||||
maxWidth: "1024px",
|
||||
}}
|
||||
>
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{ bgcolor: 'background.paper' }}
|
||||
>
|
||||
<Tab value={0} label="Job Description" />
|
||||
{hasResume && <Tab value={1} label="Resume" />}
|
||||
{hasFacts && <Tab value={2} label="Fact Check" />}
|
||||
</Tabs>
|
||||
|
||||
{/* Document display area */}
|
||||
<Box sx={{
|
||||
display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView({ height: "calc(100vh - 72px - 48px)" })}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
ResumeBuilderPage
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user