From d9f2b452e8823e69cc72c69430c8fcc266021e4a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Thu, 26 Feb 2026 13:40:52 -0800 Subject: [PATCH] refactor: replace sidebar LLM generate panel with inline field buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the dropdown-based sidebar panel in favour of ✨ Generate buttons placed directly below Career Summary, Voice & Personality, and each Mission & Values row. Prompts now incorporate the live field value as a draft to improve, plus resume experience bullets as context for Career Summary. --- app/pages/2_Settings.py | 123 +++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 72 deletions(-) diff --git a/app/pages/2_Settings.py b/app/pages/2_Settings.py index 327736d..d15101e 100644 --- a/app/pages/2_Settings.py +++ b/app/pages/2_Settings.py @@ -95,10 +95,9 @@ if _show_dev_tab: _all_tabs = st.tabs(_tab_names) tab_profile, tab_resume, tab_search, tab_system, tab_finetune, tab_license = _all_tabs[:6] -# ── Sidebar LLM generate panel ──────────────────────────────────────────────── -# Paid-tier feature: generates content for any LLM-injectable profile field. -# Writes directly into session state keyed to the target widget's `key=` param, -# then reruns so the field picks up the new value automatically. +# ── Inline LLM generate buttons ─────────────────────────────────────────────── +# Paid-tier feature: ✨ Generate buttons sit directly below each injectable field. +# Writes into session state keyed to the widget's `key=` param, then reruns. from app.wizard.tiers import can_use as _cu _gen_panel_active = bool(_profile) and _cu( _profile.effective_tier if _profile else "free", "llm_career_summary" @@ -113,73 +112,6 @@ for _fk, _fv in [ if _fk not in st.session_state: st.session_state[_fk] = _fv -if _gen_panel_active: - @st.fragment - def _generate_sidebar_panel(): - st.markdown("**✨ AI Generate**") - st.caption("Select a field, add an optional hint, then click Generate. The result is injected directly into the field.") - - _GEN_FIELDS = { - "Career Summary": "profile_career_summary", - "Voice & Personality": "profile_candidate_voice", - "Mission Note": "_mission_note_preview", - } - _tgt_label = st.selectbox( - "Field", list(_GEN_FIELDS.keys()), - key="gen_panel_target", label_visibility="collapsed", - ) - _tgt_key = _GEN_FIELDS[_tgt_label] - - if _tgt_label == "Mission Note": - _gen_domain = st.text_input("Domain", placeholder="e.g. animal welfare", key="gen_panel_domain") - else: - _gen_domain = None - - _gen_hint = st.text_input("Hint (optional)", placeholder="e.g. emphasise leadership", key="gen_panel_hint") - - if st.button("✨ Generate", type="primary", key="gen_panel_run", use_container_width=True): - _p = _profile - if _tgt_label == "Career Summary": - _prompt = ( - f"Write a 3-4 sentence professional career summary for {_p.name} in first person, " - f"suitable for use in cover letters and LLM prompts. " - f"Current summary: {_p.career_summary}. " - ) - elif _tgt_label == "Voice & Personality": - _prompt = ( - f"Write a 2-4 sentence voice and personality descriptor for {_p.name} " - f"to guide an LLM writing cover letters in their authentic style. " - f"Describe personality traits, tone, and writing voice — not a bio. " - f"Career context: {_p.career_summary}. " - ) - else: - _prompt = ( - f"Write a 2-3 sentence personal mission alignment note (first person, warm, authentic) " - f"for {_p.name} in the '{_gen_domain or 'this'}' domain for use in cover letters. " - f"Background: {_p.career_summary}. " - f"Voice: {_p.candidate_voice}. " - "Do not start with 'I'." - ) - if _gen_hint: - _prompt += f" Additional guidance: {_gen_hint}." - with st.spinner("Generating…"): - from scripts.llm_router import LLMRouter as _LR - _result = _LR().complete(_prompt).strip() - st.session_state[_tgt_key] = _result - if _tgt_label != "Mission Note": - st.rerun() - - if st.session_state.get("_mission_note_preview"): - st.caption("Copy into a Mission & Values domain row:") - st.text_area("", st.session_state["_mission_note_preview"], - height=80, key="gen_mission_display") - if st.button("✓ Clear", key="gen_mission_clear", use_container_width=True): - del st.session_state["_mission_note_preview"] - st.rerun() - - with st.sidebar: - _generate_sidebar_panel() - with tab_profile: from scripts.user_profile import UserProfile as _UP, _DEFAULTS as _UP_DEFAULTS import yaml as _yaml_up @@ -197,12 +129,55 @@ with tab_profile: u_linkedin = c2.text_input("LinkedIn URL", _u.get("linkedin", "")) u_summary = st.text_area("Career Summary (used in LLM prompts)", key="profile_career_summary", height=100) + if _gen_panel_active: + if st.button("✨ Generate", key="gen_career_summary", help="Generate career summary with AI"): + _cs_draft = st.session_state.get("profile_career_summary", "").strip() + _cs_resume_ctx = "" + if RESUME_PATH.exists(): + _rdata = load_yaml(RESUME_PATH) + _exps = (_rdata.get("experience_details") or [])[:3] + _exp_lines = [] + for _e in _exps: + _t = _e.get("position", "") + _c = _e.get("company", "") + _b = "; ".join((_e.get("key_responsibilities") or [])[:2]) + _exp_lines.append(f"- {_t} at {_c}: {_b}") + _cs_resume_ctx = "\n".join(_exp_lines) + _cs_prompt = ( + f"Write a 3-4 sentence professional career summary for {_profile.name} in first person, " + f"suitable for use in cover letters and LLM prompts. " + f"Return only the summary, no preamble.\n" + ) + if _cs_draft: + _cs_prompt += f"\nExisting draft to improve or replace:\n{_cs_draft}\n" + if _cs_resume_ctx: + _cs_prompt += f"\nRecent experience for context:\n{_cs_resume_ctx}\n" + with st.spinner("Generating…"): + from scripts.llm_router import LLMRouter as _LLMRouter + st.session_state["profile_career_summary"] = _LLMRouter().complete(_cs_prompt).strip() + st.rerun() u_voice = st.text_area( "Voice & Personality (shapes cover letter tone)", key="profile_candidate_voice", height=80, help="Personality traits and writing voice that the LLM uses to write authentically in your style. Never disclosed in applications.", ) + if _gen_panel_active: + if st.button("✨ Generate", key="gen_candidate_voice", help="Generate voice descriptor with AI"): + _vc_draft = st.session_state.get("profile_candidate_voice", "").strip() + _vc_prompt = ( + f"Write a 2-4 sentence voice and personality descriptor for {_profile.name} " + f"to guide an LLM writing cover letters in their authentic style. " + f"Describe personality traits, tone, and writing voice — not a bio. " + f"Career context: {_profile.career_summary}. " + f"Return only the descriptor, no preamble.\n" + ) + if _vc_draft: + _vc_prompt += f"\nExisting descriptor to improve:\n{_vc_draft}\n" + with st.spinner("Generating…"): + from scripts.llm_router import LLMRouter as _LLMRouter + st.session_state["profile_candidate_voice"] = _LLMRouter().complete(_vc_prompt).strip() + st.rerun() with st.expander("🎯 Mission & Values"): st.caption("Industry passions and causes you care about. Used to inject authentic Para 3 alignment when a company matches. Never disclosed in applications.") @@ -242,6 +217,7 @@ with tab_profile: if _can_generate: if st.button("✨", key=f"mgen_{_idx}", help="Generate alignment note with AI"): _domain = _row["key"].replace("_", " ") + _m_draft = st.session_state.get(f"mval_{_idx}", _row["value"]).strip() _gen_prompt = ( f"Write a 2–3 sentence personal mission alignment note " f"(first person, warm, authentic) for {_profile.name if _profile else 'the candidate'} " @@ -249,8 +225,11 @@ with tab_profile: f"Background: {_profile.career_summary if _profile else ''}. " f"Voice: {_profile.candidate_voice if _profile else ''}. " f"The note should explain their genuine personal connection and why they'd " - f"be motivated working in this space. Do not start with 'I'." + f"be motivated working in this space. Do not start with 'I'. " + f"Return only the note, no preamble.\n" ) + if _m_draft: + _gen_prompt += f"\nExisting note to improve:\n{_m_draft}\n" with st.spinner(f"Generating note for {_domain}…"): from scripts.llm_router import LLMRouter as _LLMRouter _row["value"] = _LLMRouter().complete(_gen_prompt).strip()