chore: mkdocs deps, CHANGELOG, remove dead Resume Editor page, backlog gap items
This commit is contained in:
parent
41019269a2
commit
f78ac24657
5 changed files with 66 additions and 191 deletions
51
CHANGELOG.md
Normal file
51
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to Peregrine are documented here.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Expanded first-run wizard: 7-step onboarding with GPU detection, tier selection,
|
||||
resume upload/parsing, LLM inference test, search profile builder, integration cards
|
||||
- Tier system: free / paid / premium feature gates (`app/wizard/tiers.py`)
|
||||
- 13 integration drivers: Notion, Google Sheets, Airtable, Google Drive, Dropbox,
|
||||
OneDrive, MEGA, Nextcloud, Google Calendar, Apple Calendar, Slack, Discord,
|
||||
Home Assistant — with auto-discovery registry
|
||||
- Resume parser: PDF (pdfplumber) and DOCX (python-docx) + LLM structuring
|
||||
- `wizard_generate` background task type with iterative refinement (feedback loop)
|
||||
- Dismissible setup banners on Home page (13 contextual prompts)
|
||||
- Developer tab in Settings: tier override selectbox and wizard reset button
|
||||
- Integrations tab in Settings: connect / test / disconnect all 12 non-Notion drivers
|
||||
- HuggingFace token moved to Developer tab
|
||||
- `params` column in `background_tasks` for wizard task payloads
|
||||
- `wizard_complete`, `wizard_step`, `tier`, `dev_tier_override`, `dismissed_banners`,
|
||||
`effective_tier` added to UserProfile
|
||||
- MkDocs documentation site (Material theme, 20 pages)
|
||||
- `LICENSE-MIT` and `LICENSE-BSL`, `CONTRIBUTING.md`, `CHANGELOG.md`
|
||||
|
||||
### Changed
|
||||
- `app.py` wizard gate now checks `wizard_complete` flag in addition to file existence
|
||||
- Settings tabs reorganised: Integrations tab added, Developer tab conditionally shown
|
||||
- HF token removed from Services tab (now Developer-only)
|
||||
|
||||
### Removed
|
||||
- Dead `app/pages/3_Resume_Editor.py` (functionality lives in Settings → Resume Profile)
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0] — 2026-02-01
|
||||
|
||||
### Added
|
||||
- Initial release: JobSpy discovery pipeline, SQLite staging, Streamlit UI
|
||||
- Job Review, Apply Workspace, Interviews kanban, Interview Prep, Survey Assistant
|
||||
- LLM router with fallback chain (Ollama, vLLM, Claude Code wrapper, Anthropic)
|
||||
- Notion sync, email sync with IMAP classifier, company research with SearXNG
|
||||
- Background task runner with daemon threads
|
||||
- Vision service (moondream2) for survey screenshot analysis
|
||||
- Adzuna, The Ladders, and Craigslist custom board scrapers
|
||||
- Docker Compose profiles: remote, cpu, single-gpu, dual-gpu
|
||||
- `setup.sh` cross-platform dependency installer
|
||||
- `scripts/preflight.py` and `scripts/migrate.py`
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
# app/pages/3_Resume_Editor.py
|
||||
"""
|
||||
Resume Editor — form-based editor for the user's AIHawk profile YAML.
|
||||
FILL_IN fields highlighted in amber.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
import streamlit as st
|
||||
import yaml
|
||||
|
||||
st.set_page_config(page_title="Resume Editor", page_icon="📝", layout="wide")
|
||||
st.title("📝 Resume Editor")
|
||||
st.caption("Edit your application profile used by AIHawk for LinkedIn Easy Apply.")
|
||||
|
||||
RESUME_PATH = Path(__file__).parent.parent.parent / "aihawk" / "data_folder" / "plain_text_resume.yaml"
|
||||
|
||||
if not RESUME_PATH.exists():
|
||||
st.error(f"Resume file not found at `{RESUME_PATH}`. Is AIHawk cloned?")
|
||||
st.stop()
|
||||
|
||||
data = yaml.safe_load(RESUME_PATH.read_text()) or {}
|
||||
|
||||
|
||||
def field(label: str, value: str, key: str, help: str = "", password: bool = False) -> str:
|
||||
"""Render a text input, highlighted amber if value is FILL_IN or empty."""
|
||||
needs_attention = str(value).startswith("FILL_IN") or value == ""
|
||||
if needs_attention:
|
||||
st.markdown(
|
||||
'<p style="color:#F59E0B;font-size:0.8em;margin-bottom:2px">⚠️ Needs your attention</p>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
return st.text_input(label, value=value or "", key=key, help=help,
|
||||
type="password" if password else "default")
|
||||
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Personal Info ─────────────────────────────────────────────────────────────
|
||||
with st.expander("👤 Personal Information", expanded=True):
|
||||
info = data.get("personal_information", {})
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
name = field("First Name", info.get("name", ""), "pi_name")
|
||||
email = field("Email", info.get("email", ""), "pi_email")
|
||||
phone = field("Phone", info.get("phone", ""), "pi_phone")
|
||||
city = field("City", info.get("city", ""), "pi_city")
|
||||
with col2:
|
||||
surname = field("Last Name", info.get("surname", ""), "pi_surname")
|
||||
linkedin = field("LinkedIn URL", info.get("linkedin", ""), "pi_linkedin")
|
||||
zip_code = field("Zip Code", info.get("zip_code", ""), "pi_zip")
|
||||
dob = field("Date of Birth", info.get("date_of_birth", ""), "pi_dob",
|
||||
help="Format: MM/DD/YYYY")
|
||||
|
||||
# ── Education ─────────────────────────────────────────────────────────────────
|
||||
with st.expander("🎓 Education"):
|
||||
edu_list = data.get("education_details", [{}])
|
||||
updated_edu = []
|
||||
degree_options = ["Bachelor's Degree", "Master's Degree", "Some College",
|
||||
"Associate's Degree", "High School", "Other"]
|
||||
for i, edu in enumerate(edu_list):
|
||||
st.markdown(f"**Entry {i+1}**")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
inst = field("Institution", edu.get("institution", ""), f"edu_inst_{i}")
|
||||
field_study = st.text_input("Field of Study", edu.get("field_of_study", ""), key=f"edu_field_{i}")
|
||||
start = st.text_input("Start Year", edu.get("start_date", ""), key=f"edu_start_{i}")
|
||||
with col2:
|
||||
current_level = edu.get("education_level", "Some College")
|
||||
level_idx = degree_options.index(current_level) if current_level in degree_options else 2
|
||||
level = st.selectbox("Degree Level", degree_options, index=level_idx, key=f"edu_level_{i}")
|
||||
end = st.text_input("Completion Year", edu.get("year_of_completion", ""), key=f"edu_end_{i}")
|
||||
updated_edu.append({
|
||||
"education_level": level, "institution": inst, "field_of_study": field_study,
|
||||
"start_date": start, "year_of_completion": end, "final_evaluation_grade": "", "exam": {},
|
||||
})
|
||||
st.divider()
|
||||
|
||||
# ── Experience ────────────────────────────────────────────────────────────────
|
||||
with st.expander("💼 Work Experience"):
|
||||
exp_list = data.get("experience_details", [{}])
|
||||
if "exp_count" not in st.session_state:
|
||||
st.session_state.exp_count = len(exp_list)
|
||||
if st.button("+ Add Experience Entry"):
|
||||
st.session_state.exp_count += 1
|
||||
exp_list.append({})
|
||||
|
||||
updated_exp = []
|
||||
for i in range(st.session_state.exp_count):
|
||||
exp = exp_list[i] if i < len(exp_list) else {}
|
||||
st.markdown(f"**Position {i+1}**")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
pos = field("Job Title", exp.get("position", ""), f"exp_pos_{i}")
|
||||
company = field("Company", exp.get("company", ""), f"exp_co_{i}")
|
||||
period = field("Employment Period", exp.get("employment_period", ""), f"exp_period_{i}",
|
||||
help="e.g. 01/2022 - Present")
|
||||
with col2:
|
||||
location = st.text_input("Location", exp.get("location", ""), key=f"exp_loc_{i}")
|
||||
industry = st.text_input("Industry", exp.get("industry", ""), key=f"exp_ind_{i}")
|
||||
|
||||
responsibilities = st.text_area(
|
||||
"Key Responsibilities (one per line)",
|
||||
value="\n".join(
|
||||
r.get(f"responsibility_{j+1}", "") if isinstance(r, dict) else str(r)
|
||||
for j, r in enumerate(exp.get("key_responsibilities", []))
|
||||
),
|
||||
key=f"exp_resp_{i}", height=100,
|
||||
)
|
||||
skills = st.text_input(
|
||||
"Skills (comma-separated)",
|
||||
value=", ".join(exp.get("skills_acquired", [])),
|
||||
key=f"exp_skills_{i}",
|
||||
)
|
||||
resp_list = [{"responsibility_1": r.strip()} for r in responsibilities.splitlines() if r.strip()]
|
||||
skill_list = [s.strip() for s in skills.split(",") if s.strip()]
|
||||
updated_exp.append({
|
||||
"position": pos, "company": company, "employment_period": period,
|
||||
"location": location, "industry": industry,
|
||||
"key_responsibilities": resp_list, "skills_acquired": skill_list,
|
||||
})
|
||||
st.divider()
|
||||
|
||||
# ── Preferences ───────────────────────────────────────────────────────────────
|
||||
with st.expander("⚙️ Preferences & Availability"):
|
||||
wp = data.get("work_preferences", {})
|
||||
sal = data.get("salary_expectations", {})
|
||||
avail = data.get("availability", {})
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
salary_range = st.text_input("Salary Range (USD)", sal.get("salary_range_usd", ""),
|
||||
key="pref_salary", help="e.g. 120000 - 180000")
|
||||
notice = st.text_input("Notice Period", avail.get("notice_period", "2 weeks"), key="pref_notice")
|
||||
with col2:
|
||||
remote_work = st.checkbox("Open to Remote", value=wp.get("remote_work", "Yes") == "Yes", key="pref_remote")
|
||||
relocation = st.checkbox("Open to Relocation", value=wp.get("open_to_relocation", "No") == "Yes", key="pref_reloc")
|
||||
assessments = st.checkbox("Willing to complete assessments",
|
||||
value=wp.get("willing_to_complete_assessments", "Yes") == "Yes", key="pref_assess")
|
||||
bg_checks = st.checkbox("Willing to undergo background checks",
|
||||
value=wp.get("willing_to_undergo_background_checks", "Yes") == "Yes", key="pref_bg")
|
||||
drug_tests = st.checkbox("Willing to undergo drug tests",
|
||||
value=wp.get("willing_to_undergo_drug_tests", "No") == "Yes", key="pref_drug")
|
||||
|
||||
# ── Self-ID ───────────────────────────────────────────────────────────────────
|
||||
with st.expander("🏳️🌈 Self-Identification (optional)"):
|
||||
sid = data.get("self_identification", {})
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
gender = st.text_input("Gender identity", sid.get("gender", "Non-binary"), key="sid_gender",
|
||||
help="Select 'Non-binary' or 'Prefer not to say' when options allow")
|
||||
pronouns = st.text_input("Pronouns", sid.get("pronouns", "Any"), key="sid_pronouns")
|
||||
ethnicity = field("Ethnicity", sid.get("ethnicity", ""), "sid_ethnicity",
|
||||
help="'Prefer not to say' is always an option")
|
||||
with col2:
|
||||
vet_options = ["No", "Yes", "Prefer not to say"]
|
||||
veteran = st.selectbox("Veteran status", vet_options,
|
||||
index=vet_options.index(sid.get("veteran", "No")), key="sid_vet")
|
||||
dis_options = ["Prefer not to say", "No", "Yes"]
|
||||
disability = st.selectbox("Disability disclosure", dis_options,
|
||||
index=dis_options.index(sid.get("disability", "Prefer not to say")),
|
||||
key="sid_dis")
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Save ──────────────────────────────────────────────────────────────────────
|
||||
if st.button("💾 Save Resume Profile", type="primary", use_container_width=True):
|
||||
data["personal_information"] = {
|
||||
**data.get("personal_information", {}),
|
||||
"name": name, "surname": surname, "email": email, "phone": phone,
|
||||
"city": city, "zip_code": zip_code, "linkedin": linkedin, "date_of_birth": dob,
|
||||
}
|
||||
data["education_details"] = updated_edu
|
||||
data["experience_details"] = updated_exp
|
||||
data["salary_expectations"] = {"salary_range_usd": salary_range}
|
||||
data["availability"] = {"notice_period": notice}
|
||||
data["work_preferences"] = {
|
||||
**data.get("work_preferences", {}),
|
||||
"remote_work": "Yes" if remote_work else "No",
|
||||
"open_to_relocation": "Yes" if relocation else "No",
|
||||
"willing_to_complete_assessments": "Yes" if assessments else "No",
|
||||
"willing_to_undergo_background_checks": "Yes" if bg_checks else "No",
|
||||
"willing_to_undergo_drug_tests": "Yes" if drug_tests else "No",
|
||||
}
|
||||
data["self_identification"] = {
|
||||
"gender": gender, "pronouns": pronouns, "veteran": veteran,
|
||||
"disability": disability, "ethnicity": ethnicity,
|
||||
}
|
||||
RESUME_PATH.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True))
|
||||
st.success("✅ Profile saved!")
|
||||
st.balloons()
|
||||
|
|
@ -7,12 +7,20 @@ Unscheduled ideas and deferred features. Roughly grouped by area.
|
|||
## Settings / Data Management
|
||||
|
||||
- **Backup / Restore / Teleport** — Settings panel option to export a full config snapshot (user.yaml + all gitignored configs) as a zip, restore from a snapshot, and "teleport" (export + import to a new machine or Docker volume). Useful for migrations, multi-machine setups, and safe wizard testing.
|
||||
- **Complete Google Drive integration test()** — `scripts/integrations/google_drive.py` `test()` currently only checks that the credentials file exists (TODO comment). Implement actual Google Drive API call using `google-api-python-client` to verify the token works.
|
||||
|
||||
---
|
||||
|
||||
## First-Run Wizard
|
||||
|
||||
- **Wire real LLM test in Step 5 (Inference)** — `app/wizard/step_inference.py` validates an `endpoint_confirmed` boolean flag only. Replace with an actual LLM call: submit a minimal prompt to the configured endpoint, show pass/fail, and only set `endpoint_confirmed: true` on success. Should test whichever backend the user selected (Ollama, vLLM, Anthropic, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Cover Letter / Resume Generation
|
||||
|
||||
- **Iterative refinement feedback loop** — Apply Workspace cover letter generator: show previous result + a "Feedback / changes requested" text area + "Regenerate" button. Pass `previous_result` and `feedback` through `generate()` in `scripts/generate_cover_letter.py` to the LLM prompt. Same pattern for resume bullet expansion in the wizard (`wizard_generate: expand_bullets`). Backend already supports `previous_result`/`feedback` in `wizard_generate` tasks (added to `_run_wizard_generate`).
|
||||
- **Apply Workspace refinement UI ready to wire** — Remaining work: add a "Feedback / changes requested" text area and "Regenerate" button in `app/pages/4_Apply.py`, pass both fields through `submit_task` → `_run_wizard_generate`. Backend is complete.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -66,3 +66,6 @@ dependencies:
|
|||
- pytest>=9.0
|
||||
- pytest-cov
|
||||
- pytest-mock
|
||||
# Documentation
|
||||
- mkdocs>=1.5
|
||||
- mkdocs-material>=9.5
|
||||
|
|
|
|||
|
|
@ -61,3 +61,7 @@ pytest>=9.0
|
|||
pytest-cov
|
||||
pytest-mock
|
||||
lxml
|
||||
|
||||
# ── Documentation ────────────────────────────────────────────────────────
|
||||
mkdocs>=1.5
|
||||
mkdocs-material>=9.5
|
||||
|
|
|
|||
Loading…
Reference in a new issue