Spaces:
Sleeping
Sleeping
File size: 7,760 Bytes
6303ae6 c9d9189 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 357ee36 6303ae6 c9d9189 6303ae6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | """Screen 4: Confirm details (developer/admin only).
This screen is gated behind ``SHOW_DEBUG_PANEL=true``. Normal users never
see it — their job details are confirmed automatically in the backend
right after extraction (see :mod:`app.ui.screenshot_screen`). In debug
mode it lets a developer review/edit the extracted job fields before the
analysis runs. Every value is editable; unknown values stay as
"Not visible" so the tool never scores against a guess. Fields are
grouped into four simple sections to make review quick.
"""
from __future__ import annotations
import re
import streamlit as st
from app.config import get_settings
from app.services.screenshot_parser import SCREENSHOT_FIELDS, _sanitize_field_value
from app.ui import theme
NOT_VISIBLE = "Not visible"
FIELD_LABELS: dict[str, str] = {
"job_title": "Job title",
"job_description": "Job description",
"client_need": "Client need",
"required_deliverables": "Required deliverables",
"required_skills": "Required skills",
"budget_or_rate": "Budget or rate",
"project_type": "Project type",
"experience_level": "Experience level",
"project_duration": "Project duration",
"posted_date": "Posted",
"proposal_count": "Proposals submitted",
"payment_verification": "Payment verified",
"client_rating": "Client rating",
"client_total_spend": "Client total spend",
"hire_rate": "Hire rate",
"client_location": "Client location",
"connects_required": "Connects required",
"contract_type": "Contract type",
"client_jobs_posted": "Client jobs posted",
"client_hires": "Client hires",
"client_last_active": "Client last active",
"hidden_keyword": "Hidden keyword",
"screening_questions": "Screening questions",
}
# Grouped, user-friendly sections.
FIELD_GROUPS: list[tuple[str, list[str]]] = [
(
"Job basics",
[
"job_title",
"job_description",
"client_need",
"project_type",
"contract_type",
"experience_level",
"project_duration",
],
),
(
"Client details",
[
"payment_verification",
"client_rating",
"client_total_spend",
"hire_rate",
"client_jobs_posted",
"client_hires",
"client_location",
"client_last_active",
],
),
(
"Budget & competition",
["budget_or_rate", "proposal_count", "posted_date", "connects_required"],
),
(
"Skills & requirements",
[
"required_skills",
"required_deliverables",
"screening_questions",
"hidden_keyword",
],
),
]
# Fields that benefit from a multi-line box.
LONG_FIELDS = {
"job_description",
"client_need",
"required_deliverables",
"required_skills",
"screening_questions",
}
def _empty_field() -> dict[str, str]:
return {"value": NOT_VISIBLE, "confidence": "low", "source": "not visible"}
def _clean_value(raw: object, key: str) -> str:
"""Sanitise a field value exactly as the normal flow's ``confirm_fields``
does (whitespace, line breaks, length cap); blank reads as "Not visible"."""
value = _sanitize_field_value(str(raw or ""), key=key)
if not value or value.lower() == NOT_VISIBLE.lower():
return NOT_VISIBLE
return value
def _resolve_source(original_value: str, original_source: str, new_value: str) -> str:
if new_value == NOT_VISIBLE:
return "not visible"
if original_value == NOT_VISIBLE:
return "manually entered"
if new_value != original_value:
return "user corrected"
return original_source
def _render_field(key: str, extracted: dict, debug: bool) -> None:
label = FIELD_LABELS.get(key, key)
field = extracted.get(key) or _empty_field()
value = field.get("value", NOT_VISIBLE)
widget_key = f"confirm_field__{key}"
if key in LONG_FIELDS:
st.text_area(label, value=value, key=widget_key, height=90)
else:
st.text_input(label, value=value, key=widget_key)
if debug:
st.caption(
f"confidence `{field.get('confidence', 'low')}` • "
f"source `{field.get('source', 'not visible')}`"
)
def _commit(extracted: dict) -> None:
confirmed: dict[str, dict[str, str]] = {}
for key in SCREENSHOT_FIELDS:
original = extracted.get(key) or _empty_field()
original_value = _clean_value(original.get("value", NOT_VISIBLE), key)
widget_key = f"confirm_field__{key}"
# A field with no box on this page keeps what the screenshot read
# instead of being wiped to "Not visible".
if widget_key in st.session_state:
new_value = _clean_value(st.session_state[widget_key], key)
else:
new_value = original_value
confirmed[key] = {
"value": new_value,
"confidence": original.get("confidence", "low"),
"source": _resolve_source(
original_value=original_value,
original_source=original.get("source", "not visible"),
new_value=new_value,
),
}
st.session_state.confirmed_job_fields = confirmed
st.session_state.fields_confirmed = True
# Force the analysis to recompute from the confirmed values. The
# confirmed fields themselves were just set above, so we clear only
# the derived per-opportunity outputs (and the fingerprint, so the
# analysis screen treats this as a new opportunity).
for k in (
"current_job_fingerprint",
"match_data",
"scoring_result",
"recommendation_result",
"generated_proposal",
"verified_proposal",
"proposal_context",
"selected_evidence_for_proposal",
):
st.session_state[k] = None
st.session_state.current_step = "analysis"
def render() -> None:
settings = get_settings()
debug = bool(getattr(settings, "show_debug_panel", False))
if not debug:
# Confirm Details is a developer/admin-only screen. Normal users never
# reach it — job details are confirmed automatically in the backend
# after extraction. If reached directly, bounce to the right step.
st.session_state.current_step = (
"analysis" if st.session_state.get("fields_confirmed") else "screenshot"
)
return
extracted = st.session_state.get("extracted_job_fields")
if not extracted:
st.error(
"This step is locked. Upload a screenshot and extract job details first."
)
if st.button("Back to Job Screenshot", key="back_to_screenshot_from_confirm"):
st.session_state.current_step = "screenshot"
st.rerun()
return
st.subheader("Confirm Details")
st.caption("Developer/admin mode — normal users skip this step.")
st.write(
"Review the extracted job details and correct anything that's off. "
"Leave unknown values as “Not visible” — analysis runs once you confirm."
)
for title, keys in FIELD_GROUPS:
# Keyed per group: the key is what the widget layer styles as a panel,
# and inside a loop it has to differ on every iteration.
slug = re.sub(r"[^a-z0-9]+", "_", title.lower()).strip("_")
with st.container(border=True, key=f"ups_panel_fields_{slug}"):
theme.section_label(title)
for key in keys:
_render_field(key, extracted, debug)
st.write("")
if st.button("Confirm Details & Analyze", type="primary", key="confirm_fields_btn"):
_commit(extracted)
st.rerun()
|