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()