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