Looking snazzy
This commit is contained in:
parent
540d286d7a
commit
142c2baac4
@ -4,12 +4,12 @@ First, what works:
|
|||||||
|
|
||||||
1. There are two personas populated:
|
1. There are two personas populated:
|
||||||
1. One is me [jketreno](/u/jketreno)
|
1. One is me [jketreno](/u/jketreno)
|
||||||
2. The other is a ficticious AI generated persona named [Eliz](/u/eliza).
|
2. The other is a ficticious AI generated persona named [Eliza](/u/eliza).
|
||||||
2. **Candidate Skill Chat** You can go to the Chat tab to ask questions about the active candaite.
|
2. **Chat** You can go to the Chat tab to ask questions about the active candaite.
|
||||||
3. **Resume Builder** You can build a resume for a person given a Job Description
|
3. **Resume Builder** You can build a resume for a person given a Job Description
|
||||||
|
|
||||||
What doesn't work:
|
What doesn't work:
|
||||||
|
|
||||||
1. User login, registration, etc.
|
1. User login, registration, etc.
|
||||||
2. Lots of the links on the site.
|
2. Lots of the links on the site.
|
||||||
3. Anything that isn't "Chat", "Resume Builder", or "About".
|
3. Basically.. anything that isn't "Chat", "Resume Builder", or "About".
|
||||||
|
BIN
frontend/public/profile.png
Executable file
BIN
frontend/public/profile.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 226 KiB |
@ -1,43 +1,6 @@
|
|||||||
import React, { useRef, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
import { SessionWrapper } from "./App/SessionWrapper";
|
|
||||||
import { Main } from "./App/Main";
|
|
||||||
import { BackstoryApp } from './NewApp/BackstoryApp';
|
import { BackstoryApp } from './NewApp/BackstoryApp';
|
||||||
import { Snack, SeverityType } from './Components/Snack';
|
|
||||||
|
|
||||||
const PathRouter = ({ setSnack }: { setSnack: any }) => {
|
|
||||||
const location = useLocation();
|
|
||||||
const segments = location.pathname.split("/").filter(Boolean);
|
|
||||||
const sessionId = segments[segments.length - 1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SessionWrapper setSnack={setSnack}>
|
|
||||||
<Main setSnack={setSnack} sessionId={sessionId} />
|
|
||||||
</SessionWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App2() {
|
|
||||||
const snackRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
|
||||||
snackRef.current?.setSnack(message, severity);
|
|
||||||
}, [snackRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Router>
|
|
||||||
<Routes>
|
|
||||||
<Route path="*" element={<PathRouter setSnack={setSnack} />} />
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
<Snack
|
|
||||||
ref={snackRef}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -69,7 +69,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
|||||||
return <pre><code className="JsonRaw">{content}</code></pre>
|
return <pre><code className="JsonRaw">{content}</code></pre>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return <pre><code className={className}>{element.children}</code></pre>;
|
return <pre><code className={className || ''}>{element.children}</code></pre>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
a: {
|
a: {
|
||||||
@ -81,7 +81,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
|||||||
if (href) {
|
if (href) {
|
||||||
if (href.match(/^\//)) {
|
if (href.match(/^\//)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.history.replaceState({}, '', `${href}/${sessionId}`);
|
window.history.replaceState({}, '', `${href}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
257
frontend/src/NewApp/BackstoryApp.css
Normal file
257
frontend/src/NewApp/BackstoryApp.css
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-container #scene {
|
||||||
|
top: 0px !important;
|
||||||
|
left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 3px solid #E0E0E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
overflow-wrap: initial;
|
||||||
|
word-break: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabPanel {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiToolbar-root .MuiBox-root {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiTabs-root .MuiTabs-indicator {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SystemInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SystemInfoItem {
|
||||||
|
display: flex; /* Grid for individual items */
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SystemInfoItem > div:first-child {
|
||||||
|
display: flex;
|
||||||
|
justify-self: end; /* Align the first column content to the right */
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SystemInfoItem > div:last-child {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-self: end; /* Align the first column content to the right */
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 2048px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Controls {
|
||||||
|
display: flex;
|
||||||
|
background-color: #F5F5F5;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: visible;
|
||||||
|
min-width: 10rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageContent div > p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MenuCard.MuiCard-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 10rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: #1A2536; /* Midnight Blue */
|
||||||
|
color: #D3CDBF; /* Warm Gray */
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MenuCard.MuiCard-root button {
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
/* Prevent toolbar from shrinking vertically when media < 600px */
|
||||||
|
.MuiToolbar-root {
|
||||||
|
min-height: 72px !important;
|
||||||
|
padding-left: 16px !important;
|
||||||
|
padding-right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 1024px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #D3CDBF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message.MuiCard-root {
|
||||||
|
background-color: #DCF8C6;
|
||||||
|
border: 1px solid #B2E0A7;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
min-width: 80%;
|
||||||
|
max-width: 80%;
|
||||||
|
justify-self: right;
|
||||||
|
display: flex;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: self-end;
|
||||||
|
align-self: end;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.About.MuiCard-root,
|
||||||
|
.assistant-message.MuiCard-root {
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
min-width: 70%;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
justify-self: left;
|
||||||
|
display: flex;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 16px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.About.MuiCard-root {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.About .MuiCardContent-root,
|
||||||
|
.assistant-message .MuiCardContent-root {
|
||||||
|
padding: 0 16px !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.About span,
|
||||||
|
.assistant-message span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .MuiCardContent-root:last-child,
|
||||||
|
.assistant-message .MuiCardContent-root:last-child,
|
||||||
|
.About .MuiCardContent-root:last-child {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users > div {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce general whitespace in markdown content */
|
||||||
|
* p.MuiTypography-root {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce space between headings and content */
|
||||||
|
* h1.MuiTypography-root,
|
||||||
|
* h2.MuiTypography-root,
|
||||||
|
* h3.MuiTypography-root,
|
||||||
|
* h4.MuiTypography-root,
|
||||||
|
* h5.MuiTypography-root,
|
||||||
|
* h6.MuiTypography-root {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce space in lists */
|
||||||
|
* ul.MuiTypography-root,
|
||||||
|
* ol.MuiTypography-root {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* li.MuiTypography-root {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* .MuiTypography-root li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce space around code blocks */
|
||||||
|
* .MuiTypography-root pre {
|
||||||
|
border: 1px solid #F5F5F5;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PromptStats .MuiTableCell-root {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#SystemPromptInput {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
@ -28,19 +28,21 @@ import { ConversationHandle } from '../Components/Conversation';
|
|||||||
|
|
||||||
|
|
||||||
import { HomePage } from './Pages/HomePage';
|
import { HomePage } from './Pages/HomePage';
|
||||||
import { HomePage as ChatPage } from '../Pages/HomePage';
|
import { ChatPage } from './Pages/ChatPage';
|
||||||
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
|
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
|
||||||
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
|
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
|
||||||
import { AboutPage } from './Pages/AboutPage';
|
import { AboutPage } from './Pages/AboutPage';
|
||||||
import { BetaPage } from './Pages/BetaPage';
|
import { BetaPage } from './Pages/BetaPage';
|
||||||
import { CreateProfilePage } from './Pages/CreateProfilePage';
|
import { CreateProfilePage } from './Pages/CreateProfilePage';
|
||||||
|
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
|
||||||
|
|
||||||
|
import './BackstoryApp.css';
|
||||||
import '@fontsource/roboto/300.css';
|
import '@fontsource/roboto/300.css';
|
||||||
import '@fontsource/roboto/400.css';
|
import '@fontsource/roboto/400.css';
|
||||||
import '@fontsource/roboto/500.css';
|
import '@fontsource/roboto/500.css';
|
||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
|
|
||||||
import { calculatePoint } from 'mermaid/dist/utils';
|
import { connectionBase } from '../Global';
|
||||||
|
|
||||||
type NavigationLinkType = {
|
type NavigationLinkType = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -153,6 +155,20 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
|||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cookie handling functions
|
||||||
|
const getCookie = (name: string) => {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop()?.split(';').shift();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCookie = (name: string, value: string, days = 7) => {
|
||||||
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||||
|
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
|
||||||
|
};
|
||||||
|
|
||||||
const BackstoryApp = () => {
|
const BackstoryApp = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -161,6 +177,7 @@ const BackstoryApp = () => {
|
|||||||
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
||||||
const snackRef = useRef<any>(null);
|
const snackRef = useRef<any>(null);
|
||||||
const chatRef = useRef<ConversationHandle>(null);
|
const chatRef = useRef<ConversationHandle>(null);
|
||||||
|
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||||
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
||||||
snackRef.current?.setSnack(message, severity);
|
snackRef.current?.setSnack(message, severity);
|
||||||
}, [snackRef]);
|
}, [snackRef]);
|
||||||
@ -170,6 +187,79 @@ const BackstoryApp = () => {
|
|||||||
navigate('/chat');
|
navigate('/chat');
|
||||||
};
|
};
|
||||||
const [page, setPage] = useState<string>("");
|
const [page, setPage] = useState<string>("");
|
||||||
|
const [storeInCookie, setStoreInCookie] = useState(true);
|
||||||
|
|
||||||
|
// Extract session ID from URL query parameter or cookie
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const urlSessionId = urlParams.get('id');
|
||||||
|
const cookieSessionId = getCookie('session_id');
|
||||||
|
|
||||||
|
// Fetch or join session on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSession = async () => {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
let newSessionId;
|
||||||
|
if (urlSessionId) {
|
||||||
|
// Attempt to join session from URL
|
||||||
|
response = await fetch(`${connectionBase}/join-session/${urlSessionId}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Session not found');
|
||||||
|
}
|
||||||
|
newSessionId = (await response.json()).id;
|
||||||
|
} else if (cookieSessionId) {
|
||||||
|
// Attempt to join session from cookie
|
||||||
|
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
// Cookie session invalid, create new session
|
||||||
|
response = await fetch(`${connectionBase}/api/create-session`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create session');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newSessionId = (await response.json()).id;
|
||||||
|
} else {
|
||||||
|
// Create a new session
|
||||||
|
response = await fetch(`${connectionBase}/api/create-session`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create session');
|
||||||
|
}
|
||||||
|
newSessionId = (await response.json()).id;
|
||||||
|
}
|
||||||
|
setSessionId(newSessionId);
|
||||||
|
// Store in cookie if user opts in
|
||||||
|
if (storeInCookie) {
|
||||||
|
setCookie('session_id', newSessionId);
|
||||||
|
}
|
||||||
|
// Update URL without reloading
|
||||||
|
if (!storeInCookie || (urlSessionId && urlSessionId !== newSessionId)) {
|
||||||
|
window.history.replaceState(null, '', `?id=${newSessionId}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setSnack("" + err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyLink = () => {
|
||||||
|
const link = `${window.location.origin}${window.location.pathname}?id=${sessionId}`;
|
||||||
|
navigator.clipboard.writeText(link).then(() => {
|
||||||
|
alert('Link copied to clipboard!');
|
||||||
|
}).catch(() => {
|
||||||
|
alert('Failed to copy link');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
||||||
@ -193,42 +283,44 @@ const BackstoryApp = () => {
|
|||||||
minHeight: "calc(100vh - 72px)",
|
minHeight: "calc(100vh - 72px)",
|
||||||
}}>
|
}}>
|
||||||
<BackstoryPageContainer userContext={userContext}>
|
<BackstoryPageContainer userContext={userContext}>
|
||||||
<Routes>
|
{sessionId !== undefined &&
|
||||||
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
|
<Routes>
|
||||||
<Route path="/about" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
|
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
||||||
<Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
|
<Route path="/about" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
||||||
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
|
<Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
||||||
<Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery} />} />
|
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
||||||
|
<Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
||||||
<Route path="/create-your-profile" element={<CreateProfilePage />} />
|
<Route path="/create-your-profile" element={<CreateProfilePage />} />
|
||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
<Route path="/dashboard" element={<DashboardPage />} />
|
||||||
<Route path="/" element={<HomePage/>}/>
|
<Route path="/" element={<HomePage />} />
|
||||||
{/* Candidate-specific routes */}
|
{/* Candidate-specific routes */}
|
||||||
{user.type === 'candidate' && (
|
{user.type === 'candidate' && (
|
||||||
<>
|
<>
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
<Route path="/backstory" element={<BackstoryPage />} />
|
<Route path="/backstory" element={<BackstoryPage />} />
|
||||||
<Route path="/resumes" element={<ResumesPage />} />
|
<Route path="/resumes" element={<ResumesPage />} />
|
||||||
<Route path="/qa-setup" element={<QASetupPage />} />
|
<Route path="/qa-setup" element={<QASetupPage />} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Employer-specific routes */}
|
{/* Employer-specific routes */}
|
||||||
{user.type === 'employer' && (
|
{user.type === 'employer' && (
|
||||||
<>
|
<>
|
||||||
<Route path="/search" element={<SearchPage />} />
|
<Route path="/search" element={<SearchPage />} />
|
||||||
<Route path="/saved" element={<SavedPage />} />
|
<Route path="/saved" element={<SavedPage />} />
|
||||||
<Route path="/jobs" element={<JobsPage />} />
|
<Route path="/jobs" element={<JobsPage />} />
|
||||||
<Route path="/company" element={<CompanyPage />} />
|
<Route path="/company" element={<CompanyPage />} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Common routes */}
|
{/* Common routes */}
|
||||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
|
|
||||||
{/* Redirect to BETA by default */}
|
{/* Redirect to BETA by default */}
|
||||||
<Route path="*" element={<BetaPage />} />
|
<Route path="*" element={<BetaPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
}
|
||||||
{location.pathname === "/" && <Footer />}
|
{location.pathname === "/" && <Footer />}
|
||||||
</BackstoryPageContainer>
|
</BackstoryPageContainer>
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
|
84
frontend/src/NewApp/Components/CandidateInfo.tsx
Normal file
84
frontend/src/NewApp/Components/CandidateInfo.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography, Avatar, Paper, Grid, Chip } from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { Tunables } from '../../Components/ChatQuery';
|
||||||
|
|
||||||
|
// Define the UserInfo interface for type safety
|
||||||
|
interface UserInfo {
|
||||||
|
profile_url: string;
|
||||||
|
description: string;
|
||||||
|
rag_content_size: number;
|
||||||
|
user_name: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
full_name: string;
|
||||||
|
contact_info: Record<string, string>;
|
||||||
|
questions: [{
|
||||||
|
question: string;
|
||||||
|
tunables?: Tunables
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define props interface for the component
|
||||||
|
interface CandidateInfoProps {
|
||||||
|
userInfo: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styled components
|
||||||
|
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CandidateInfo: React.FC<CandidateInfoProps> = ({ userInfo }) => {
|
||||||
|
// Format RAG content size (e.g., if it's in bytes, convert to KB/MB)
|
||||||
|
const formatRagSize = (size: number): string => {
|
||||||
|
if (size < 1000) return `${size} RAG elements`;
|
||||||
|
if (size < 1000000) return `${(size / 1000).toFixed(1)}K RAG elements`;
|
||||||
|
return `${(size / 1000000).toFixed(1)}M RAG elements`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledPaper>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Avatar
|
||||||
|
src={userInfo.profile_url}
|
||||||
|
alt={`${userInfo.full_name}'s profile`}
|
||||||
|
sx={{
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
border: '2px solid #e0e0e0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, sm: 10 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||||||
|
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold' }}>
|
||||||
|
{userInfo.full_name}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={formatRagSize(userInfo.rag_content_size)}
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
sx={{ ml: 2 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
{userInfo.description}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</StyledPaper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
UserInfo
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CandidateInfo };
|
57
frontend/src/NewApp/Components/Document.tsx
Normal file
57
frontend/src/NewApp/Components/Document.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import { Message } from '../../Components/Message';
|
||||||
|
import { ChatBubble } from '../../Components/ChatBubble';
|
||||||
|
import { BackstoryElementProps } from '../../Components/BackstoryTab';
|
||||||
|
import { StyledMarkdown } from '../../Components/StyledMarkdown';
|
||||||
|
|
||||||
|
interface DocumentProps extends BackstoryElementProps {
|
||||||
|
filepath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Document = (props: DocumentProps) => {
|
||||||
|
const { sessionId, setSnack, submitQuery, filepath } = props;
|
||||||
|
const backstoryProps = {
|
||||||
|
submitQuery,
|
||||||
|
setSnack,
|
||||||
|
sessionId
|
||||||
|
};
|
||||||
|
|
||||||
|
const [document, setDocument] = useState<string>("");
|
||||||
|
|
||||||
|
// Get the markdown
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filepath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fetchDocument = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(filepath, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error(`${filepath} not found.`);
|
||||||
|
}
|
||||||
|
const data = await response.text();
|
||||||
|
setDocument(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error obtaining About content information:', error);
|
||||||
|
setDocument(`${filepath} not found.`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDocument();
|
||||||
|
}, [document, setDocument, filepath])
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<StyledMarkdown {...backstoryProps} content={document}/>
|
||||||
|
</>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Document
|
||||||
|
};
|
68
frontend/src/NewApp/Components/LoadingComponent.tsx
Normal file
68
frontend/src/NewApp/Components/LoadingComponent.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, CircularProgress, Typography, Grid, LinearProgress, Fade } from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
|
// Types for props
|
||||||
|
interface LoadingComponentProps {
|
||||||
|
/** Text to display while loading */
|
||||||
|
loadingText?: string;
|
||||||
|
/** Type of loader to show */
|
||||||
|
loaderType?: 'circular' | 'linear';
|
||||||
|
/** Whether to show with fade-in animation */
|
||||||
|
withFade?: boolean;
|
||||||
|
/** Duration of fade-in animation in ms */
|
||||||
|
fadeDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styled components
|
||||||
|
const LoadingContainer = styled(Box)(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A loading component to display at the top of pages while content is loading
|
||||||
|
*/
|
||||||
|
const LoadingComponent: React.FC<LoadingComponentProps> = ({
|
||||||
|
loadingText = 'Loading content...',
|
||||||
|
loaderType = 'circular',
|
||||||
|
withFade = true,
|
||||||
|
fadeDuration = 800,
|
||||||
|
}) => {
|
||||||
|
const content = (
|
||||||
|
<LoadingContainer>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
|
||||||
|
{loaderType === 'circular' ? (
|
||||||
|
<CircularProgress color="primary" />
|
||||||
|
) : (
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<LinearProgress color="primary" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12 }} sx={{ textAlign: 'center' }}>
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
{loadingText}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</LoadingContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return with or without fade animation
|
||||||
|
return withFade ? (
|
||||||
|
<Fade in={true} timeout={fadeDuration}>
|
||||||
|
{content}
|
||||||
|
</Fade>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LoadingComponent};
|
@ -1,7 +1,37 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
||||||
import { FileText, Code, Layers, Layout, Activity, Palette, Menu, X } from 'lucide-react';
|
import {
|
||||||
import { Document } from '../../Components/Document';
|
Box,
|
||||||
|
Drawer,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardActionArea,
|
||||||
|
Divider,
|
||||||
|
useTheme,
|
||||||
|
useMediaQuery
|
||||||
|
} from '@mui/material';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
|
import CodeIcon from '@mui/icons-material/Code';
|
||||||
|
import LayersIcon from '@mui/icons-material/Layers';
|
||||||
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
|
import PaletteIcon from '@mui/icons-material/Palette';
|
||||||
|
import AnalyticsIcon from '@mui/icons-material/Analytics';
|
||||||
|
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
|
||||||
|
|
||||||
|
import { Document } from '../Components/Document';
|
||||||
import { BackstoryPageProps } from '../../Components/BackstoryTab';
|
import { BackstoryPageProps } from '../../Components/BackstoryTab';
|
||||||
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
|
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
|
||||||
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
|
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
|
||||||
@ -12,29 +42,31 @@ import { MockupPage } from './MockupPage';
|
|||||||
const getDocumentIcon = (title: string) => {
|
const getDocumentIcon = (title: string) => {
|
||||||
switch (title) {
|
switch (title) {
|
||||||
case 'About':
|
case 'About':
|
||||||
return <FileText className="w-5 h-5" />;
|
return <DescriptionIcon />;
|
||||||
case 'BETA':
|
case 'BETA':
|
||||||
return <Code className="w-5 h-5" />;
|
return <CodeIcon />;
|
||||||
case 'Resume Generation Architecture':
|
case 'Resume Generation Architecture':
|
||||||
case 'Application Architecture':
|
case 'Application Architecture':
|
||||||
return <Layers className="w-5 h-5" />;
|
return <LayersIcon />;
|
||||||
case 'UI Overview':
|
case 'UI Overview':
|
||||||
case 'UI Mockup':
|
case 'UI Mockup':
|
||||||
return <Layout className="w-5 h-5" />;
|
return <DashboardIcon />;
|
||||||
case 'Theme Visualizer':
|
case 'Theme Visualizer':
|
||||||
return <Palette className="w-5 h-5" />;
|
return <PaletteIcon />;
|
||||||
case 'App Analysis':
|
case 'App Analysis':
|
||||||
return <Activity className="w-5 h-5" />;
|
return <AnalyticsIcon />;
|
||||||
default:
|
default:
|
||||||
return <FileText className="w-5 h-5" />;
|
return <ViewQuiltIcon />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sidebar navigation component with improved styling
|
// Sidebar navigation component using MUI components
|
||||||
const Sidebar: React.FC<{
|
const Sidebar: React.FC<{
|
||||||
currentPage: string;
|
currentPage: string;
|
||||||
onDocumentSelect: (docName: string, open: boolean) => void;
|
onDocumentSelect: (docName: string, open: boolean) => void;
|
||||||
}> = ({ currentPage, onDocumentSelect }) => {
|
onClose?: () => void;
|
||||||
|
isMobile: boolean;
|
||||||
|
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
|
||||||
// Document definitions
|
// Document definitions
|
||||||
const documents = [
|
const documents = [
|
||||||
{ title: "About", route: "about" },
|
{ title: "About", route: "about" },
|
||||||
@ -47,30 +79,73 @@ const Sidebar: React.FC<{
|
|||||||
{ title: "UI Mockup", route: "ui-mockup" }
|
{ title: "UI Mockup", route: "ui-mockup" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleItemClick = (route: string) => {
|
||||||
|
onDocumentSelect(route, true);
|
||||||
|
if (isMobile && onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-md p-3 h-full">
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
<h2 className="text-xl font-bold px-2 py-3 border-b mb-4">Documentation</h2>
|
<Box sx={{
|
||||||
<nav>
|
p: 2,
|
||||||
<ul className="space-y-1">
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider'
|
||||||
|
}}>
|
||||||
|
<Typography variant="h6" component="h2" fontWeight="bold">
|
||||||
|
Documentation
|
||||||
|
</Typography>
|
||||||
|
{isMobile && onClose && (
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
size="small"
|
||||||
|
aria-label="Close navigation"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
p: 1
|
||||||
|
}}>
|
||||||
|
<List>
|
||||||
{documents.map((doc, index) => (
|
{documents.map((doc, index) => (
|
||||||
<li key={index}>
|
<ListItem key={index} disablePadding>
|
||||||
<button
|
<ListItemButton
|
||||||
onClick={() => onDocumentSelect(doc.route, true)}
|
onClick={() => handleItemClick(doc.route)}
|
||||||
className={`w-full text-left px-4 py-3 rounded-md flex items-center transition-colors ${currentPage === doc.route
|
selected={currentPage === doc.route}
|
||||||
? 'bg-blue-50 text-blue-700 font-medium shadow-sm'
|
sx={{
|
||||||
: 'text-gray-700 hover:bg-gray-50'
|
borderRadius: 1,
|
||||||
}`}
|
mb: 0.5
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className={`mr-3 ${currentPage === doc.route ? 'text-blue-600' : 'text-gray-500'}`}>
|
<ListItemIcon sx={{
|
||||||
|
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
|
||||||
|
minWidth: 40
|
||||||
|
}}>
|
||||||
{getDocumentIcon(doc.title)}
|
{getDocumentIcon(doc.title)}
|
||||||
</span>
|
</ListItemIcon>
|
||||||
<span className="text-base">{doc.title}</span>
|
<ListItemText
|
||||||
</button>
|
primary={doc.title}
|
||||||
</li>
|
slotProps={{
|
||||||
|
primary: {
|
||||||
|
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</List>
|
||||||
</nav>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,7 +156,10 @@ const AboutPage = (props: BackstoryPageProps) => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { paramPage = '' } = useParams();
|
const { paramPage = '' } = useParams();
|
||||||
const [page, setPage] = useState<string>(paramPage);
|
const [page, setPage] = useState<string>(paramPage);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
// Track location changes
|
// Track location changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -93,6 +171,13 @@ const AboutPage = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
|
// Close drawer when changing to desktop view
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile) {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
}
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
// Handle document navigation
|
// Handle document navigation
|
||||||
const onDocumentExpand = (docName: string, open: boolean) => {
|
const onDocumentExpand = (docName: string, open: boolean) => {
|
||||||
console.log("Document expanded:", { docName, open, location });
|
console.log("Document expanded:", { docName, open, location });
|
||||||
@ -110,170 +195,227 @@ const AboutPage = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle sidebar on mobile
|
// Toggle mobile drawer
|
||||||
const toggleSidebar = () => {
|
const toggleDrawer = () => {
|
||||||
setSidebarOpen(!sidebarOpen);
|
setDrawerOpen(!drawerOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Close the drawer
|
||||||
<div className="flex flex-col h-full">
|
const closeDrawer = () => {
|
||||||
{/* Mobile Toggle Button */}
|
setDrawerOpen(false);
|
||||||
<button
|
};
|
||||||
className="md:hidden flex items-center justify-center gap-2 mx-auto mb-4 px-4 py-2 bg-white rounded-md shadow-sm border text-gray-700 hover:bg-gray-50"
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
>
|
|
||||||
{sidebarOpen ? (
|
|
||||||
<>
|
|
||||||
<X size={18} />
|
|
||||||
<span>Hide Navigation</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Menu size={18} />
|
|
||||||
<span>Show Navigation</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-5 h-full">
|
// Helper function to get document title from route
|
||||||
{/* Sidebar - hidden on mobile when closed */}
|
function documentTitleFromRoute(route: string): string {
|
||||||
{sidebarOpen && (
|
const titles: Record<string, string> = {
|
||||||
<div className="w-full md:w-64 flex-shrink-0 mb-4 md:mb-0">
|
'about': 'About',
|
||||||
|
'beta': 'BETA',
|
||||||
|
'resume-generation': 'Resume Generation Architecture',
|
||||||
|
'about-app': 'Application Architecture',
|
||||||
|
'ui-overview': 'UI Overview',
|
||||||
|
'theme-visualizer': 'Theme Visualizer',
|
||||||
|
'app-analysis': 'App Analysis',
|
||||||
|
'ui-mockup': 'UI Mockup'
|
||||||
|
};
|
||||||
|
|
||||||
|
return titles[route] || 'Documentation';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocViewProps {
|
||||||
|
page: string
|
||||||
|
};
|
||||||
|
const DocView = (props: DocViewProps) => {
|
||||||
|
const { page } = props;
|
||||||
|
const title = documentTitleFromRoute(page);
|
||||||
|
const icon = getDocumentIcon(title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}>
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</Box>
|
||||||
|
<Document
|
||||||
|
filepath={`/docs/${page}.md`}
|
||||||
|
sessionId={sessionId}
|
||||||
|
submitQuery={submitQuery}
|
||||||
|
setSnack={setSnack}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render the appropriate content based on current page
|
||||||
|
function renderContent() {
|
||||||
|
switch (page) {
|
||||||
|
case 'about':
|
||||||
|
return <DocView page={page} />
|
||||||
|
case 'beta':
|
||||||
|
return <DocView page={page} />
|
||||||
|
case 'resume-generation':
|
||||||
|
return <DocView page={page} />
|
||||||
|
case 'about-app':
|
||||||
|
return <DocView page={page} />
|
||||||
|
case 'ui-overview':
|
||||||
|
return (<BackstoryUIOverviewPage />);
|
||||||
|
case 'theme-visualizer':
|
||||||
|
return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
|
||||||
|
case 'app-analysis':
|
||||||
|
return (<BackstoryAppAnalysisPage />);
|
||||||
|
case 'ui-mockup':
|
||||||
|
return (<MockupPage />);
|
||||||
|
default:
|
||||||
|
// Document grid for landing page
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3 }} elevation={1}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
Documentation
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" paragraph>
|
||||||
|
Select a document from the sidebar to view detailed technical information about the application.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{[
|
||||||
|
{ title: "About", route: "about", description: "General information about the application and its purpose" },
|
||||||
|
{ title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features" },
|
||||||
|
{ title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated" },
|
||||||
|
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information" },
|
||||||
|
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions" },
|
||||||
|
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles" },
|
||||||
|
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application" },
|
||||||
|
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts" }
|
||||||
|
].map((doc, index) => (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||||
|
<Card>
|
||||||
|
<CardActionArea onClick={() => onDocumentExpand(doc.route, true)}>
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||||
|
<Box sx={{ color: 'primary.main', mr: 1.5 }}>
|
||||||
|
{getDocumentIcon(doc.title)}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h6">{doc.title}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
|
||||||
|
{doc.description}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate drawer width
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', height: '100%' }}>
|
||||||
|
{/* Mobile App Bar */}
|
||||||
|
{isMobile && (
|
||||||
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
sx={{
|
||||||
|
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||||
|
ml: { sm: `${drawerWidth}px` },
|
||||||
|
display: { md: 'none' }
|
||||||
|
}}
|
||||||
|
elevation={0}
|
||||||
|
color="default"
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
aria-label="open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}>
|
||||||
|
{page ? documentTitleFromRoute(page) : "Documentation"}
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation drawer */}
|
||||||
|
<Box
|
||||||
|
component="nav"
|
||||||
|
sx={{
|
||||||
|
width: { md: drawerWidth },
|
||||||
|
flexShrink: { md: 0 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Mobile drawer (temporary) */}
|
||||||
|
{isMobile ? (
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' },
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: drawerWidth
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
onDocumentSelect={onDocumentExpand}
|
onDocumentSelect={onDocumentExpand}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
isMobile={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Drawer>
|
||||||
|
) : (
|
||||||
|
// Desktop drawer (permanent)
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: drawerWidth,
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
currentPage={page}
|
||||||
|
onDocumentSelect={onDocumentExpand}
|
||||||
|
isMobile={false}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main content */}
|
||||||
<div className="flex-grow">
|
<Box
|
||||||
{page === 'about' && (
|
component="main"
|
||||||
<Document
|
sx={{
|
||||||
title="About"
|
flexGrow: 1,
|
||||||
filepath="/docs/about.md"
|
p: 3,
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('about', open); }}
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||||
expanded={page === 'about'}
|
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
|
||||||
sessionId={sessionId}
|
height: '100%',
|
||||||
submitQuery={submitQuery}
|
overflow: 'auto'
|
||||||
setSnack={setSnack}
|
}}
|
||||||
/>
|
>
|
||||||
)}
|
{renderContent()}
|
||||||
{page === 'beta' && (
|
</Box>
|
||||||
<Document
|
</Box>
|
||||||
title="BETA"
|
|
||||||
filepath="/docs/beta.md"
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('beta', open); }}
|
|
||||||
expanded={page === 'beta'}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === 'resume-generation' && (
|
|
||||||
<Document
|
|
||||||
title="Resume Generation Architecture"
|
|
||||||
filepath="/docs/resume-generation.md"
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('resume-generation', open); }}
|
|
||||||
expanded={page === 'resume-generation'}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === 'about-app' && (
|
|
||||||
<Document
|
|
||||||
title="Application Architecture"
|
|
||||||
filepath="/docs/about-app.md"
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('about-app', open); }}
|
|
||||||
expanded={page === 'about-app'}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === 'ui-overview' && (
|
|
||||||
<Document
|
|
||||||
title="UI Overview"
|
|
||||||
children={<BackstoryUIOverviewPage />}
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('ui-overview', open); }}
|
|
||||||
expanded={page === 'ui-overview'}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === 'theme-visualizer' && (
|
|
||||||
<Document
|
|
||||||
title="Theme Visualizer"
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('theme-visualizer', open); }}
|
|
||||||
expanded={page === 'theme-visualizer'}
|
|
||||||
children={<BackstoryThemeVisualizerPage />}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === 'app-analysis' && (
|
|
||||||
<Document
|
|
||||||
title="App Analysis"
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('app-analysis', open); }}
|
|
||||||
expanded={page === 'app-analysis'}
|
|
||||||
children={<BackstoryAppAnalysisPage />}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === 'ui-mockup' && (
|
|
||||||
<Document
|
|
||||||
title="UI Mockup"
|
|
||||||
onExpand={(open: boolean) => { onDocumentExpand('ui-mockup', open); }}
|
|
||||||
expanded={page === 'ui-mockup'}
|
|
||||||
children={<MockupPage />}
|
|
||||||
sessionId={sessionId}
|
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show welcome grid if no specific page is selected */}
|
|
||||||
{!page && (
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
|
||||||
<h1 className="text-2xl font-bold mb-3">Documentation</h1>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
Select a document from the sidebar to view detailed technical information about the application.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{[
|
|
||||||
{ title: "About", route: "about", description: "General information about the application and its purpose" },
|
|
||||||
{ title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features" },
|
|
||||||
{ title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated" },
|
|
||||||
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information" },
|
|
||||||
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions" },
|
|
||||||
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles" },
|
|
||||||
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application" },
|
|
||||||
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts" }
|
|
||||||
].map((doc, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => onDocumentExpand(doc.route, true)}
|
|
||||||
className="text-left p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-center mb-2">
|
|
||||||
<span className="text-blue-600 mr-3">{getDocumentIcon(doc.title)}</span>
|
|
||||||
<span className="font-medium">{doc.title}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500 ml-8">{doc.description}</p>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { backstoryTheme } from '../BackstoryTheme';
|
import { backstoryTheme } from '../BackstoryTheme';
|
||||||
|
import { Box, Typography, Paper, Container } from '@mui/material';
|
||||||
|
|
||||||
// This component provides a visual demonstration of the theme colors
|
// This component provides a visual demonstration of the theme colors
|
||||||
const BackstoryThemeVisualizerPage = () => {
|
const BackstoryThemeVisualizerPage = () => {
|
||||||
@ -15,6 +16,10 @@ const BackstoryThemeVisualizerPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box sx={{ backgroundColor: 'background.default', minHeight: '100vh', py: 4 }}>
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<Paper sx={{ p: 4, boxShadow: 2 }}>
|
||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<h1 className="text-2xl font-bold mb-6" style={{ color: backstoryTheme.palette.text.primary }}>
|
<h1 className="text-2xl font-bold mb-6" style={{ color: backstoryTheme.palette.text.primary }}>
|
||||||
Backstory Theme Visualization
|
Backstory Theme Visualization
|
||||||
@ -188,6 +193,7 @@ const BackstoryThemeVisualizerPage = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Paper></Container></Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
263
frontend/src/NewApp/Pages/BetaPage.tsx
Normal file
263
frontend/src/NewApp/Pages/BetaPage.tsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Button,
|
||||||
|
useMediaQuery,
|
||||||
|
alpha,
|
||||||
|
GlobalStyles
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import ConstructionIcon from '@mui/icons-material/Construction';
|
||||||
|
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
|
||||||
|
|
||||||
|
interface BetaPageProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
returnPath?: string;
|
||||||
|
returnLabel?: string;
|
||||||
|
onReturn?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BetaPage: React.FC<BetaPageProps> = ({
|
||||||
|
children,
|
||||||
|
title = "Coming Soon",
|
||||||
|
subtitle = "This page is currently in development",
|
||||||
|
returnPath = "/",
|
||||||
|
returnLabel = "Return to Dashboard",
|
||||||
|
onReturn,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [showSparkle, setShowSparkle] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Enhanced sparkle effect for background elements
|
||||||
|
const [sparkles, setSparkles] = useState<Array<{
|
||||||
|
id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
size: number;
|
||||||
|
opacity: number;
|
||||||
|
duration: number;
|
||||||
|
delay: number;
|
||||||
|
}>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Generate sparkle elements with random properties
|
||||||
|
const newSparkles = Array.from({ length: 30 }).map((_, index) => ({
|
||||||
|
id: index,
|
||||||
|
x: Math.random() * 100,
|
||||||
|
y: Math.random() * 100,
|
||||||
|
size: 2 + Math.random() * 5,
|
||||||
|
opacity: 0.3 + Math.random() * 0.7,
|
||||||
|
duration: 2 + Math.random() * 4,
|
||||||
|
delay: Math.random() * 3,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSparkles(newSparkles);
|
||||||
|
|
||||||
|
// Show main sparkle effect after a short delay
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShowSparkle(true);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReturn = () => {
|
||||||
|
if (onReturn) {
|
||||||
|
onReturn();
|
||||||
|
} else if (returnPath) {
|
||||||
|
window.location.href = returnPath;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: 'calc(100vh-72px)',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
bgcolor: theme.palette.background.default,
|
||||||
|
pt: 8,
|
||||||
|
pb: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Animated background elements */}
|
||||||
|
<Box sx={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 0, overflow: 'hidden' }}>
|
||||||
|
{sparkles.map((sparkle) => (
|
||||||
|
<Box
|
||||||
|
key={sparkle.id}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${sparkle.x}%`,
|
||||||
|
top: `${sparkle.y}%`,
|
||||||
|
width: sparkle.size,
|
||||||
|
height: sparkle.size,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: alpha(theme.palette.primary.main, sparkle.opacity),
|
||||||
|
boxShadow: `0 0 ${sparkle.size * 2}px ${alpha(theme.palette.primary.main, sparkle.opacity)}`,
|
||||||
|
animation: `float ${sparkle.duration}s ease-in-out ${sparkle.delay}s infinite alternate`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Container maxWidth="lg" sx={{ position: 'relative', zIndex: 2 }}>
|
||||||
|
<Grid container spacing={4} direction="column" alignItems="center">
|
||||||
|
<Grid size={{xs: 12}} sx={{ textAlign: 'center', mb: 2 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
component="h1"
|
||||||
|
gutterBottom
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
|
||||||
|
animation: showSparkle ? 'titleGlow 3s ease-in-out infinite alternate' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
component="h2"
|
||||||
|
color="textSecondary"
|
||||||
|
sx={{ mb: 6 }}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{xs: 12, md: 10, lg: 8}} sx={{ mb: 4 }}>
|
||||||
|
<Paper
|
||||||
|
elevation={8}
|
||||||
|
sx={{
|
||||||
|
p: { xs: 3, md: 5 },
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: alpha(theme.palette.background.paper, 0.8),
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
boxShadow: `0 8px 32px ${alpha(theme.palette.primary.main, 0.15)}`,
|
||||||
|
border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Construction icon */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -15,
|
||||||
|
right: -15,
|
||||||
|
bgcolor: theme.palette.warning.main,
|
||||||
|
color: theme.palette.warning.contrastText,
|
||||||
|
borderRadius: '50%',
|
||||||
|
p: 2,
|
||||||
|
boxShadow: 3,
|
||||||
|
transform: 'rotate(15deg)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ConstructionIcon fontSize="large" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<Box sx={{ mt: 3, mb: 3 }}>
|
||||||
|
{children || (
|
||||||
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<RocketLaunchIcon
|
||||||
|
fontSize="large"
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
fontSize: 80,
|
||||||
|
mb: 2,
|
||||||
|
animation: 'rocketWobble 3s ease-in-out infinite'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>
|
||||||
|
We're working hard to bring you this exciting new feature!
|
||||||
|
</Typography>
|
||||||
|
<Typography color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
Check back soon for updates.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Return button */}
|
||||||
|
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleReturn}
|
||||||
|
sx={{
|
||||||
|
px: 4,
|
||||||
|
py: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: `0 4px 14px ${alpha(theme.palette.primary.main, 0.4)}`,
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: `0 6px 20px ${alpha(theme.palette.primary.main, 0.6)}`,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{returnLabel}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Global styles added with MUI's GlobalStyles component */}
|
||||||
|
<GlobalStyles
|
||||||
|
styles={{
|
||||||
|
'@keyframes float': {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateY(0) scale(1)',
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateY(-20px) scale(1.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@keyframes sparkleFloat': {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateY(0) scale(1)',
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
'50%': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateY(-15px) scale(1.2)',
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@keyframes titleGlow': {
|
||||||
|
'0%': {
|
||||||
|
textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
textShadow: `0 0 25px ${alpha(theme.palette.primary.main, 0.7)}, 0 0 40px ${alpha(theme.palette.primary.main, 0.4)}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@keyframes rocketWobble': {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateY(0) rotate(0deg)',
|
||||||
|
},
|
||||||
|
'50%': {
|
||||||
|
transform: 'translateY(-10px) rotate(3deg)',
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateY(0) rotate(-2deg)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
25
frontend/src/NewApp/Pages/BetaSample.tsx
Normal file
25
frontend/src/NewApp/Pages/BetaSample.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { BetaPage } from './BetaPage';
|
||||||
|
|
||||||
|
const MyIncompletePage = () => {
|
||||||
|
return (
|
||||||
|
<BetaPage
|
||||||
|
title="Analytics Dashboard"
|
||||||
|
subtitle="Our powerful analytics tools are coming soon"
|
||||||
|
returnLabel="Back to Home"
|
||||||
|
returnPath="/home"
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
We're building a comprehensive analytics dashboard that will provide real-time insights
|
||||||
|
into your business performance. The expected completion date is June 15, 2025.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" sx={{ mt: 2 }}>
|
||||||
|
Features will include custom reports, data visualization, and export capabilities.
|
||||||
|
</Typography>
|
||||||
|
</BetaPage>
|
||||||
|
);
|
||||||
|
};
|
103
frontend/src/NewApp/Pages/ChatPage.tsx
Normal file
103
frontend/src/NewApp/Pages/ChatPage.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, { forwardRef, useEffect, useState } 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 '../../Components//BackstoryTab';
|
||||||
|
import { Conversation, ConversationHandle } from '../../Components/Conversation';
|
||||||
|
import { ChatQuery, Tunables } from '../../Components/ChatQuery';
|
||||||
|
import { MessageList } from '../../Components/Message';
|
||||||
|
import { CandidateInfo, UserInfo } from 'NewApp/Components/CandidateInfo';
|
||||||
|
import { connectionBase } from '../../Global';
|
||||||
|
import { LoadingComponent } from 'NewApp/Components/LoadingComponent';
|
||||||
|
|
||||||
|
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||||
|
const { sessionId, setSnack, submitQuery } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [preamble, setPreamble] = useState<MessageList>([]);
|
||||||
|
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
||||||
|
const [user, setUser] = useState<UserInfo | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreamble([{
|
||||||
|
role: 'system',
|
||||||
|
disableCopy: true,
|
||||||
|
content: `
|
||||||
|
What would you like to know about ${user.first_name}?
|
||||||
|
`,
|
||||||
|
}]);
|
||||||
|
|
||||||
|
setQuestions([
|
||||||
|
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||||
|
{user.questions.map(({ question, tunables }, i: number) =>
|
||||||
|
<ChatQuery key={i} query={{ prompt: question, tunables: tunables }} submitQuery={submitQuery} />
|
||||||
|
)}
|
||||||
|
</Box>,
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<MuiMarkdown>
|
||||||
|
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${user.full_name}** if you have any questions.`}
|
||||||
|
</MuiMarkdown>
|
||||||
|
</Box>]);
|
||||||
|
}, [user, isMobile, submitQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(connectionBase + `/api/user/${sessionId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setUser(data);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error getting user info:', error);
|
||||||
|
setSnack("Unable to obtain user information.", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchUserInfo();
|
||||||
|
}, [setSnack, sessionId]);
|
||||||
|
|
||||||
|
if (sessionId === undefined || user === undefined) {
|
||||||
|
return <LoadingComponent
|
||||||
|
loadingText="Fetching user information..."
|
||||||
|
loaderType="linear"
|
||||||
|
withFade={true}
|
||||||
|
fadeDuration={1200} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<CandidateInfo userInfo={user}/>
|
||||||
|
<Conversation
|
||||||
|
ref={ref}
|
||||||
|
{...{
|
||||||
|
multiline: true,
|
||||||
|
type: "chat",
|
||||||
|
placeholder: `What would you like to know about ${user.first_name}?`,
|
||||||
|
resetLabel: "chat",
|
||||||
|
sessionId,
|
||||||
|
setSnack,
|
||||||
|
// preamble: preamble,
|
||||||
|
defaultPrompts: questions,
|
||||||
|
submitQuery,
|
||||||
|
}}/>
|
||||||
|
</Box>);
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChatPage
|
||||||
|
};
|
381
frontend/src/NewApp/Pages/CreateProfilePage.tsx
Normal file
381
frontend/src/NewApp/Pages/CreateProfilePage.tsx
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Avatar,
|
||||||
|
IconButton,
|
||||||
|
Stepper,
|
||||||
|
Step,
|
||||||
|
StepLabel,
|
||||||
|
useMediaQuery,
|
||||||
|
CircularProgress,
|
||||||
|
Snackbar,
|
||||||
|
Alert
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { CloudUpload, PhotoCamera } from '@mui/icons-material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
// Interfaces
|
||||||
|
interface ProfileFormData {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
jobTitle: string;
|
||||||
|
location: string;
|
||||||
|
bio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styled components
|
||||||
|
const VisuallyHiddenInput = styled('input')({
|
||||||
|
clip: 'rect(0 0 0 0)',
|
||||||
|
clipPath: 'inset(50%)',
|
||||||
|
height: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const CreateProfilePage: React.FC = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const [activeStep, setActiveStep] = useState<number>(0);
|
||||||
|
const [profileImage, setProfileImage] = useState<string | null>(null);
|
||||||
|
const [resumeFile, setResumeFile] = useState<File | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [snackbar, setSnackbar] = useState<{open: boolean, message: string, severity: "success" | "error"}>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
severity: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<ProfileFormData>({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
jobTitle: '',
|
||||||
|
location: '',
|
||||||
|
bio: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Steps for the profile creation process
|
||||||
|
const steps = ['Personal Information', 'Professional Details', 'Resume Upload'];
|
||||||
|
|
||||||
|
// Handle form input changes
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle profile image upload
|
||||||
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (event.target?.result) {
|
||||||
|
setProfileImage(event.target.result.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle resume file upload
|
||||||
|
const handleResumeUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
setResumeFile(e.target.files[0]);
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: `Resume uploaded: ${e.target.files[0].name}`,
|
||||||
|
severity: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigation functions
|
||||||
|
const handleNext = () => {
|
||||||
|
if (activeStep === steps.length - 1) {
|
||||||
|
handleSubmit();
|
||||||
|
} else {
|
||||||
|
setActiveStep((prevStep) => prevStep + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setActiveStep((prevStep) => prevStep - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Simulate API call with timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: 'Profile created successfully! Redirecting to dashboard...',
|
||||||
|
severity: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect would happen here in a real application
|
||||||
|
// history.push('/dashboard');
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
const isStepValid = () => {
|
||||||
|
switch (activeStep) {
|
||||||
|
case 0:
|
||||||
|
return formData.firstName.trim() !== '' &&
|
||||||
|
formData.lastName.trim() !== '' &&
|
||||||
|
formData.email.trim() !== '';
|
||||||
|
case 1:
|
||||||
|
return formData.jobTitle.trim() !== '';
|
||||||
|
case 2:
|
||||||
|
return resumeFile !== null;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stepper content based on active step
|
||||||
|
const getStepContent = (step: number) => {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{xs: 12}} sx={{ textAlign: 'center', mb: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||||
|
<Avatar
|
||||||
|
src={profileImage || ''}
|
||||||
|
sx={{
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
mb: 2,
|
||||||
|
border: `2px solid ${theme.palette.primary.main}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
aria-label="upload picture"
|
||||||
|
component="label"
|
||||||
|
>
|
||||||
|
<PhotoCamera />
|
||||||
|
<VisuallyHiddenInput
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageUpload}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="caption" color="textSecondary">
|
||||||
|
Add profile photo
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{xs: 12, sm: 6}}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
label="First Name"
|
||||||
|
name="firstName"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{xs: 12, sm: 6}}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
label="Last Name"
|
||||||
|
name="lastName"
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{xs: 12}}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
label="Email Address"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{xs:12}}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Phone Number"
|
||||||
|
name="phoneNumber"
|
||||||
|
value={formData.phoneNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{xs:12}}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
label="Job Title"
|
||||||
|
name="jobTitle"
|
||||||
|
value={formData.jobTitle}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{xs: 12}}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Location"
|
||||||
|
name="location"
|
||||||
|
placeholder="City, State, Country"
|
||||||
|
value={formData.location}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{xs:12}}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
label="Professional Bio"
|
||||||
|
name="bio"
|
||||||
|
placeholder="Tell us about yourself and your professional experience"
|
||||||
|
value={formData.bio}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{xs: 12}}>
|
||||||
|
<Typography variant="body1" component="p">
|
||||||
|
Upload your resume to complete your profile. We'll analyze it to better understand your skills and experience.
|
||||||
|
(Supported formats: PDF, DOCX)
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ textAlign: 'center', mt: 2 }}>
|
||||||
|
<Button
|
||||||
|
component="label"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<CloudUpload />}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
Upload Resume
|
||||||
|
<VisuallyHiddenInput
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.docx"
|
||||||
|
onChange={handleResumeUpload}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{resumeFile && (
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
File uploaded: {resumeFile.name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return 'Unknown step';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component="main">
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
p: { xs: 2, sm: 4 },
|
||||||
|
mt: { xs: 2, sm: 4 },
|
||||||
|
mb: { xs: 2, sm: 4 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography component="h1" variant="h4" align="center" gutterBottom>
|
||||||
|
Create Your Profile
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stepper
|
||||||
|
activeStep={activeStep}
|
||||||
|
alternativeLabel={!isMobile}
|
||||||
|
orientation={isMobile ? 'vertical' : 'horizontal'}
|
||||||
|
sx={{ mt: 3, mb: 5 }}
|
||||||
|
>
|
||||||
|
{steps.map((label) => (
|
||||||
|
<Step key={label}>
|
||||||
|
<StepLabel>{label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2, mb: 4 }}>
|
||||||
|
{getStepContent(activeStep)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
|
||||||
|
<Button
|
||||||
|
disabled={activeStep === 0}
|
||||||
|
onClick={handleBack}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!isStepValid() || loading}
|
||||||
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : null}
|
||||||
|
>
|
||||||
|
{activeStep === steps.length - 1 ? 'Create Profile' : 'Next'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={snackbar.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||||
|
severity={snackbar.severity}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{snackbar.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CreateProfilePage };
|
151
src/server.py
151
src/server.py
@ -278,12 +278,12 @@ class WebServer:
|
|||||||
self.setup_routes()
|
self.setup_routes()
|
||||||
|
|
||||||
def setup_routes(self):
|
def setup_routes(self):
|
||||||
@self.app.get("/")
|
# @self.app.get("/")
|
||||||
async def root():
|
# async def root():
|
||||||
context = self.create_context(username=defines.default_username)
|
# context = self.create_context(username=defines.default_username)
|
||||||
logger.info(f"Redirecting non-context to {context.id}")
|
# logger.info(f"Redirecting non-context to {context.id}")
|
||||||
return RedirectResponse(url=f"/{context.id}", status_code=307)
|
# return RedirectResponse(url=f"/{context.id}", status_code=307)
|
||||||
# return JSONResponse({"redirect": f"/{context.id}"})
|
# # return JSONResponse({"redirect": f"/{context.id}"})
|
||||||
|
|
||||||
@self.app.get("/api/umap/entry/{doc_id}/{context_id}")
|
@self.app.get("/api/umap/entry/{doc_id}/{context_id}")
|
||||||
async def get_umap(doc_id: str, context_id: str, request: Request):
|
async def get_umap(doc_id: str, context_id: str, request: Request):
|
||||||
@ -576,6 +576,8 @@ class WebServer:
|
|||||||
"last_name": user.last_name,
|
"last_name": user.last_name,
|
||||||
"full_name": user.full_name,
|
"full_name": user.full_name,
|
||||||
"contact_info": user.contact_info,
|
"contact_info": user.contact_info,
|
||||||
|
"rag_content_size": user.rag_content_size,
|
||||||
|
"profile_url": user.profile_url,
|
||||||
"questions": [ q.model_dump(mode='json') for q in user.user_questions],
|
"questions": [ q.model_dump(mode='json') for q in user.user_questions],
|
||||||
}
|
}
|
||||||
return JSONResponse(user_data)
|
return JSONResponse(user_data)
|
||||||
@ -730,6 +732,20 @@ class WebServer:
|
|||||||
logger.error(f"Error in post_chat_endpoint: {e}")
|
logger.error(f"Error in post_chat_endpoint: {e}")
|
||||||
return JSONResponse({"error": str(e)}, status_code=500)
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@self.app.post("/api/create-session")
|
||||||
|
async def create_session(request: Request):
|
||||||
|
logger.info(f"{request.method} {request.url.path}")
|
||||||
|
context = self.create_context(username=defines.default_username)
|
||||||
|
return JSONResponse({"id": context.id})
|
||||||
|
|
||||||
|
@self.app.get("/api/join-session/{context_id}")
|
||||||
|
async def join_session(context_id: str, request: Request):
|
||||||
|
logger.info(f"{request.method} {request.url.path}")
|
||||||
|
context = self.load_context(context_id=context_id)
|
||||||
|
if not context:
|
||||||
|
return JSONResponse({"error": f"{context_id} does not exist."}, 404)
|
||||||
|
return JSONResponse({"id": context.id})
|
||||||
|
|
||||||
@self.app.post("/api/context/u/{username}")
|
@self.app.post("/api/context/u/{username}")
|
||||||
async def create_user_context(username: str, request: Request):
|
async def create_user_context(username: str, request: Request):
|
||||||
logger.info(f"{request.method} {request.url.path}")
|
logger.info(f"{request.method} {request.url.path}")
|
||||||
@ -884,7 +900,7 @@ class WebServer:
|
|||||||
|
|
||||||
return context_id
|
return context_id
|
||||||
|
|
||||||
def load_or_create_context(self, context_id: str) -> Context:
|
def load_context(self, context_id: str) -> Context | None:
|
||||||
"""
|
"""
|
||||||
Load a context from a file in the context directory or create a new one if it doesn't exist.
|
Load a context from a file in the context directory or create a new one if it doesn't exist.
|
||||||
Args:
|
Args:
|
||||||
@ -896,71 +912,74 @@ class WebServer:
|
|||||||
|
|
||||||
# Check if the file exists
|
# Check if the file exists
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
logger.info(f"Context file {file_path} not found. Creating new context.")
|
return None
|
||||||
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
|
|
||||||
else:
|
|
||||||
# Read and deserialize the data
|
|
||||||
with open(file_path, "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
logger.info(
|
|
||||||
f"Loading context from {file_path}, content length: {len(content)}"
|
|
||||||
)
|
|
||||||
json_data = {}
|
|
||||||
try:
|
|
||||||
# Try parsing as JSON first to ensure valid JSON
|
|
||||||
json_data = json.loads(content)
|
|
||||||
logger.info("JSON parsed successfully, attempting model validation")
|
|
||||||
|
|
||||||
context = Context.model_validate(json_data)
|
# Read and deserialize the data
|
||||||
username = context.username
|
with open(file_path, "r") as f:
|
||||||
if not User.exists(username):
|
content = f.read()
|
||||||
raise ValueError(f"Attempt to load context {context.id} with invalid user {username}")
|
logger.info(
|
||||||
|
f"Loading context from {file_path}, content length: {len(content)}"
|
||||||
|
)
|
||||||
|
json_data = {}
|
||||||
|
try:
|
||||||
|
# Try parsing as JSON first to ensure valid JSON
|
||||||
|
json_data = json.loads(content)
|
||||||
|
logger.info("JSON parsed successfully, attempting model validation")
|
||||||
|
|
||||||
matching_user = next((user for user in self.users if user.username == username), None)
|
context = Context.model_validate(json_data)
|
||||||
if matching_user:
|
username = context.username
|
||||||
user = matching_user
|
if not User.exists(username):
|
||||||
else:
|
raise ValueError(f"Attempt to load context {context.id} with invalid user {username}")
|
||||||
user = User(username=username, llm=self.llm)
|
|
||||||
user.initialize(prometheus_collector=self.prometheus_collector)
|
|
||||||
self.users.append(user)
|
|
||||||
context.user = user
|
|
||||||
|
|
||||||
# Now set context on agents manually
|
matching_user = next((user for user in self.users if user.username == username), None)
|
||||||
agent_types = [agent.agent_type for agent in context.agents]
|
if matching_user:
|
||||||
if len(agent_types) != len(set(agent_types)):
|
user = matching_user
|
||||||
raise ValueError(
|
else:
|
||||||
"Context cannot contain multiple agents of the same agent_type"
|
user = User(username=username, llm=self.llm)
|
||||||
)
|
user.initialize(prometheus_collector=self.prometheus_collector)
|
||||||
for agent in context.agents:
|
self.users.append(user)
|
||||||
agent.set_context(context)
|
context.user = user
|
||||||
|
|
||||||
self.contexts[context_id] = context
|
# Now set context on agents manually
|
||||||
|
agent_types = [agent.agent_type for agent in context.agents]
|
||||||
logger.info(f"Successfully loaded context {context_id}")
|
if len(agent_types) != len(set(agent_types)):
|
||||||
except ValidationError as e:
|
raise ValueError(
|
||||||
logger.error(e)
|
"Context cannot contain multiple agents of the same agent_type"
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
for error in e.errors():
|
|
||||||
print(f"Field: {error['loc'][0]}, Error: {error['msg']}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error validating context: {str(e)}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
for key in json_data:
|
|
||||||
logger.info(f"{key} = {type(json_data[key])} {str(json_data[key])[:60] if json_data[key] else "None"}")
|
|
||||||
logger.info("*" * 50)
|
|
||||||
if len(self.users) == 0:
|
|
||||||
user = User(username=defines.default_username, llm=self.llm)
|
|
||||||
user.initialize(prometheus_collector=self.prometheus_collector)
|
|
||||||
self.users.append(user)
|
|
||||||
# Fallback to creating a new context
|
|
||||||
user = self.users[0]
|
|
||||||
self.contexts[context_id] = Context(
|
|
||||||
id=context_id,
|
|
||||||
user=user,
|
|
||||||
rags=[ rag.model_copy() for rag in user.rags ],
|
|
||||||
tools=Tools.all_tools()
|
|
||||||
)
|
)
|
||||||
|
for agent in context.agents:
|
||||||
|
agent.set_context(context)
|
||||||
|
|
||||||
|
self.contexts[context_id] = context
|
||||||
|
|
||||||
|
logger.info(f"Successfully loaded context {context_id}")
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error(e)
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
for error in e.errors():
|
||||||
|
print(f"Field: {error['loc'][0]}, Error: {error['msg']}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error validating context: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
for key in json_data:
|
||||||
|
logger.info(f"{key} = {type(json_data[key])} {str(json_data[key])[:60] if json_data[key] else "None"}")
|
||||||
|
logger.info("*" * 50)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.contexts[context_id]
|
||||||
|
|
||||||
|
def load_or_create_context(self, context_id: str) -> Context:
|
||||||
|
"""
|
||||||
|
Load a context from a file in the context directory or create a new one if it doesn't exist.
|
||||||
|
Args:
|
||||||
|
context_id: UUID string for the context.
|
||||||
|
Returns:
|
||||||
|
A Context object with the specified ID and default settings.
|
||||||
|
"""
|
||||||
|
context = self.load_context(context_id)
|
||||||
|
if context:
|
||||||
|
return context
|
||||||
|
logger.info(f"Context not found. Creating new instance of context {context_id}.")
|
||||||
|
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
|
||||||
return self.contexts[context_id]
|
return self.contexts[context_id]
|
||||||
|
|
||||||
def create_context(self, username: str, context_id=None) -> Context:
|
def create_context(self, username: str, context_id=None) -> Context:
|
||||||
|
@ -39,6 +39,9 @@ class User(BaseModel):
|
|||||||
first_name: str = ""
|
first_name: str = ""
|
||||||
last_name: str = ""
|
last_name: str = ""
|
||||||
full_name: str = ""
|
full_name: str = ""
|
||||||
|
description: str = ""
|
||||||
|
profile_url: str = ""
|
||||||
|
rag_content_size : int = 0
|
||||||
contact_info : Dict[str, str] = {}
|
contact_info : Dict[str, str] = {}
|
||||||
user_questions : List[Question] = []
|
user_questions : List[Question] = []
|
||||||
|
|
||||||
@ -197,6 +200,8 @@ class User(BaseModel):
|
|||||||
self.first_name = info.get("first_name", self.username)
|
self.first_name = info.get("first_name", self.username)
|
||||||
self.last_name = info.get("last_name", "")
|
self.last_name = info.get("last_name", "")
|
||||||
self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}")
|
self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}")
|
||||||
|
self.description = info.get("first_name", self.description)
|
||||||
|
self.profile_url = info.get("profile_url", self.description)
|
||||||
self.contact_info = info.get("contact_info", {})
|
self.contact_info = info.get("contact_info", {})
|
||||||
questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"])
|
questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"])
|
||||||
self.user_questions = []
|
self.user_questions = []
|
||||||
@ -226,5 +231,7 @@ class User(BaseModel):
|
|||||||
name=self.username,
|
name=self.username,
|
||||||
description=f"Expert data about {self.full_name}.",
|
description=f"Expert data about {self.full_name}.",
|
||||||
))
|
))
|
||||||
|
self.rag_content_size = self.file_watcher.collection.count()
|
||||||
|
|
||||||
|
|
||||||
User.model_rebuild()
|
User.model_rebuild()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user