diff --git a/Dockerfile b/Dockerfile index bcdb5f9..12d781b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -294,17 +294,16 @@ RUN apt-get update \ WORKDIR /opt/ollama # Download the nightly ollama release from ipex-llm -#ENV OLLAMA_VERSION=https://github.com/intel/ipex-llm/releases/download/v2.2.0/ollama-ipex-llm-2.2.0-ubuntu.tgz -#ENV OLLAMA_VERSION=https://github.com/intel/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250415-ubuntu.tgz # NOTE: NO longer at github.com/intel -- now at ipex-llm # This version does not work: -# ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250429-ubuntu.tgz - ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.2.0/ollama-ipex-llm-2.2.0-ubuntu.tgz -#ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250429-ubuntu.tgz + +# Does not work -- crashes +# ENV OLLAMA_VERSION=https://github.com/ipex-llm/ipex-llm/releases/download/v2.3.0-nightly/ollama-ipex-llm-2.3.0b20250612-ubuntu.tgz + RUN wget -qO - ${OLLAMA_VERSION} | \ tar --strip-components=1 -C . -xzv diff --git a/docker-compose.yml b/docker-compose.yml index 1a19592..fdc591d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ services: context: . dockerfile: Dockerfile target: backstory - container_name: backstory #image: backstory + container_name: backstory restart: "always" env_file: - .env @@ -15,6 +15,7 @@ services: - MODEL_NAME=${MODEL_NAME:-qwen2.5:7b} - REDIS_URL=redis://redis:6379 - REDIS_DB=0 + - SSL_ENABLED=true devices: - /dev/dri:/dev/dri depends_on: @@ -50,6 +51,7 @@ services: - .env environment: - PRODUCTION=1 + - FRONTEND_URL=https://backstory.ketrenos.com - MODEL_NAME=${MODEL_NAME:-qwen2.5:7b} - REDIS_URL=redis://redis:6379 - REDIS_DB=1 @@ -58,6 +60,7 @@ services: - /dev/dri:/dev/dri depends_on: - ollama + - redis networks: - internal ports: diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index ecc12ab..2217ab6 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "ai.ketrenos.com", - "name": "Ketrenos AI Chat", + "short_name": "backstory.ketrenos.com", + "name": "Backstory", "icons": [ { "src": "favicon.ico", diff --git a/frontend/src/components/ui/ResumeInfo.tsx b/frontend/src/components/ui/ResumeInfo.tsx index 94c0424..b5c3577 100644 --- a/frontend/src/components/ui/ResumeInfo.tsx +++ b/frontend/src/components/ui/ResumeInfo.tsx @@ -55,7 +55,7 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js'; import { useReactToPrint } from 'react-to-print'; import { useAuth } from 'hooks/AuthContext'; -import { useAppState } from 'hooks/GlobalContext'; +import { useAppState, useSelectedJob } from 'hooks/GlobalContext'; import { StyledMarkdown } from 'components/StyledMarkdown'; import { Resume } from 'types/types'; import { BackstoryTextField } from 'components/BackstoryTextField'; @@ -355,10 +355,11 @@ const resumeStyles: Record = { // Styled Header Component interface BackstoryStyledResumeProps { candidate: Types.Candidate; + job?: Types.Job; style: ResumeStyle; } -const StyledFooter: React.FC = ({ candidate, style }) => { +const StyledFooter: React.FC = ({ candidate, job, style }) => { return ( <> = ({ candidate, style } color: style.color.secondary, }} > - Ask any questions you may have at my Backstory... + Dive deeper into my qualifications at Backstory... {candidate?.username - ? `https://backstory.ketrenos.com/u/${candidate?.username}` + ? `${window.location.protocol}://${window.location.host}/u/${candidate?.username}` : 'backstory'}   @@ -537,7 +538,7 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { const [status, setStatus] = useState(''); const [statusType, setStatusType] = useState(null); const [error, setError] = useState(null); - const [selectedStyle, setSelectedStyle] = useState('modern'); + const [selectedStyle, setSelectedStyle] = useState('corporate'); const printContentRef = useRef(null); const reactToPrintFn = useReactToPrint({ @@ -785,13 +786,11 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { avatar={} sx={{ p: 0, pb: 1 }} action={ - isAdmin && ( - - - - - - ) + + + + + } /> @@ -899,19 +898,76 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { fullScreen={true} > - Edit Resume Content - - Resume for {activeResume.candidate?.fullName || activeResume.candidateId},{' '} - {activeResume.job?.title || 'No Job Title Assigned'},{' '} - {activeResume.job?.company || 'No Company Assigned'} - - - Resume ID: # {activeResume.id} - - - Last saved:{' '} - {activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'} - + + + Edit Resume Content + + Resume for {activeResume.candidate?.fullName || activeResume.candidateId},{' '} + {activeResume.job?.title || 'No Job Title Assigned'},{' '} + {activeResume.job?.company || 'No Company Assigned'} + + + Resume ID: # {activeResume.id} + + + Last saved:{' '} + {activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'} + + + + + {/* Style Selector */} + + Resume Style + + + + + + } label="Markdown" /> + {activeResume.systemPrompt && ( + } label="System Prompt" /> + )} + {activeResume.systemPrompt && ( + } label="Prompt" /> + )} + } label="Preview" /> + } label="Print" /> + } label="Regenerate" /> + + + = (props: ResumeInfoProps) => { overflow: 'hidden', }} > - - - } label="Markdown" /> - {activeResume.systemPrompt && ( - } label="System Prompt" /> - )} - {activeResume.systemPrompt && ( - } label="Prompt" /> - )} - } label="Preview" /> - } label="Print" /> - } label="Regenerate" /> - - - {/* Style Selector */} - - - - Resume Style - - - - - {/* Style Preview Chip */} - } - label={currentStyle.name} - sx={{ - backgroundColor: currentStyle.color.primary, - color: '#ffffff', - fontWeight: 'bold', - }} - /> - - {status && ( @@ -1117,8 +1114,12 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { {/* QR Code Footer */} - {activeResume.candidate && ( - + {activeResume.candidate && activeResume.job && ( + )} diff --git a/src/backend/defines.py b/src/backend/defines.py index 45566dc..fa31cb2 100644 --- a/src/backend/defines.py +++ b/src/backend/defines.py @@ -2,6 +2,8 @@ import os ollama_api_url = "http://ollama:11434" # Default Ollama local endpoint +frontend_url = os.getenv("FRONTEND_URL", "https://backstory.ketrenos.com") + user_dir = "/opt/backstory/users" user_info_file = "info.json" # Relative to "{user_dir}/{user}" default_username = "jketreno" @@ -16,8 +18,8 @@ persist_directory = "db" # Relative to "{user_dir}/{user}" # model = "gemma3:4b" # Gemma Requires newer ollama https://ai.google.dev/gemma/terms # model = "llama3.2" # Llama Good results; qwen seems slightly better https://huggingface.co/meta-llama/Llama-3.2-1B/blob/main/LICENSE.txt # model = "mistral:7b" # Apache 2.0 Tool calls don"t work +# model = "qwen3:8b" # Apache 2.0 Requires newer ollama model = "qwen2.5:7b" # Apache 2.0 Good results -# model = "qwen3:8b" # Apache 2.0 Requires newer ollama model = os.getenv("MODEL_NAME", model) # Embedding model for producing vectors to use in RAG diff --git a/src/backend/routes/auth.py b/src/backend/routes/auth.py index c0143b0..04edcfb 100644 --- a/src/backend/routes/auth.py +++ b/src/backend/routes/auth.py @@ -1170,7 +1170,7 @@ async def verify_email( dir_path = os.path.join(defines.user_dir, username) if not os.path.exists(dir_path): os.makedirs(dir_path, exist_ok=True) - qrobj = pyqrcode.create(f"https://backstory.ketrenos.com/u/{username}") + qrobj = pyqrcode.create(f"{defines.frontend_url}/u/{username}") dir_path = os.path.join(dir_path, "qrcode.png") with open(dir_path, "wb") as f: qrobj.png(f, scale=2) diff --git a/src/backend/routes/candidates.py b/src/backend/routes/candidates.py index 1dec917..8e4a7d7 100644 --- a/src/backend/routes/candidates.py +++ b/src/backend/routes/candidates.py @@ -714,9 +714,10 @@ async def get_candidate_profile_image( status_code=500, content=create_error_response("FETCH_ERROR", "Failed to retrieve profile image") ) -@router.get("/qr-code/{candidate_id}") +@router.get("/qr-code/{candidate_id}/{job_id}") async def get_candidate_qr_code( candidate_id: str = Path(..., description="ID of the candidate"), + job_id: Optional[str] = Path(..., description="ID of the candidate"), # current_user = Depends(get_current_user), database: RedisDatabase = Depends(get_database), ): @@ -735,13 +736,29 @@ async def get_candidate_qr_code( return JSONResponse(status_code=404, content=create_error_response("NOT_FOUND", "Candidate not found")) candidate = Candidate.model_validate(candidates_list[0]) - file_path = os.path.join(defines.user_dir, candidate.username, "qrcode.png") - file_path = pathlib.Path(file_path) + + job = None + if job_id: + job_data = await database.get_job(job_id) + if not job_data: + logger.warning(f"⚠️ Job not found for ID: {job_id}") + return JSONResponse( + status_code=404, + content=create_error_response("JOB_NOT_FOUND", f"Job with id '{job_id}' not found"), + ) + job = Job.model_validate(job_data) + + file_name = f"{job.id}.png" if job else "qrcode.png" + file_path = pathlib.Path(defines.user_dir) / candidate.username / file_name if not file_path.exists(): - logger.error(f"❌ QR code not found on disk: {file_path}") - return JSONResponse( - status_code=404, content=create_error_response("FILE_NOT_FOUND", "QR code not found on disk") - ) + import pyqrcode + + if job: + qrobj = pyqrcode.create(f"{defines.frontend_url}/job-analysis/{candidate.id}/{job.id}") + else: + qrobj = pyqrcode.create(f"{defines.frontend_url}/u/{candidate.id}") + with open(file_path, "wb") as f: + qrobj.png(f, scale=2) return FileResponse( file_path, media_type=f"image/{file_path.suffix[1:]}", # Get extension without dot