Add AI editing for resumes
This commit is contained in:
parent
c31752e50f
commit
79372231eb
46
Dockerfile
46
Dockerfile
@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# Build Pyton 3.11 for use in later stages
|
# Build Pyton 3.11 for use in later stages
|
||||||
#
|
#
|
||||||
FROM ubuntu:oracular AS python
|
FROM ubuntu:oracular AS python-local
|
||||||
|
|
||||||
SHELL [ "/bin/bash", "-c" ]
|
SHELL [ "/bin/bash", "-c" ]
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ RUN cmake .. \
|
|||||||
# * ollama-ipex-llm
|
# * ollama-ipex-llm
|
||||||
# * src/server.py - model server supporting RAG and fine-tuned models
|
# * src/server.py - model server supporting RAG and fine-tuned models
|
||||||
#
|
#
|
||||||
FROM python AS llm-base
|
FROM python-local AS llm-base
|
||||||
|
|
||||||
# Install Intel graphics runtimes
|
# Install Intel graphics runtimes
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
@ -180,8 +180,8 @@ FROM llm-base AS backstory
|
|||||||
|
|
||||||
SHELL [ "/opt/backstory/shell" ]
|
SHELL [ "/opt/backstory/shell" ]
|
||||||
|
|
||||||
#COPY /src/requirements.txt /opt/backstory/src/requirements.txt
|
#COPY /src/requirements.txt /opt/backstory/requirements.txt
|
||||||
#RUN pip install -r /opt/backstory/src/requirements.txt
|
#RUN pip install -r /opt/backstory/requirements.txt
|
||||||
RUN pip install 'markitdown[all]' pydantic 'pydantic[email]'
|
RUN pip install 'markitdown[all]' pydantic 'pydantic[email]'
|
||||||
|
|
||||||
# Prometheus
|
# Prometheus
|
||||||
@ -200,7 +200,7 @@ RUN pip install pyyaml user-agents cryptography
|
|||||||
RUN pip install openapi-python-client
|
RUN pip install openapi-python-client
|
||||||
|
|
||||||
# QR code generator
|
# QR code generator
|
||||||
RUN pip install pyqrcode pypng
|
RUN pip install setuptools pyqrcode pypng
|
||||||
|
|
||||||
# Anthropic and other backends
|
# Anthropic and other backends
|
||||||
RUN pip install anthropic pydantic_ai
|
RUN pip install anthropic pydantic_ai
|
||||||
@ -250,7 +250,7 @@ RUN { \
|
|||||||
echo ' while true; do'; \
|
echo ' while true; do'; \
|
||||||
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
|
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
|
||||||
echo ' echo "Launching Backstory server..."'; \
|
echo ' echo "Launching Backstory server..."'; \
|
||||||
echo ' python src/backend/main.py "${@}" || echo "Backstory server died."'; \
|
echo ' python3 src/backend/main.py "${@}" || echo "Backstory server died."'; \
|
||||||
echo ' echo "Sleeping for 3 seconds."'; \
|
echo ' echo "Sleeping for 3 seconds."'; \
|
||||||
echo ' else'; \
|
echo ' else'; \
|
||||||
echo ' if [[ ${once} -eq 0 ]]; then' ; \
|
echo ' if [[ ${once} -eq 0 ]]; then' ; \
|
||||||
@ -470,7 +470,7 @@ ENV PATH=/opt/backstory:$PATH
|
|||||||
|
|
||||||
ENTRYPOINT [ "/entrypoint-jupyter.sh" ]
|
ENTRYPOINT [ "/entrypoint-jupyter.sh" ]
|
||||||
|
|
||||||
FROM python AS miniircd
|
FROM python-local AS miniircd
|
||||||
|
|
||||||
# Get a couple prerequisites
|
# Get a couple prerequisites
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
@ -657,7 +657,7 @@ RUN pip install huggingface_hub modelscope
|
|||||||
|
|
||||||
ENTRYPOINT ["/bin/bash", "-c", "source /home/ollama_ov_server/openvino_genai_ubuntu24_2025.2.0.0.dev20250513_x86_64/setupvars.sh && /usr/bin/ollama serve"]
|
ENTRYPOINT ["/bin/bash", "-c", "source /home/ollama_ov_server/openvino_genai_ubuntu24_2025.2.0.0.dev20250513_x86_64/setupvars.sh && /usr/bin/ollama serve"]
|
||||||
|
|
||||||
FROM python AS vllm
|
FROM llm-base AS vllm
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||||
@ -676,7 +676,7 @@ ENV PATH=~/.local/bin:$PATH
|
|||||||
|
|
||||||
RUN { \
|
RUN { \
|
||||||
echo '#!/bin/bash' ; \
|
echo '#!/bin/bash' ; \
|
||||||
echo 'source /opt/vllm/.venv/bin/activate' ; \
|
echo 'source /opt/backstory/venv/bin/activate'; \
|
||||||
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash -i; fi' ; \
|
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash -i; fi' ; \
|
||||||
} > /opt/vllm/shell ; \
|
} > /opt/vllm/shell ; \
|
||||||
chmod +x /opt/vllm/shell
|
chmod +x /opt/vllm/shell
|
||||||
@ -695,13 +695,27 @@ RUN { \
|
|||||||
echo '#!/bin/bash'; \
|
echo '#!/bin/bash'; \
|
||||||
echo 'echo "Container: vLLM"'; \
|
echo 'echo "Container: vLLM"'; \
|
||||||
echo 'set -e'; \
|
echo 'set -e'; \
|
||||||
echo 'python -m vllm.entrypoints.openai.api_server \'; \
|
echo 'source /opt/backstory/venv/bin/activate'; \
|
||||||
echo ' --model=facebook/opt-13b \' ; \
|
echo 'while true; do'; \
|
||||||
echo ' --dtype=bfloat16 \' ; \
|
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
|
||||||
echo ' --max_model_len=1024 \' ; \
|
echo ' echo "Launching vLLM server..."'; \
|
||||||
echo ' --distributed-executor-backend=ray \' ; \
|
echo ' python3 -m vllm.entrypoints.openai.api_server \'; \
|
||||||
echo ' --pipeline-parallel-size=2 \' ; \
|
echo ' --model=Qwen/Qwen3-8b \' ; \
|
||||||
echo ' -tp=8' ; \
|
echo ' --device xpu' ; \
|
||||||
|
# echo ' --dtype=bfloat16 \' ; \
|
||||||
|
# echo ' --max_model_len=1024 \' ; \
|
||||||
|
# echo ' --distributed-executor-backend=ray \' ; \
|
||||||
|
# echo ' --pipeline-parallel-size=2 \' ; \
|
||||||
|
# echo ' -tp=1' ; \
|
||||||
|
echo ' echo "Sleeping for 3 seconds."'; \
|
||||||
|
echo ' else'; \
|
||||||
|
echo ' if [[ ${once} -eq 0 ]]; then' ; \
|
||||||
|
echo ' echo "/opt/vllm/block-server exists. Sleeping for 3 seconds."'; \
|
||||||
|
echo ' once=1' ; \
|
||||||
|
echo ' fi' ; \
|
||||||
|
echo ' fi' ; \
|
||||||
|
echo ' sleep 3'; \
|
||||||
|
echo 'done' ; \
|
||||||
} > /entrypoint.sh \
|
} > /entrypoint.sh \
|
||||||
&& chmod +x /entrypoint.sh
|
&& chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
@ -213,10 +213,11 @@ services:
|
|||||||
vllm:
|
vllm:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.xpu
|
||||||
target: vllm
|
target: vllm-openai
|
||||||
container_name: vllm
|
container_name: vllm-openai
|
||||||
restart: "always"
|
restart: "always"
|
||||||
|
shm_size: 10.24gb
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
@ -14,7 +14,15 @@
|
|||||||
"plugins": ["@typescript-eslint", "react", "react-hooks"],
|
"plugins": ["@typescript-eslint", "react", "react-hooks"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"prettier/prettier": ["error", { "arrowParens": "avoid" }]
|
"prettier/prettier": ["error", { "arrowParens": "avoid" }],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
|
@ -14,30 +14,35 @@ module.exports = {
|
|||||||
throw new Error('webpack-dev-server is not defined');
|
throw new Error('webpack-dev-server is not defined');
|
||||||
}
|
}
|
||||||
|
|
||||||
devServer.app.use('/api', createProxyMiddleware({
|
devServer.app.use(
|
||||||
target: 'https://backstory:8911',
|
'/api',
|
||||||
changeOrigin: true,
|
createProxyMiddleware({
|
||||||
secure: false,
|
target: 'https://backstory:8911',
|
||||||
buffer: false,
|
changeOrigin: true,
|
||||||
proxyTimeout: 3600000,
|
secure: false,
|
||||||
onProxyRes: function(proxyRes, req, res) {
|
buffer: false,
|
||||||
proxyRes.headers['cache-control'] = 'no-cache';
|
proxyTimeout: 3600000,
|
||||||
|
onProxyRes: function (proxyRes, req, res) {
|
||||||
|
proxyRes.headers['cache-control'] = 'no-cache';
|
||||||
|
|
||||||
if (req.url.includes('/docs') ||
|
if (
|
||||||
|
req.url.includes('/docs') ||
|
||||||
req.url.includes('/redoc') ||
|
req.url.includes('/redoc') ||
|
||||||
req.url.includes('/openapi.json')) {
|
req.url.includes('/openapi.json')
|
||||||
return; // Let original headers pass through
|
) {
|
||||||
}
|
return; // Let original headers pass through
|
||||||
// Remove any header that might cause buffering
|
}
|
||||||
proxyRes.headers['transfer-encoding'] = 'chunked';
|
// Remove any header that might cause buffering
|
||||||
delete proxyRes.headers['content-length'];
|
proxyRes.headers['transfer-encoding'] = 'chunked';
|
||||||
|
delete proxyRes.headers['content-length'];
|
||||||
|
|
||||||
// Set proper streaming headers
|
// Set proper streaming headers
|
||||||
proxyRes.headers['cache-control'] = 'no-cache';
|
proxyRes.headers['cache-control'] = 'no-cache';
|
||||||
proxyRes.headers['content-type'] = 'text/event-stream';
|
proxyRes.headers['content-type'] = 'text/event-stream';
|
||||||
proxyRes.headers['connection'] = 'keep-alive';
|
proxyRes.headers['connection'] = 'keep-alive';
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return middlewares;
|
return middlewares;
|
||||||
}
|
}
|
||||||
@ -46,11 +51,9 @@ module.exports = {
|
|||||||
configure: (webpackConfig) => {
|
configure: (webpackConfig) => {
|
||||||
webpackConfig.devtool = 'source-map';
|
webpackConfig.devtool = 'source-map';
|
||||||
// Add .ts and .tsx to resolve.extensions
|
// Add .ts and .tsx to resolve.extensions
|
||||||
webpackConfig.resolve.extensions = [
|
webpackConfig.resolve.extensions = [...webpackConfig.resolve.extensions, '.ts', '.tsx'];
|
||||||
...webpackConfig.resolve.extensions,
|
// Ignore source map warnings for node_modules
|
||||||
'.ts',
|
webpackConfig.ignoreWarnings = [/Failed to parse source map/];
|
||||||
'.tsx',
|
|
||||||
];
|
|
||||||
return webpackConfig;
|
return webpackConfig;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
2108
frontend/package-lock.json
generated
2108
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,11 +18,13 @@
|
|||||||
"@types/lodash": "^4.17.17",
|
"@types/lodash": "^4.17.17",
|
||||||
"@types/luxon": "^3.6.2",
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/node": "^16.18.126",
|
"@types/node": "^16.18.126",
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@uiw/react-json-view": "^2.0.0-alpha.31",
|
"@uiw/react-json-view": "^2.0.0-alpha.31",
|
||||||
"@uiw/react-markdown-editor": "^6.1.4",
|
"@uiw/react-markdown-editor": "^6.1.4",
|
||||||
"country-state-city": "^3.2.1",
|
"country-state-city": "^3.2.1",
|
||||||
|
"diff": "^8.0.2",
|
||||||
|
"diff2html": "^3.4.52",
|
||||||
"jsonrepair": "^3.12.0",
|
"jsonrepair": "^3.12.0",
|
||||||
"libphonenumber-js": "^1.12.9",
|
"libphonenumber-js": "^1.12.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@ -33,6 +35,7 @@
|
|||||||
"mui-markdown": "^2.0.1",
|
"mui-markdown": "^2.0.1",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-diff-view": "^3.3.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-markdown-it": "^1.0.2",
|
"react-markdown-it": "^1.0.2",
|
||||||
|
9
frontend/src/components/DiffViewer.css
Normal file
9
frontend/src/components/DiffViewer.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.d2h-file-side-diff .d2h-code-line pre {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
overflow-wrap: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d2h-file-header {
|
||||||
|
display: none;
|
||||||
|
}
|
62
frontend/src/components/DiffViewer.tsx
Normal file
62
frontend/src/components/DiffViewer.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { createPatch } from 'diff';
|
||||||
|
import { Box, SxProps } from '@mui/material';
|
||||||
|
import * as Diff2Html from 'diff2html';
|
||||||
|
import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui';
|
||||||
|
import 'diff2html/bundles/css/diff2html.min.css';
|
||||||
|
import './DiffViewer.css';
|
||||||
|
|
||||||
|
interface DiffViewerProps {
|
||||||
|
original: string;
|
||||||
|
content: string;
|
||||||
|
sx?: SxProps;
|
||||||
|
outputFormat?: 'line-by-line' | 'side-by-side';
|
||||||
|
drawFileList?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DiffViewer: React.FC<DiffViewerProps> = (props: DiffViewerProps) => {
|
||||||
|
const { original, content, sx, outputFormat = 'line-by-line', drawFileList = false } = props;
|
||||||
|
const diffRef = useRef<HTMLDivElement>(null);
|
||||||
|
const diffString = createPatch('Resume', original || '', content || '', 'original', 'modified', {
|
||||||
|
context: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (diffRef.current && diffString) {
|
||||||
|
// Clear previous content
|
||||||
|
diffRef.current.innerHTML = '';
|
||||||
|
diffRef.current.className = `diff-viewer`;
|
||||||
|
// Generate HTML from diff string
|
||||||
|
const diff2htmlUi = new Diff2HtmlUI(diffRef.current, diffString, {
|
||||||
|
drawFileList: false,
|
||||||
|
matching: 'lines',
|
||||||
|
outputFormat: 'side-by-side',
|
||||||
|
synchronisedScroll: true,
|
||||||
|
highlight: true,
|
||||||
|
fileContentToggle: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
diff2htmlUi.draw();
|
||||||
|
diff2htmlUi.highlightCode();
|
||||||
|
diff2htmlUi.synchronisedScroll();
|
||||||
|
// diffRef.current.innerHTML = diff2htmlUi;
|
||||||
|
}
|
||||||
|
}, [diffString, outputFormat, drawFileList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={diffRef}
|
||||||
|
className="diff-viewer"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
maxWidth: '100%',
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DiffViewer };
|
@ -196,6 +196,7 @@ interface MessageProps extends BackstoryElementProps {
|
|||||||
chatSession?: ChatSession;
|
chatSession?: ChatSession;
|
||||||
className?: string;
|
className?: string;
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
|
viewAsMarkdown?: boolean; // Whether to render messages as markdown
|
||||||
expandable?: boolean;
|
expandable?: boolean;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
onExpand?: (open: boolean) => void;
|
onExpand?: (open: boolean) => void;
|
||||||
@ -463,7 +464,17 @@ const MessageContainer = (props: MessageContainerProps): JSX.Element => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Message = (props: MessageProps): JSX.Element => {
|
const Message = (props: MessageProps): JSX.Element => {
|
||||||
const { message, title, sx, className, chatSession, onExpand, expanded, expandable } = props;
|
const {
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
sx,
|
||||||
|
className,
|
||||||
|
chatSession,
|
||||||
|
onExpand,
|
||||||
|
expanded,
|
||||||
|
expandable,
|
||||||
|
viewAsMarkdown = true,
|
||||||
|
} = props;
|
||||||
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const type: ApiActivityType | ChatSenderType | 'error' =
|
const type: ApiActivityType | ChatSenderType | 'error' =
|
||||||
@ -491,11 +502,20 @@ const Message = (props: MessageProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageView = (
|
const messageView = (
|
||||||
<StyledMarkdown
|
<>
|
||||||
chatSession={chatSession}
|
{viewAsMarkdown && (
|
||||||
streaming={message.status === 'streaming'}
|
<StyledMarkdown
|
||||||
content={content}
|
chatSession={chatSession}
|
||||||
/>
|
streaming={message.status === 'streaming'}
|
||||||
|
content={content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!viewAsMarkdown && (
|
||||||
|
<Box>
|
||||||
|
<pre>{content}</pre>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
let metadataView = <></>;
|
let metadataView = <></>;
|
||||||
|
499
frontend/src/components/ResumeChat.tsx
Normal file
499
frontend/src/components/ResumeChat.tsx
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
import React, { forwardRef, useState, useEffect, useRef, JSX } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
SxProps,
|
||||||
|
Typography,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Send as SendIcon, Person as PersonIcon } from '@mui/icons-material';
|
||||||
|
// import PrecisionManufacturingIcon from '@mui/icons-material/PrecisionManufacturing';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import UndoIcon from '@mui/icons-material/Undo';
|
||||||
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
import PreviewIcon from '@mui/icons-material/Preview';
|
||||||
|
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||||
|
import EditDocumentIcon from '@mui/icons-material/EditDocument';
|
||||||
|
import {
|
||||||
|
ChatMessage,
|
||||||
|
ChatSession,
|
||||||
|
ChatMessageUser,
|
||||||
|
ChatMessageError,
|
||||||
|
ChatMessageStreaming,
|
||||||
|
ChatMessageStatus,
|
||||||
|
ChatMessageMetaData,
|
||||||
|
} from 'types/types';
|
||||||
|
import { ConversationHandle } from 'components/Conversation';
|
||||||
|
import { Message } from 'components/Message';
|
||||||
|
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||||
|
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||||
|
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||||
|
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||||
|
import { Scrollable } from 'components/Scrollable';
|
||||||
|
import { StyledMarkdown } from './StyledMarkdown';
|
||||||
|
import { DiffViewer } from './DiffViewer';
|
||||||
|
|
||||||
|
const emptyMetadata: ChatMessageMetaData = {
|
||||||
|
model: 'qwen2.5',
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 0,
|
||||||
|
topP: 0,
|
||||||
|
frequencyPenalty: 0,
|
||||||
|
presencePenalty: 0,
|
||||||
|
stopSequences: [],
|
||||||
|
usage: {
|
||||||
|
evalCount: 0,
|
||||||
|
evalDuration: 0,
|
||||||
|
promptEvalCount: 0,
|
||||||
|
promptEvalDuration: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultMessage: ChatMessage = {
|
||||||
|
status: 'done',
|
||||||
|
type: 'text',
|
||||||
|
sessionId: '',
|
||||||
|
timestamp: new Date(),
|
||||||
|
content: '',
|
||||||
|
role: 'user',
|
||||||
|
metadata: emptyMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ResumeChatProps {
|
||||||
|
onResumeChange: (resume: string) => void; // Callback when the resume changes
|
||||||
|
resume: string;
|
||||||
|
session: string; // Session identifier for the chat
|
||||||
|
sx?: SxProps; // Optional styles for the component
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditViewerProps {
|
||||||
|
original: string; // Original resume content
|
||||||
|
content: string; // Edited resume content
|
||||||
|
sx?: SxProps; // Optional styles for the component
|
||||||
|
onUpdate?: (content: string) => void; // Callback when the content is updated
|
||||||
|
}
|
||||||
|
const EditViewer = (props: EditViewerProps) => {
|
||||||
|
const { original, content, sx, onUpdate } = props;
|
||||||
|
const [tabValue, setTabValue] = useState<string>('raw');
|
||||||
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||||
|
if (newValue === 'diff') {
|
||||||
|
setTabValue(newValue);
|
||||||
|
} else {
|
||||||
|
setTabValue(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ...sx }}>
|
||||||
|
<Tabs value={tabValue} onChange={handleTabChange} sx={{ m: 0 }}>
|
||||||
|
<Tab value="raw" icon={<EditDocumentIcon sx={{ height: 16 }} />} sx={{ height: 20 }} />
|
||||||
|
<Tab value="markdown" icon={<PreviewIcon sx={{ height: 16 }} />} sx={{ height: 20 }} />
|
||||||
|
<Tab value="diff" icon={<DifferenceIcon sx={{ height: 16 }} />} sx={{ height: 20 }} />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{tabValue === 'raw' && (
|
||||||
|
<Box>
|
||||||
|
<pre>{content}</pre>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{tabValue === 'markdown' && <StyledMarkdown content={content} />}
|
||||||
|
<Dialog
|
||||||
|
open={tabValue === 'diff'}
|
||||||
|
onClose={(): void => {
|
||||||
|
setTabValue('raw');
|
||||||
|
}}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
disableEscapeKeyDown={false}
|
||||||
|
fullScreen={true}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
flex: 1 /* Take remaining space in some-container */,
|
||||||
|
overflowY: 'auto' /* Scroll if content overflows */,
|
||||||
|
p: 0,
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DiffViewer original={original} content={content} />
|
||||||
|
</Scrollable>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title="Revert Changes">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
minWidth: 'auto',
|
||||||
|
maxHeight: 'min-content',
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={(): void => {
|
||||||
|
setTabValue('raw');
|
||||||
|
onUpdate && onUpdate(original);
|
||||||
|
}}
|
||||||
|
disabled={!onUpdate}
|
||||||
|
>
|
||||||
|
<UndoIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Save Changes">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
minWidth: 'auto',
|
||||||
|
maxHeight: 'min-content',
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={(): void => {
|
||||||
|
setTabValue('raw');
|
||||||
|
onUpdate && onUpdate(content);
|
||||||
|
}}
|
||||||
|
disabled={!onUpdate}
|
||||||
|
>
|
||||||
|
<SaveIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResumeChat = forwardRef<ConversationHandle, ResumeChatProps>(
|
||||||
|
(props: ResumeChatProps, ref): JSX.Element => {
|
||||||
|
const {
|
||||||
|
session,
|
||||||
|
onResumeChange,
|
||||||
|
resume,
|
||||||
|
sx,
|
||||||
|
// tunables = {
|
||||||
|
// enableRAG: true,
|
||||||
|
// enableTools: true,
|
||||||
|
// enableContext: true,
|
||||||
|
// topK: 50,
|
||||||
|
// temperature: 0.7,
|
||||||
|
// threshold: 0.75,
|
||||||
|
// },
|
||||||
|
} = props;
|
||||||
|
const { apiClient } = useAuth();
|
||||||
|
const { selectedCandidate } = useSelectedCandidate();
|
||||||
|
const [processingMessage, setProcessingMessage] = useState<
|
||||||
|
ChatMessageStatus | ChatMessageError | null
|
||||||
|
>(null);
|
||||||
|
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
||||||
|
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||||
|
|
||||||
|
const { setSnack } = useAppState();
|
||||||
|
|
||||||
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [streaming, setStreaming] = useState<boolean>(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const onDelete = async (session: ChatSession): Promise<void> => {
|
||||||
|
if (!session.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await apiClient.resetChatSession(session.id);
|
||||||
|
// If we're deleting the currently selected session, clear it
|
||||||
|
setMessages([]);
|
||||||
|
setSnack('Session reset succeeded', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete session:', error);
|
||||||
|
setSnack('Failed to delete session', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const sendMessage = async (message: string): Promise<void> => {
|
||||||
|
if (!message.trim() || !chatSession?.id || streaming || !selectedCandidate) return;
|
||||||
|
|
||||||
|
const messageContent = message;
|
||||||
|
setStreaming(true);
|
||||||
|
|
||||||
|
const chatMessage: ChatMessageUser = {
|
||||||
|
sessionId: chatSession.id,
|
||||||
|
role: 'user',
|
||||||
|
content: messageContent,
|
||||||
|
status: 'done',
|
||||||
|
type: 'text',
|
||||||
|
extraContext: { resume: resume },
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setProcessingMessage({
|
||||||
|
...defaultMessage,
|
||||||
|
status: 'status',
|
||||||
|
activity: 'info',
|
||||||
|
content: `Establishing connection with ${selectedCandidate.firstName}'s chat session.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setMessages(prev => {
|
||||||
|
return [...prev, chatMessage] as ChatMessage[];
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
apiClient.sendMessageStream(chatMessage, {
|
||||||
|
onMessage: (msg: ChatMessage): void => {
|
||||||
|
setMessages(prev => {
|
||||||
|
const filtered = prev.filter(m => m.id !== msg.id);
|
||||||
|
return [...filtered, msg] as ChatMessage[];
|
||||||
|
});
|
||||||
|
setStreamingMessage(null);
|
||||||
|
setProcessingMessage(null);
|
||||||
|
},
|
||||||
|
onError: (error: string | ChatMessageError): void => {
|
||||||
|
console.log('onError:', error);
|
||||||
|
// Type-guard to determine if this is a ChatMessageBase or a string
|
||||||
|
if (typeof error === 'object' && error !== null && 'content' in error) {
|
||||||
|
setProcessingMessage(error);
|
||||||
|
} else {
|
||||||
|
setProcessingMessage({
|
||||||
|
...defaultMessage,
|
||||||
|
status: 'error',
|
||||||
|
content: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setStreaming(false);
|
||||||
|
},
|
||||||
|
onStreaming: (chunk: ChatMessageStreaming): void => {
|
||||||
|
// console.log("onStreaming:", chunk);
|
||||||
|
setStreamingMessage({
|
||||||
|
...chunk,
|
||||||
|
role: 'assistant',
|
||||||
|
metadata: emptyMetadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStatus: (status: ChatMessageStatus): void => {
|
||||||
|
setProcessingMessage(status);
|
||||||
|
},
|
||||||
|
onComplete: (): void => {
|
||||||
|
console.log('onComplete');
|
||||||
|
setStreamingMessage(null);
|
||||||
|
setProcessingMessage(null);
|
||||||
|
setStreaming(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send message:', error);
|
||||||
|
setStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new messages arrive
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Load sessions when username changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedCandidate) {
|
||||||
|
setSnack('Candidate not selected', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
apiClient
|
||||||
|
.getOrCreateChatSession(selectedCandidate, session, 'edit_resume')
|
||||||
|
.then(session => {
|
||||||
|
setChatSession(session);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setSnack('Unable to load chat session', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedCandidate, apiClient, setSnack]);
|
||||||
|
|
||||||
|
// Load messages when session changes
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMessages = async (): Promise<void> => {
|
||||||
|
if (!chatSession?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiClient.getChatMessages(chatSession.id);
|
||||||
|
const chatMessages: ChatMessage[] = result.data;
|
||||||
|
setMessages(chatMessages);
|
||||||
|
setProcessingMessage(null);
|
||||||
|
setStreamingMessage(null);
|
||||||
|
console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (chatSession?.id) {
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
}, [chatSession, apiClient]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%' /* Restrict to main-container's height */,
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
minHeight: 0 /* Prevent flex overflow */,
|
||||||
|
maxHeight: 'min-content',
|
||||||
|
'& > *:not(.Scrollable)': {
|
||||||
|
flexShrink: 0 /* Prevent shrinking */,
|
||||||
|
},
|
||||||
|
position: 'relative',
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Chat Interface */}
|
||||||
|
{/* Scrollable Messages Area */}
|
||||||
|
{chatSession && (
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
flex: 1 /* Take remaining space in some-container */,
|
||||||
|
overflowY: 'auto' /* Scroll if content overflows */,
|
||||||
|
p: 0,
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.map((message: ChatMessage) => (
|
||||||
|
<Box
|
||||||
|
key={message.id}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
maxWidth: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
'& pre, & > p': { border: 0, mt: '0.25rem', mb: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.role === 'user' && (
|
||||||
|
<>
|
||||||
|
<PersonIcon />
|
||||||
|
<Typography>{message.content}</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{message.role === 'assistant' && (
|
||||||
|
<EditViewer
|
||||||
|
original={resume}
|
||||||
|
content={message.content}
|
||||||
|
onUpdate={(content: string): void => {
|
||||||
|
onResumeChange && onResumeChange(content);
|
||||||
|
}}
|
||||||
|
sx={{ maxWidth: '100%', boxSizing: 'border-box' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{processingMessage !== null && (
|
||||||
|
<Message {...{ chatSession, message: processingMessage }} />
|
||||||
|
)}
|
||||||
|
{streamingMessage !== null && (
|
||||||
|
<Box sx={{ '& pre': { border: 0 } }}>
|
||||||
|
<pre>{streamingMessage.content}</pre>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{streaming && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
m: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PropagateLoader
|
||||||
|
size="10px"
|
||||||
|
loading={streaming}
|
||||||
|
aria-label="Loading Spinner"
|
||||||
|
data-testid="loader"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</Scrollable>
|
||||||
|
)}
|
||||||
|
{/* Fixed Message Input */}
|
||||||
|
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
|
||||||
|
<DeleteConfirmation
|
||||||
|
onDelete={(): void => {
|
||||||
|
chatSession && onDelete(chatSession);
|
||||||
|
}}
|
||||||
|
disabled={!chatSession}
|
||||||
|
sx={{ minWidth: 'auto', px: 2, maxHeight: 'min-content' }}
|
||||||
|
action="reset"
|
||||||
|
label="chat session"
|
||||||
|
title="Reset Chat Session"
|
||||||
|
message={`Are you sure you want to reset the session? This action cannot be undone.`}
|
||||||
|
/>
|
||||||
|
<BackstoryTextField
|
||||||
|
placeholder="What would you like to change about this resume?"
|
||||||
|
ref={backstoryTextRef}
|
||||||
|
onEnter={sendMessage}
|
||||||
|
disabled={streaming || loading}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Send">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
minWidth: 'auto',
|
||||||
|
maxHeight: 'min-content',
|
||||||
|
alignSelf: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={(): void => {
|
||||||
|
sendMessage(
|
||||||
|
(backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ''
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={streaming || loading}
|
||||||
|
>
|
||||||
|
<SendIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ResumeChat.displayName = 'ResumeChat';
|
||||||
|
export { ResumeChat };
|
@ -25,7 +25,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
|
|||||||
const { job, candidate, skills, onComplete } = props;
|
const { job, candidate, skills, onComplete } = props;
|
||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { apiClient, user } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const [resume, setResume] = useState<string>('');
|
const [resume, setResume] = useState<string>('');
|
||||||
const [prompt, setPrompt] = useState<string>('');
|
const [prompt, setPrompt] = useState<string>('');
|
||||||
const [systemPrompt, setSystemPrompt] = useState<string>('');
|
const [systemPrompt, setSystemPrompt] = useState<string>('');
|
||||||
|
@ -161,7 +161,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
label: item.label as string,
|
label: item.label as string,
|
||||||
icon: item.icon || null,
|
icon: item.icon || null,
|
||||||
action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
|
action: () => item.path && navigate(item.path.replace(/:.*$/, ''), { replace: true }),
|
||||||
group: 'profile',
|
group: 'profile',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -42,13 +42,11 @@ import {
|
|||||||
Visibility as VisibilityIcon,
|
Visibility as VisibilityIcon,
|
||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
Delete as DeleteIcon,
|
Delete as DeleteIcon,
|
||||||
Close as CloseIcon,
|
|
||||||
ModelTraining,
|
ModelTraining,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { TransitionProps } from '@mui/material/transitions';
|
import { TransitionProps } from '@mui/material/transitions';
|
||||||
import * as Types from 'types/types'; // Adjust the import path as necessary
|
import * as Types from 'types/types'; // Adjust the import path as necessary
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
|
||||||
import { Scrollable } from 'components/Scrollable';
|
import { Scrollable } from 'components/Scrollable';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { JobInfo } from './JobInfo';
|
import { JobInfo } from './JobInfo';
|
||||||
|
@ -147,6 +147,6 @@
|
|||||||
|
|
||||||
.BackstoryResumeHeader p {
|
.BackstoryResumeHeader p {
|
||||||
/* border: 3px solid purple; */
|
/* border: 3px solid purple; */
|
||||||
margin: 0;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Grid,
|
|
||||||
SxProps,
|
SxProps,
|
||||||
Stack,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
Button,
|
Button,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
@ -26,7 +24,6 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Chip,
|
|
||||||
Theme,
|
Theme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import PrintIcon from '@mui/icons-material/Print';
|
import PrintIcon from '@mui/icons-material/Print';
|
||||||
@ -40,7 +37,6 @@ import {
|
|||||||
Person as PersonIcon,
|
Person as PersonIcon,
|
||||||
Schedule as ScheduleIcon,
|
Schedule as ScheduleIcon,
|
||||||
ModelTraining,
|
ModelTraining,
|
||||||
Style as StyleIcon,
|
|
||||||
Email as EmailIcon,
|
Email as EmailIcon,
|
||||||
Phone as PhoneIcon,
|
Phone as PhoneIcon,
|
||||||
LocationOn as LocationIcon,
|
LocationOn as LocationIcon,
|
||||||
@ -65,6 +61,7 @@ import { Scrollable } from 'components/Scrollable';
|
|||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
import { StreamingOptions } from 'services/api-client';
|
import { StreamingOptions } from 'services/api-client';
|
||||||
import { StatusBox, StatusIcon } from './StatusIcon';
|
import { StatusBox, StatusIcon } from './StatusIcon';
|
||||||
|
import { ResumeChat } from 'components/ResumeChat';
|
||||||
|
|
||||||
interface ResumeInfoProps {
|
interface ResumeInfoProps {
|
||||||
resume: Resume;
|
resume: Resume;
|
||||||
@ -91,266 +88,278 @@ interface ResumeStyle {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const resumeStyles: Record<string, ResumeStyle> = {
|
const generateResumeStyles = () => {
|
||||||
classic: {
|
const defaultStyle = {
|
||||||
name: 'Classic',
|
display: 'flex',
|
||||||
description: 'Traditional, professional serif design',
|
flexDirection: 'row',
|
||||||
headerStyle: {
|
};
|
||||||
fontFamily: '"Times New Roman", Times, serif',
|
|
||||||
borderBottom: '2px solid #2c3e50',
|
return {
|
||||||
paddingBottom: 2,
|
classic: {
|
||||||
marginBottom: 3,
|
name: 'Classic',
|
||||||
},
|
description: 'Traditional, professional serif design',
|
||||||
footerStyle: {
|
headerStyle: {
|
||||||
fontFamily: '"Times New Roman", Times, serif',
|
...defaultStyle,
|
||||||
borderTop: '2px solid #2c3e50',
|
|
||||||
paddingTop: 2,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
alignContent: 'center',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
pb: 2,
|
|
||||||
mb: 2,
|
|
||||||
},
|
|
||||||
contentStyle: {
|
|
||||||
fontFamily: '"Times New Roman", Times, serif',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
color: '#2c3e50',
|
|
||||||
},
|
|
||||||
markdownStyle: {
|
|
||||||
fontFamily: '"Times New Roman", Times, serif',
|
|
||||||
'& h1, & h2, & h3': {
|
|
||||||
fontFamily: '"Times New Roman", Times, serif',
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
borderBottom: '2px solid #2c3e50',
|
||||||
|
paddingBottom: 2,
|
||||||
|
marginBottom: 3,
|
||||||
|
},
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
borderTop: '2px solid #2c3e50',
|
||||||
|
paddingTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
lineHeight: 1.6,
|
||||||
color: '#2c3e50',
|
color: '#2c3e50',
|
||||||
borderBottom: '1px solid #bdc3c7',
|
|
||||||
paddingBottom: 1,
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
},
|
||||||
'& p, & li': {
|
markdownStyle: {
|
||||||
lineHeight: 1.6,
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
marginBottom: 1,
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
color: '#2c3e50',
|
||||||
|
borderBottom: '1px solid #bdc3c7',
|
||||||
|
paddingBottom: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.6,
|
||||||
|
marginBottom: 1,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'& ul': {
|
color: {
|
||||||
paddingLeft: 3,
|
primary: '#2c3e50',
|
||||||
|
secondary: '#34495e',
|
||||||
|
accent: '#3498db',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
color: {
|
modern: {
|
||||||
primary: '#2c3e50',
|
name: 'Modern',
|
||||||
secondary: '#34495e',
|
description: 'Clean, minimalist sans-serif layout',
|
||||||
accent: '#3498db',
|
headerStyle: {
|
||||||
text: '#2c3e50',
|
...defaultStyle,
|
||||||
background: '#ffffff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
modern: {
|
|
||||||
name: 'Modern',
|
|
||||||
description: 'Clean, minimalist sans-serif layout',
|
|
||||||
headerStyle: {
|
|
||||||
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
||||||
borderLeft: '4px solid #3498db',
|
|
||||||
paddingLeft: 2,
|
|
||||||
marginBottom: 3,
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
},
|
|
||||||
footerStyle: {
|
|
||||||
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
||||||
borderLeft: '4px solid #3498db',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
paddingTop: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
alignContent: 'center',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
pb: 2,
|
|
||||||
mb: 2,
|
|
||||||
},
|
|
||||||
contentStyle: {
|
|
||||||
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
||||||
lineHeight: 1.5,
|
|
||||||
color: '#2c3e50',
|
|
||||||
},
|
|
||||||
markdownStyle: {
|
|
||||||
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
||||||
'& h1, & h2, & h3': {
|
|
||||||
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
color: '#3498db',
|
borderLeft: '4px solid #3498db',
|
||||||
fontWeight: 300,
|
paddingLeft: 2,
|
||||||
marginBottom: 1.5,
|
marginBottom: 3,
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 1,
|
||||||
},
|
},
|
||||||
'& h1': {
|
footerStyle: {
|
||||||
fontSize: '1.75rem',
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
borderLeft: '4px solid #3498db',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
paddingTop: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
},
|
},
|
||||||
'& h2': {
|
contentStyle: {
|
||||||
fontSize: '1.5rem',
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
},
|
|
||||||
'& h3': {
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
},
|
|
||||||
'& p, & li': {
|
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
marginBottom: 0.75,
|
color: '#2c3e50',
|
||||||
},
|
},
|
||||||
'& ul': {
|
markdownStyle: {
|
||||||
paddingLeft: 2.5,
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
color: '#3498db',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: 1.5,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.5,
|
||||||
|
marginBottom: 0.75,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 2.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: '#3498db',
|
||||||
|
secondary: '#2c3e50',
|
||||||
|
accent: '#e74c3c',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
color: {
|
creative: {
|
||||||
primary: '#3498db',
|
name: 'Creative',
|
||||||
secondary: '#2c3e50',
|
description: 'Colorful, unique design with personality',
|
||||||
accent: '#e74c3c',
|
headerStyle: {
|
||||||
text: '#2c3e50',
|
...defaultStyle,
|
||||||
background: '#ffffff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
creative: {
|
|
||||||
name: 'Creative',
|
|
||||||
description: 'Colorful, unique design with personality',
|
|
||||||
headerStyle: {
|
|
||||||
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
color: '#ffffff',
|
|
||||||
padding: 2.5,
|
|
||||||
borderRadius: 1.5,
|
|
||||||
marginBottom: 3,
|
|
||||||
},
|
|
||||||
footerStyle: {
|
|
||||||
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
color: '#ffffff',
|
|
||||||
paddingTop: 2,
|
|
||||||
borderRadius: 1.5,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
alignContent: 'center',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
pb: 2,
|
|
||||||
mb: 2,
|
|
||||||
},
|
|
||||||
contentStyle: {
|
|
||||||
fontFamily: '"Open Sans", Arial, sans-serif',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
color: '#444444',
|
|
||||||
},
|
|
||||||
markdownStyle: {
|
|
||||||
fontFamily: '"Open Sans", Arial, sans-serif',
|
|
||||||
'& h1, & h2, & h3': {
|
|
||||||
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
color: '#667eea',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
fontWeight: 600,
|
color: '#ffffff',
|
||||||
marginBottom: 2,
|
padding: 2.5,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
marginBottom: 3,
|
||||||
},
|
},
|
||||||
'& h1': {
|
footerStyle: {
|
||||||
fontSize: '1.5rem',
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: '#ffffff',
|
||||||
|
paddingTop: 2,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
},
|
},
|
||||||
'& h2': {
|
contentStyle: {
|
||||||
fontSize: '1.25rem',
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
||||||
},
|
|
||||||
'& h3': {
|
|
||||||
fontSize: '1.1rem',
|
|
||||||
},
|
|
||||||
'& p, & li': {
|
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
marginBottom: 1,
|
|
||||||
color: '#444444',
|
color: '#444444',
|
||||||
},
|
},
|
||||||
'& strong': {
|
markdownStyle: {
|
||||||
color: '#764ba2',
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
||||||
fontWeight: 600,
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
color: '#667eea',
|
||||||
|
fontWeight: 600,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.6,
|
||||||
|
marginBottom: 1,
|
||||||
|
color: '#444444',
|
||||||
|
},
|
||||||
|
'& strong': {
|
||||||
|
color: '#764ba2',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'& ul': {
|
color: {
|
||||||
paddingLeft: 3,
|
primary: '#667eea',
|
||||||
|
secondary: '#764ba2',
|
||||||
|
accent: '#f093fb',
|
||||||
|
text: '#444444',
|
||||||
|
background: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
color: {
|
corporate: {
|
||||||
primary: '#667eea',
|
name: 'Corporate',
|
||||||
secondary: '#764ba2',
|
description: 'Formal, structured business format',
|
||||||
accent: '#f093fb',
|
headerStyle: {
|
||||||
text: '#444444',
|
...defaultStyle,
|
||||||
background: '#ffffff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
corporate: {
|
|
||||||
name: 'Corporate',
|
|
||||||
description: 'Formal, structured business format',
|
|
||||||
headerStyle: {
|
|
||||||
fontFamily: '"Arial", sans-serif',
|
|
||||||
border: '2px solid #34495e',
|
|
||||||
padding: 2.5,
|
|
||||||
marginBottom: 3,
|
|
||||||
backgroundColor: '#ecf0f1',
|
|
||||||
},
|
|
||||||
footerStyle: {
|
|
||||||
fontFamily: '"Arial", sans-serif',
|
|
||||||
border: '2px solid #34495e',
|
|
||||||
backgroundColor: '#ecf0f1',
|
|
||||||
paddingTop: 2,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
alignContent: 'center',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
pb: 2,
|
|
||||||
mb: 2,
|
|
||||||
},
|
|
||||||
contentStyle: {
|
|
||||||
fontFamily: '"Arial", sans-serif',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
color: '#2c3e50',
|
|
||||||
},
|
|
||||||
markdownStyle: {
|
|
||||||
fontFamily: '"Arial", sans-serif',
|
|
||||||
'& h1, & h2, & h3': {
|
|
||||||
fontFamily: '"Arial", sans-serif',
|
fontFamily: '"Arial", sans-serif',
|
||||||
color: '#34495e',
|
border: '2px solid #34495e',
|
||||||
fontWeight: 'bold',
|
padding: 2.5,
|
||||||
|
marginBottom: 3,
|
||||||
|
backgroundColor: '#ecf0f1',
|
||||||
|
},
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
border: '2px solid #34495e',
|
||||||
|
backgroundColor: '#ecf0f1',
|
||||||
|
paddingTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
fontSize: '0.875rem',
|
alignContent: 'center',
|
||||||
letterSpacing: '1px',
|
fontSize: '0.8rem',
|
||||||
marginBottom: 1.5,
|
pb: 2,
|
||||||
borderBottom: '1px solid #bdc3c7',
|
mb: 2,
|
||||||
paddingBottom: 0.5,
|
|
||||||
},
|
},
|
||||||
'& h1': {
|
contentStyle: {
|
||||||
fontSize: '1rem',
|
fontFamily: '"Arial", sans-serif',
|
||||||
},
|
|
||||||
'& h2': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
'& h3': {
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
},
|
|
||||||
'& p, & li': {
|
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
marginBottom: 0.75,
|
color: '#2c3e50',
|
||||||
fontSize: '0.75rem',
|
|
||||||
},
|
},
|
||||||
'& ul': {
|
markdownStyle: {
|
||||||
paddingLeft: 2,
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
color: '#34495e',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
letterSpacing: '1px',
|
||||||
|
marginBottom: 1.5,
|
||||||
|
borderBottom: '1px solid #bdc3c7',
|
||||||
|
paddingBottom: 0.5,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.4,
|
||||||
|
marginBottom: 0.75,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: '#34495e',
|
||||||
|
secondary: '#2c3e50',
|
||||||
|
accent: '#95a5a6',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
color: {
|
};
|
||||||
primary: '#34495e',
|
|
||||||
secondary: '#2c3e50',
|
|
||||||
accent: '#95a5a6',
|
|
||||||
text: '#2c3e50',
|
|
||||||
background: '#ffffff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
const resumeStyles: Record<string, ResumeStyle> = generateResumeStyles();
|
||||||
|
|
||||||
// Styled Header Component
|
// Styled Header Component
|
||||||
interface BackstoryStyledResumeProps {
|
interface BackstoryStyledResumeProps {
|
||||||
@ -400,102 +409,104 @@ const StyledHeader: React.FC<BackstoryStyledResumeProps> = ({ candidate, style }
|
|||||||
const phone = parsePhoneNumberFromString(candidate.phone || '', 'US');
|
const phone = parsePhoneNumberFromString(candidate.phone || '', 'US');
|
||||||
return (
|
return (
|
||||||
<Box className="BackstoryResumeHeader" sx={style.headerStyle}>
|
<Box className="BackstoryResumeHeader" sx={style.headerStyle}>
|
||||||
<Typography
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
variant="h4"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 'bold',
|
|
||||||
mb: 1,
|
|
||||||
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{candidate.fullName}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* {candidate.title && (
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h4"
|
||||||
sx={{
|
sx={{
|
||||||
mb: 2,
|
fontWeight: 'bold',
|
||||||
fontWeight: 300,
|
mb: 1,
|
||||||
color: style.name === 'creative' ? '#ffffff' : style.color.secondary,
|
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{candidate.title}
|
{candidate.fullName}
|
||||||
</Typography>
|
</Typography>
|
||||||
)} */}
|
|
||||||
|
{candidate.description && (
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
fontWeight: 300,
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.secondary,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: '0.8rem !important',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
justifyContent: 'space-between',
|
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
alignContent: 'center',
|
alignContent: 'center',
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 'fit-content',
|
||||||
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{candidate.email && (
|
{candidate.email && (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', m: 0, p: 0 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<EmailIcon
|
||||||
<EmailIcon
|
fontSize="small"
|
||||||
fontSize="small"
|
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
/>
|
||||||
/>
|
<Typography
|
||||||
<Typography
|
variant="body2"
|
||||||
variant="body2"
|
sx={{
|
||||||
sx={{
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
fontFamily: 'inherit',
|
||||||
fontFamily: 'inherit',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{candidate.email}
|
||||||
{candidate.email}
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{phone?.isValid() && (
|
{phone?.isValid() && (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<PhoneIcon
|
||||||
<PhoneIcon
|
fontSize="small"
|
||||||
fontSize="small"
|
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
/>
|
||||||
/>
|
<Typography
|
||||||
<Typography
|
variant="body2"
|
||||||
variant="body2"
|
sx={{
|
||||||
sx={{
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
fontFamily: 'inherit',
|
||||||
fontFamily: 'inherit',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{phone.formatInternational()}
|
||||||
{phone.formatInternational()}
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{candidate.location && (
|
{candidate.location && (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<LocationIcon
|
||||||
<LocationIcon
|
fontSize="small"
|
||||||
fontSize="small"
|
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
/>
|
||||||
/>
|
<Typography
|
||||||
<Typography
|
variant="body2"
|
||||||
variant="body2"
|
sx={{
|
||||||
sx={{
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
fontFamily: 'inherit',
|
||||||
fontFamily: 'inherit',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{candidate.location.city
|
||||||
{candidate.location.city}, {candidate.location.state}
|
? `${candidate.location.city}, ${candidate.location.state}`
|
||||||
</Typography>
|
: candidate.location.text}
|
||||||
</Box>
|
</Typography>
|
||||||
</Grid>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* {(candidate.website || candidate.linkedin) && (
|
{/* {(candidate.website || candidate.linkedin) && (
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<WebsiteIcon
|
<WebsiteIcon
|
||||||
@ -514,7 +525,6 @@ const StyledHeader: React.FC<BackstoryStyledResumeProps> = ({ candidate, style }
|
|||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
)} */}
|
)} */}
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -535,6 +545,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
const [editPrompt, setEditPrompt] = useState<string>('');
|
const [editPrompt, setEditPrompt] = useState<string>('');
|
||||||
const [saving, setSaving] = useState<boolean>(false);
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
const [tabValue, setTabValue] = useState('markdown');
|
const [tabValue, setTabValue] = useState('markdown');
|
||||||
|
const [jobTabValue, setJobTabValue] = useState('chat');
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
|
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
|
||||||
const [error, setError] = useState<Types.ChatMessageError | null>(null);
|
const [error, setError] = useState<Types.ChatMessageError | null>(null);
|
||||||
@ -677,6 +688,10 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
setTabValue(newValue);
|
setTabValue(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleJobTabChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||||
|
setJobTabValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -716,64 +731,61 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
mb: 2,
|
mb: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid container spacing={2}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
{activeResume.candidate && (
|
||||||
<Stack spacing={1}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
{activeResume.candidate && (
|
<PersonIcon color="primary" fontSize="small" />
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Typography variant="subtitle2" fontWeight="bold">
|
||||||
<PersonIcon color="primary" fontSize="small" />
|
Candidate
|
||||||
<Typography variant="subtitle2" fontWeight="bold">
|
|
||||||
Candidate
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{activeResume.candidate?.fullName || activeResume.candidateId}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{activeResume.candidate?.fullName || activeResume.candidateId}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{activeResume.job && (
|
{activeResume.job && (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
mt: 1,
|
mt: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorkIcon color="primary" fontSize="small" />
|
<WorkIcon color="primary" fontSize="small" />
|
||||||
<Typography variant="subtitle2" fontWeight="bold">
|
|
||||||
Job
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{activeResume.job.title} at {activeResume.job.company}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
|
||||||
<Stack spacing={1}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<ScheduleIcon color="primary" fontSize="small" />
|
|
||||||
<Typography variant="subtitle2" fontWeight="bold">
|
<Typography variant="subtitle2" fontWeight="bold">
|
||||||
Timeline
|
Job
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Created: {formatDate(activeResume.createdAt)}
|
{activeResume.job.title} at {activeResume.job.company}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
</>
|
||||||
Updated: {formatDate(activeResume.updatedAt)}
|
)}
|
||||||
</Typography>
|
</Box>
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Resume ID: {activeResume.id}
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
</Typography>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
</Stack>
|
<ScheduleIcon color="primary" fontSize="small" />
|
||||||
</Grid>
|
<Typography variant="subtitle2" fontWeight="bold">
|
||||||
</Grid>
|
Timeline
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Created: {formatDate(activeResume.createdAt)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Updated: {formatDate(activeResume.updatedAt)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Job ID: {activeResume.job?.id}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Resume ID: {activeResume.id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
@ -793,35 +805,13 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CardContent sx={{ p: 0 }}>
|
{variant === 'all' && activeResume.resume && (
|
||||||
<Box sx={{ position: 'relative' }}>
|
<CardContent sx={{ p: 0 }}>
|
||||||
<Scrollable sx={{ maxHeight: '10rem', overflowY: 'auto' }}>
|
<StyledMarkdown content={activeResume.resume} />
|
||||||
<Box
|
</CardContent>
|
||||||
sx={{
|
)}
|
||||||
display: 'flex',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
fontSize: '0.875rem !important',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
p: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{activeResume.resume}
|
|
||||||
</Box>
|
|
||||||
</Scrollable>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{variant === 'all' && activeResume.resume && (
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
<StyledMarkdown content={activeResume.resume} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Admin Controls */}
|
{/* Admin Controls */}
|
||||||
@ -1142,19 +1132,42 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: '#f8f0e0',
|
// backgroundColor: '#f8f0e0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeResume.job !== undefined && (
|
<Tabs value={jobTabValue} onChange={handleJobTabChange}>
|
||||||
|
{activeResume.job !== undefined && (
|
||||||
|
<Tab value="job" icon={<WorkIcon />} label="Job" />
|
||||||
|
)}
|
||||||
|
<Tab value="chat" icon={<ModelTraining />} label="AI Edit" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{activeResume.job !== undefined && jobTabValue === 'job' && (
|
||||||
<JobInfo
|
<JobInfo
|
||||||
variant={'all'}
|
variant={'all'}
|
||||||
job={activeResume.job}
|
job={activeResume.job}
|
||||||
sx={{
|
sx={{
|
||||||
mt: 2,
|
m: 0,
|
||||||
|
p: 1,
|
||||||
backgroundColor: '#f8f0e0',
|
backgroundColor: '#f8f0e0',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{jobTabValue === 'chat' && (
|
||||||
|
<ResumeChat
|
||||||
|
session={activeResume.id || ''}
|
||||||
|
resume={editContent}
|
||||||
|
onResumeChange={(newResume: string): void => {
|
||||||
|
setEditContent(newResume);
|
||||||
|
setActiveResume({ ...activeResume, resume: newResume });
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
m: 1,
|
||||||
|
p: 1,
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -394,18 +394,6 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
|
|||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
<TableCell
|
|
||||||
sx={{
|
|
||||||
py: isMobile ? 0.25 : 0.5,
|
|
||||||
px: isMobile ? 0.5 : 1,
|
|
||||||
width: isMobile ? '30%' : 'auto',
|
|
||||||
backgroundColor: 'background.paper',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" fontWeight="bold" noWrap>
|
|
||||||
ID
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -494,22 +482,6 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
<TableCell
|
|
||||||
sx={{
|
|
||||||
py: isMobile ? 0.25 : 0.5,
|
|
||||||
px: isMobile ? 0.5 : 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
noWrap
|
|
||||||
sx={{ fontSize: isMobile ? '0.65rem' : '0.7rem' }}
|
|
||||||
>
|
|
||||||
{resume.id}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { forwardRef, useState, useEffect, useRef, JSX } from 'react';
|
import React, { forwardRef, useState, useEffect, useRef, JSX } from 'react';
|
||||||
import { Box, Paper, Button, Tooltip } from '@mui/material';
|
import { Box, Paper, Button, Tooltip, SxProps } from '@mui/material';
|
||||||
import { Send as SendIcon } from '@mui/icons-material';
|
import { Send as SendIcon } from '@mui/icons-material';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import {
|
import {
|
||||||
@ -13,7 +13,6 @@ import {
|
|||||||
CandidateQuestion,
|
CandidateQuestion,
|
||||||
} from 'types/types';
|
} from 'types/types';
|
||||||
import { ConversationHandle } from 'components/Conversation';
|
import { ConversationHandle } from 'components/Conversation';
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
|
||||||
import { Message } from 'components/Message';
|
import { Message } from 'components/Message';
|
||||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
@ -50,8 +49,13 @@ const defaultMessage: ChatMessage = {
|
|||||||
metadata: emptyMetadata,
|
metadata: emptyMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
interface CandidateChatPageProps {
|
||||||
(_props: BackstoryPageProps, ref): JSX.Element => {
|
sx?: SxProps; // Optional styles for the component
|
||||||
|
}
|
||||||
|
|
||||||
|
const CandidateChatPage = forwardRef<ConversationHandle, CandidateChatPageProps>(
|
||||||
|
(props: CandidateChatPageProps, ref): JSX.Element => {
|
||||||
|
const { sx } = props;
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const { selectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate } = useSelectedCandidate();
|
||||||
const [processingMessage, setProcessingMessage] = useState<
|
const [processingMessage, setProcessingMessage] = useState<
|
||||||
@ -240,6 +244,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
|||||||
flexShrink: 0 /* Prevent shrinking */,
|
flexShrink: 0 /* Prevent shrinking */,
|
||||||
},
|
},
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Paper elevation={2} sx={{ m: 1, p: 1 }}>
|
<Paper elevation={2} sx={{ m: 1, p: 1 }}>
|
||||||
|
@ -491,7 +491,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ mb: 1, justifyContent: 'center' }}>
|
<Box sx={{ mb: 1, justifyContent: 'center' }}>
|
||||||
Candidate{!isMobile && 'Selection'}
|
Candidate{!isMobile && ' Selection'}
|
||||||
</Box>
|
</Box>
|
||||||
{selectedCandidate !== null && !isMobile && (
|
{selectedCandidate !== null && !isMobile && (
|
||||||
<Box
|
<Box
|
||||||
@ -557,7 +557,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ mb: 1, justifyContent: 'center' }}>Job{!isMobile && 'Selection'}</Box>
|
<Box sx={{ mb: 1, justifyContent: 'center' }}>Job{!isMobile && ' Selection'}</Box>
|
||||||
{selectedJob !== null && !isMobile && (
|
{selectedJob !== null && !isMobile && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -2,9 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
SxProps,
|
SxProps,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
|
@ -141,7 +141,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
isCurrent: false,
|
isCurrent: false,
|
||||||
description: '',
|
description: '',
|
||||||
skills: [],
|
skills: [],
|
||||||
location: { city: '', country: '' },
|
location: { text: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -176,7 +176,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle form input changes
|
// Handle form input changes
|
||||||
const handleInputChange = (field: string, value: boolean | string): void => {
|
const handleInputChange = (field: string, value: boolean | string | Types.Location): void => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
@ -301,7 +301,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
isCurrent: false,
|
isCurrent: false,
|
||||||
description: '',
|
description: '',
|
||||||
skills: [],
|
skills: [],
|
||||||
location: { city: '', country: '' },
|
location: { text: '' },
|
||||||
});
|
});
|
||||||
setExperienceDialog(false);
|
setExperienceDialog(false);
|
||||||
setSnack('Experience added successfully!');
|
setSnack('Experience added successfully!');
|
||||||
@ -494,13 +494,34 @@ const CandidateProfile: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="entry">
|
<Box className="entry">
|
||||||
<Box className="title">
|
{editMode.basic ? (
|
||||||
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
|
<TextField
|
||||||
Location
|
fullWidth
|
||||||
</Box>
|
multiline
|
||||||
<Box className="value">
|
rows={3}
|
||||||
{candidate.location?.city || 'Not specified'} {candidate.location?.country || ''}
|
label="Location"
|
||||||
</Box>
|
value={formData.location?.text || ''}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
location: { ...formData.location, text: e.target.value },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Box className="title">
|
||||||
|
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||||
|
Location
|
||||||
|
</Box>
|
||||||
|
<Box className="value">
|
||||||
|
{candidate.location?.city
|
||||||
|
? `${candidate.location?.city} ${candidate.location?.country}`
|
||||||
|
: candidate.location?.text || 'Not provided'}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="entry">
|
<Box className="entry">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Generated TypeScript types from Pydantic models
|
// Generated TypeScript types from Pydantic models
|
||||||
// Source: src/backend/models.py
|
// Source: src/backend/models.py
|
||||||
// Generated on: 2025-07-11T20:02:47.037054
|
// Generated on: 2025-07-15T16:43:21.492940
|
||||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -19,7 +19,7 @@ export type ApiStatusType = "streaming" | "status" | "done" | "error";
|
|||||||
|
|
||||||
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
|
||||||
|
|
||||||
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match";
|
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "edit_resume" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match";
|
||||||
|
|
||||||
export type ChatSenderType = "user" | "assistant" | "system" | "information" | "warning" | "error";
|
export type ChatSenderType = "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
|
|
||||||
@ -284,7 +284,7 @@ export interface Certification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatContext {
|
export interface ChatContext {
|
||||||
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match";
|
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "edit_resume" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match";
|
||||||
relatedEntityId?: string;
|
relatedEntityId?: string;
|
||||||
relatedEntityType?: "job" | "candidate" | "employer";
|
relatedEntityType?: "job" | "candidate" | "employer";
|
||||||
additionalContext?: Record<string, any>;
|
additionalContext?: Record<string, any>;
|
||||||
@ -299,6 +299,7 @@ export interface ChatMessage {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
|
extraContext?: Record<string, any>;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
metadata: ChatMessageMetaData;
|
metadata: ChatMessageMetaData;
|
||||||
}
|
}
|
||||||
@ -349,6 +350,7 @@ export interface ChatMessageResume {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
|
extraContext?: Record<string, any>;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
metadata: ChatMessageMetaData;
|
metadata: ChatMessageMetaData;
|
||||||
resume: Resume;
|
resume: Resume;
|
||||||
@ -363,6 +365,7 @@ export interface ChatMessageSkillAssessment {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
|
extraContext?: Record<string, any>;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
metadata: ChatMessageMetaData;
|
metadata: ChatMessageMetaData;
|
||||||
skillAssessment: SkillAssessment;
|
skillAssessment: SkillAssessment;
|
||||||
@ -398,6 +401,7 @@ export interface ChatMessageUser {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
|
extraContext?: Record<string, any>;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,9 +812,10 @@ export interface Language {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Location {
|
export interface Location {
|
||||||
city: string;
|
text: string;
|
||||||
|
city?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
country: string;
|
country?: string;
|
||||||
postalCode?: string;
|
postalCode?: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
@ -1004,6 +1009,7 @@ export interface ResumeMessage {
|
|||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
role: "user" | "assistant" | "system" | "information" | "warning" | "error";
|
||||||
content: string;
|
content: string;
|
||||||
|
extraContext?: Record<string, any>;
|
||||||
tunables?: Tunables;
|
tunables?: Tunables;
|
||||||
resume: Resume;
|
resume: Resume;
|
||||||
}
|
}
|
||||||
@ -1094,6 +1100,9 @@ export interface Tunables {
|
|||||||
enableRAG: boolean;
|
enableRAG: boolean;
|
||||||
enableTools: boolean;
|
enableTools: boolean;
|
||||||
enableContext: boolean;
|
enableContext: boolean;
|
||||||
|
temperature: number;
|
||||||
|
topK: number;
|
||||||
|
threshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsageStats {
|
export interface UsageStats {
|
||||||
|
@ -644,6 +644,7 @@ Content: {content}
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
@ -692,6 +693,10 @@ Content: {content}
|
|||||||
rag_message = message
|
rag_message = message
|
||||||
context = self.get_rag_context(rag_message)
|
context = self.get_rag_context(rag_message)
|
||||||
|
|
||||||
|
if extra_context:
|
||||||
|
# Add extra context to the messages if provided
|
||||||
|
context = f"{context}\n\n".join(f"<{key}>\n{value}</{key}>" for key, value in extra_context.items())
|
||||||
|
|
||||||
# Add the RAG context to the messages if available
|
# Add the RAG context to the messages if available
|
||||||
if context:
|
if context:
|
||||||
messages.append(
|
messages.append(
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Literal, AsyncGenerator, ClassVar, Optional, Any
|
from typing import Literal, AsyncGenerator, ClassVar, Optional, Any
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
from database.core import RedisDatabase
|
from database.core import RedisDatabase
|
||||||
|
|
||||||
from .base import Agent, agent_registry
|
from .base import Agent, agent_registry
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
from models import ApiMessage, Tunables, ApiStatusType, LLMMessage
|
from models import ApiMessage, Tunables, ApiStatusType
|
||||||
|
|
||||||
|
|
||||||
system_message = """
|
system_message = """
|
||||||
@ -34,7 +33,6 @@ class CandidateChat(Agent):
|
|||||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
|
|
||||||
system_prompt: str = system_message
|
system_prompt: str = system_message
|
||||||
sessions: dict[str, list[LLMMessage]] = Field(default_factory=dict)
|
|
||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
self,
|
self,
|
||||||
@ -43,6 +41,7 @@ class CandidateChat(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
@ -62,9 +61,6 @@ Use that spelling instead of any spelling you may find in the <|context|>.
|
|||||||
|
|
||||||
{system_message}
|
{system_message}
|
||||||
"""
|
"""
|
||||||
if session_id not in self.sessions:
|
|
||||||
self.sessions[session_id] = [LLMMessage(role="user", content=prompt)]
|
|
||||||
|
|
||||||
async for message in super().generate(
|
async for message in super().generate(
|
||||||
llm=llm,
|
llm=llm,
|
||||||
model=model,
|
model=model,
|
||||||
|
255
src/backend/agents/edit_resume.py
Normal file
255
src/backend/agents/edit_resume.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import time
|
||||||
|
from typing import List, Literal, AsyncGenerator, ClassVar, Optional, Any
|
||||||
|
|
||||||
|
|
||||||
|
from database.core import RedisDatabase
|
||||||
|
|
||||||
|
from .base import Agent, agent_registry
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
from models import (
|
||||||
|
ApiActivityType,
|
||||||
|
ApiMessage,
|
||||||
|
ChatMessage,
|
||||||
|
ChatMessageError,
|
||||||
|
ChatMessageMetaData,
|
||||||
|
ChatMessageRagSearch,
|
||||||
|
ChatMessageStatus,
|
||||||
|
ChatMessageStreaming,
|
||||||
|
ChatMessageUser,
|
||||||
|
ChatOptions,
|
||||||
|
Tunables,
|
||||||
|
ApiStatusType,
|
||||||
|
UsageStats,
|
||||||
|
LLMMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EditResume(Agent):
|
||||||
|
"""
|
||||||
|
EditResume Agent
|
||||||
|
"""
|
||||||
|
|
||||||
|
agent_type: Literal["edit_resume"] = "edit_resume" # type: ignore
|
||||||
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
|
|
||||||
|
async def edit_resume(
|
||||||
|
self,
|
||||||
|
llm: Any,
|
||||||
|
model: str,
|
||||||
|
session_id: str,
|
||||||
|
prompt: str,
|
||||||
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
|
tunables: Optional[Tunables] = None,
|
||||||
|
temperature=0.2,
|
||||||
|
):
|
||||||
|
if not self.user:
|
||||||
|
error_message = ChatMessageError(session_id=session_id, content="No user set for chat generation.")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
user_message = ChatMessageUser(
|
||||||
|
session_id=session_id,
|
||||||
|
content=prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.add_chat_message(session_id, user_message.model_dump())
|
||||||
|
logger.info(f"💬 User message saved to database for session {session_id}")
|
||||||
|
|
||||||
|
# Create a pruned down message list based purely on the prompt and responses,
|
||||||
|
# discarding the full preamble generated by prepare_message
|
||||||
|
messages: List[LLMMessage] = [LLMMessage(role="system", content=self.system_prompt)]
|
||||||
|
# Add the conversation history to the messages
|
||||||
|
messages.extend(
|
||||||
|
[
|
||||||
|
LLMMessage(role=m["role"], content=m["content"])
|
||||||
|
for m in await database.get_recent_chat_messages(session_id=session_id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user.metrics.generate_count.labels(agent=self.agent_type).inc()
|
||||||
|
with self.user.metrics.generate_duration.labels(agent=self.agent_type).time():
|
||||||
|
context = None
|
||||||
|
rag_message: ChatMessageRagSearch | None = None
|
||||||
|
if self.user:
|
||||||
|
logger.info("Generating resume enhanced RAG results")
|
||||||
|
rag_prompt = ""
|
||||||
|
if extra_context:
|
||||||
|
# Add extra context to the messages if provided
|
||||||
|
rag_prompt = f"{context}\n\n".join(f"<{key}>\n{value}</{key}>" for key, value in extra_context.items())
|
||||||
|
rag_prompt += f"\n\nPrompt to respond to:\n{prompt}\n"
|
||||||
|
else:
|
||||||
|
rag_prompt = prompt
|
||||||
|
message = None
|
||||||
|
async for message in self.generate_rag_results(session_id=session_id, prompt=rag_prompt, top_k=10):
|
||||||
|
if message.status == ApiStatusType.ERROR:
|
||||||
|
yield message
|
||||||
|
return
|
||||||
|
# Only yield messages that are in a streaming state
|
||||||
|
if message.status == ApiStatusType.STATUS:
|
||||||
|
yield message
|
||||||
|
|
||||||
|
if not isinstance(message, ChatMessageRagSearch):
|
||||||
|
raise ValueError(f"Expected ChatMessageRagSearch, got {type(rag_message)}")
|
||||||
|
|
||||||
|
rag_message = message
|
||||||
|
context = self.get_rag_context(rag_message)
|
||||||
|
|
||||||
|
if extra_context:
|
||||||
|
# Add extra context to the messages if provided
|
||||||
|
context = f"{context}\n\n".join(f"<{key}>\n{value}</{key}>" for key, value in extra_context.items())
|
||||||
|
|
||||||
|
# Add the RAG context to the messages if available
|
||||||
|
if context:
|
||||||
|
messages.append(
|
||||||
|
LLMMessage(
|
||||||
|
role="user",
|
||||||
|
content=f"<|context|>\nThe following is context information about {self.user.full_name}:\n{context}\n</|context|>\n\nPrompt to respond to:\n{prompt}\n",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Only the actual user query is provided with the full context message
|
||||||
|
messages.append(LLMMessage(role="user", content=prompt))
|
||||||
|
|
||||||
|
# not use_tools
|
||||||
|
status_message = ChatMessageStatus(
|
||||||
|
session_id=session_id, activity=ApiActivityType.GENERATING, content="Generating response..."
|
||||||
|
)
|
||||||
|
yield status_message
|
||||||
|
|
||||||
|
# Set the response for streaming
|
||||||
|
self.set_optimal_context_size(llm, model, prompt=prompt)
|
||||||
|
|
||||||
|
options = ChatOptions(
|
||||||
|
seed=8911,
|
||||||
|
num_ctx=self.context_size,
|
||||||
|
temperature=temperature,
|
||||||
|
)
|
||||||
|
logger.info(f"Message options: {options.model_dump(exclude_unset=True)} with {len(messages)} messages")
|
||||||
|
content = ""
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
response = None
|
||||||
|
async for response in llm.chat_stream(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
options={
|
||||||
|
**options.model_dump(exclude_unset=True),
|
||||||
|
},
|
||||||
|
stream=True,
|
||||||
|
):
|
||||||
|
if not response:
|
||||||
|
error_message = ChatMessageError(session_id=session_id, content="No response from LLM.")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
content += response.content
|
||||||
|
|
||||||
|
if not response.finish_reason:
|
||||||
|
streaming_message = ChatMessageStreaming(
|
||||||
|
session_id=session_id,
|
||||||
|
content=response.content,
|
||||||
|
)
|
||||||
|
yield streaming_message
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
error_message = ChatMessageError(session_id=session_id, content="No response from LLM.")
|
||||||
|
yield error_message
|
||||||
|
return
|
||||||
|
|
||||||
|
self.user.collect_metrics(agent=self, response=response)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
|
chat_message = ChatMessage(
|
||||||
|
session_id=session_id,
|
||||||
|
tunables=tunables,
|
||||||
|
status=ApiStatusType.DONE,
|
||||||
|
content=content,
|
||||||
|
metadata=ChatMessageMetaData(
|
||||||
|
options=options,
|
||||||
|
usage=UsageStats(
|
||||||
|
eval_count=response.usage.eval_count,
|
||||||
|
eval_duration=response.usage.eval_duration,
|
||||||
|
prompt_eval_count=response.usage.prompt_eval_count,
|
||||||
|
prompt_eval_duration=response.usage.prompt_eval_duration,
|
||||||
|
),
|
||||||
|
rag_results=rag_message.content if rag_message else [],
|
||||||
|
llm_history=messages,
|
||||||
|
timers={
|
||||||
|
"llm_streamed": end_time - start_time,
|
||||||
|
"llm_with_tools": 0, # Placeholder for tool processing time
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await database.add_chat_message(session_id, chat_message.model_dump())
|
||||||
|
logger.info(f"🤖 Assistent response saved to database for session {session_id}")
|
||||||
|
|
||||||
|
# Add the user and chat messages to the conversation
|
||||||
|
yield chat_message
|
||||||
|
return
|
||||||
|
|
||||||
|
async def generate(
|
||||||
|
self,
|
||||||
|
llm: Any,
|
||||||
|
model: str,
|
||||||
|
session_id: str,
|
||||||
|
prompt: str,
|
||||||
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
|
tunables: Optional[Tunables] = None,
|
||||||
|
temperature=0.2,
|
||||||
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
user = self.user
|
||||||
|
if not user:
|
||||||
|
logger.error("User is not set for Edit Resume agent.")
|
||||||
|
raise ValueError("User must be set before generating candidate chat responses.")
|
||||||
|
self.system_prompt = """
|
||||||
|
You are a professional copy editor. Your task is to edit and enhance the provided resume content based on the requested edits.
|
||||||
|
|
||||||
|
**CRITICAL: NEVER INVENT OR FABRICATE ANY INFORMATION**
|
||||||
|
- DO NOT create any metrics, percentages, dollar amounts, timeframes, or statistics that are not explicitly stated in the original resume or <|context|>
|
||||||
|
- DO NOT add quantitative claims like "increased by X%", "reduced by X hours", "saved $X", "improved by X%" unless these exact figures are provided
|
||||||
|
- DO NOT estimate, approximate, or infer numerical data
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
- You are provided the current resume content in the <resume> section
|
||||||
|
- Only make edits that are requested by the user
|
||||||
|
- Do not add any additional information that is not present in the original resume
|
||||||
|
- Only add factual information supported by <|context|> or the <resume> content
|
||||||
|
- DO NOT make assumptions about the candidate's experience or skills
|
||||||
|
|
||||||
|
**For impact summaries specifically:**
|
||||||
|
- Focus on describing actions and responsibilities using strong action verbs
|
||||||
|
- Emphasize qualitative improvements like "streamlined processes", "enhanced efficiency", "improved workflow" WITHOUT adding invented metrics
|
||||||
|
- If no specific metrics are provided, describe the nature of the impact in general terms
|
||||||
|
- Example of ACCEPTABLE impact: "Streamlined resume analysis processes and reduced manual review requirements"
|
||||||
|
- Example of UNACCEPTABLE impact: "Streamlined resume analysis processes with a 40% reduction in manual review time" (unless that 40% is stated in the original content)
|
||||||
|
|
||||||
|
If the user asks a question about the resume, provide a brief answer based only on the content of the resume or the context provided.
|
||||||
|
|
||||||
|
If the user did not ask a question, return the entire resume with the requested edits applied while maintaining the current formatting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Generating resume edits for session {session_id} with prompt: {prompt} and context: {extra_context} and tunables: {tunables}"
|
||||||
|
)
|
||||||
|
async for message in self.edit_resume(
|
||||||
|
llm=llm,
|
||||||
|
model=model,
|
||||||
|
session_id=session_id,
|
||||||
|
prompt=prompt,
|
||||||
|
database=database,
|
||||||
|
temperature=temperature,
|
||||||
|
tunables=tunables,
|
||||||
|
extra_context=extra_context or {},
|
||||||
|
):
|
||||||
|
if message.status == ApiStatusType.ERROR:
|
||||||
|
yield message
|
||||||
|
return
|
||||||
|
yield message
|
||||||
|
|
||||||
|
|
||||||
|
# Register the base agent
|
||||||
|
agent_registry.register(EditResume._agent_type, EditResume)
|
@ -47,6 +47,7 @@ class ImageGenerator(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
||||||
|
@ -311,6 +311,7 @@ class GeneratePersona(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
) -> AsyncGenerator[ChatMessage | ChatMessageStatus | ChatMessageError | ChatMessageStreaming, None]:
|
||||||
|
@ -163,6 +163,7 @@ Avoid vague categorizations and be precise about whether skills are explicitly r
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
@ -24,6 +24,7 @@ class RagSearchChat(Agent):
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
@ -116,6 +116,7 @@ JSON RESPONSE:"""
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
database: RedisDatabase,
|
database: RedisDatabase,
|
||||||
|
extra_context: Optional[dict[str, str | int | float | bool]] = None,
|
||||||
tunables: Optional[Tunables] = None,
|
tunables: Optional[Tunables] = None,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
) -> AsyncGenerator[ApiMessage, None]:
|
) -> AsyncGenerator[ApiMessage, None]:
|
||||||
|
@ -14,7 +14,7 @@ def test_model_creation():
|
|||||||
print("🧪 Testing model creation...")
|
print("🧪 Testing model creation...")
|
||||||
|
|
||||||
# Create supporting objects
|
# Create supporting objects
|
||||||
location = Location(city="Austin", country="USA")
|
location = Location(text="Austin, TX USA")
|
||||||
skill = Skill(name="Python", category="Programming", level=SkillLevel.ADVANCED)
|
skill = Skill(name="Python", category="Programming", level=SkillLevel.ADVANCED)
|
||||||
|
|
||||||
# Create candidate
|
# Create candidate
|
||||||
|
@ -186,6 +186,7 @@ class ChatContextType(str, Enum):
|
|||||||
CANDIDATE_CHAT = "candidate_chat"
|
CANDIDATE_CHAT = "candidate_chat"
|
||||||
INTERVIEW_PREP = "interview_prep"
|
INTERVIEW_PREP = "interview_prep"
|
||||||
RESUME_REVIEW = "resume_review"
|
RESUME_REVIEW = "resume_review"
|
||||||
|
EDIT_RESUME = "edit_resume"
|
||||||
GENERAL = "general"
|
GENERAL = "general"
|
||||||
GENERATE_PERSONA = "generate_persona"
|
GENERATE_PERSONA = "generate_persona"
|
||||||
GENERATE_PROFILE = "generate_profile"
|
GENERATE_PROFILE = "generate_profile"
|
||||||
@ -373,6 +374,9 @@ class Tunables(BaseModel):
|
|||||||
enable_rag: bool = Field(default=True, alias=str("enableRAG"))
|
enable_rag: bool = Field(default=True, alias=str("enableRAG"))
|
||||||
enable_tools: bool = Field(default=True, alias=str("enableTools"))
|
enable_tools: bool = Field(default=True, alias=str("enableTools"))
|
||||||
enable_context: bool = Field(default=True, alias=str("enableContext"))
|
enable_context: bool = Field(default=True, alias=str("enableContext"))
|
||||||
|
temperature: float = Field(default=0.7, alias=str("temperature"))
|
||||||
|
top_k: int = Field(default=5, alias=str("topK"))
|
||||||
|
threshold: Optional[float] = Field(default=None, alias=str("threshold"))
|
||||||
|
|
||||||
|
|
||||||
class CandidateQuestion(BaseModel):
|
class CandidateQuestion(BaseModel):
|
||||||
@ -381,15 +385,38 @@ class CandidateQuestion(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Location(BaseModel):
|
class Location(BaseModel):
|
||||||
city: str
|
text: str
|
||||||
|
city: Optional[str] = None
|
||||||
state: Optional[str] = None
|
state: Optional[str] = None
|
||||||
country: str
|
country: Optional[str] = None
|
||||||
postal_code: Optional[str] = Field(default=None, alias=str("postalCode"))
|
postal_code: Optional[str] = Field(default=None, alias=str("postalCode"))
|
||||||
latitude: Optional[float] = None
|
latitude: Optional[float] = None
|
||||||
longitude: Optional[float] = None
|
longitude: Optional[float] = None
|
||||||
remote: Optional[bool] = None
|
remote: Optional[bool] = None
|
||||||
hybrid_options: Optional[List[str]] = Field(default=None, alias=str("hybridOptions"))
|
hybrid_options: Optional[List[str]] = Field(default=None, alias=str("hybridOptions"))
|
||||||
address: Optional[str] = None
|
address: Optional[str] = None
|
||||||
|
model_config = ConfigDict(populate_by_name=True, use_enum_values=True)
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def set_text_from_location(cls, data: Any) -> Any:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
# Check if text is missing, None, or empty string
|
||||||
|
if not data.get("text"):
|
||||||
|
city = data.get("city", "")
|
||||||
|
country = data.get("country", "")
|
||||||
|
|
||||||
|
# Build text from available location components
|
||||||
|
location_parts = []
|
||||||
|
if city:
|
||||||
|
location_parts.append(city)
|
||||||
|
if country:
|
||||||
|
location_parts.append(country)
|
||||||
|
|
||||||
|
# Set text to combined location or a default if both are empty
|
||||||
|
data["text"] = ", ".join(location_parts) if location_parts else "Unknown Location"
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class Skill(BaseModel):
|
class Skill(BaseModel):
|
||||||
@ -1141,11 +1168,13 @@ class SkillMatchRequest(BaseModel):
|
|||||||
skill: str
|
skill: str
|
||||||
regenerate: bool = Field(default=False, description="Whether to regenerate the skill match even if cached")
|
regenerate: bool = Field(default=False, description="Whether to regenerate the skill match even if cached")
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageUser(ApiMessage):
|
class ChatMessageUser(ApiMessage):
|
||||||
type: ApiMessageType = ApiMessageType.TEXT
|
type: ApiMessageType = ApiMessageType.TEXT
|
||||||
status: ApiStatusType = ApiStatusType.DONE
|
status: ApiStatusType = ApiStatusType.DONE
|
||||||
role: ChatSenderType = ChatSenderType.USER
|
role: ChatSenderType = ChatSenderType.USER
|
||||||
content: str = ""
|
content: str = ""
|
||||||
|
extra_context: Optional[Dict[str, str | int | float | bool]] = Field(default=None, alias=str("extraContext"))
|
||||||
tunables: Optional[Tunables] = None
|
tunables: Optional[Tunables] = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -1249,6 +1249,7 @@ async def update_candidate(
|
|||||||
return create_success_response(updated_candidate.model_dump(by_alias=True))
|
return create_success_response(updated_candidate.model_dump(by_alias=True))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(backstory_traceback.format_exc())
|
||||||
logger.error(f"❌ Update candidate error: {e}")
|
logger.error(f"❌ Update candidate error: {e}")
|
||||||
return JSONResponse(status_code=400, content=create_error_response("UPDATE_FAILED", str(e)))
|
return JSONResponse(status_code=400, content=create_error_response("UPDATE_FAILED", str(e)))
|
||||||
|
|
||||||
|
@ -82,6 +82,8 @@ async def stream_agent_response(
|
|||||||
session_id=user_message.session_id,
|
session_id=user_message.session_id,
|
||||||
prompt=user_message.content,
|
prompt=user_message.content,
|
||||||
database=database,
|
database=database,
|
||||||
|
extra_context=user_message.extra_context,
|
||||||
|
tunables=user_message.tunables,
|
||||||
):
|
):
|
||||||
if generated_message.status == ApiStatusType.ERROR:
|
if generated_message.status == ApiStatusType.ERROR:
|
||||||
logger.error(f"❌ AI generation error: {generated_message.content}")
|
logger.error(f"❌ AI generation error: {generated_message.content}")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user