Two UI changes: criteria delete button and bidder toggle
Browse filestab_tender.py: add × button per criterion (columns layout, one per row).
Clicking removes that criterion from session_state and reruns — evaluation
will only run against the remaining criteria.
tab_bidders.py: wrap all criteria rows in st.expander("View all N criteria").
Bidder card header (name, overall verdict, pass/total) always visible when
collapsed; all criterion rows appear on expand.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ui/tab_bidders.py +82 -80
- ui/tab_tender.py +23 -19
ui/tab_bidders.py
CHANGED
|
@@ -118,87 +118,89 @@ def render() -> None:
|
|
| 118 |
unsafe_allow_html=True,
|
| 119 |
)
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
'<div style="display:grid;grid-template-columns:2.5fr 1.6fr 1.8fr 2.2fr 1.4fr;'
|
| 124 |
-
'gap:8px;padding:6px 2px;border-top:1px solid rgba(128,128,128,0.15);'
|
| 125 |
-
'border-bottom:1px solid rgba(128,128,128,0.15);margin-bottom:4px;">'
|
| 126 |
-
+ "".join(
|
| 127 |
-
f'<div style="font-size:0.68rem;font-weight:700;'
|
| 128 |
-
f'color:var(--text-color);opacity:0.4;'
|
| 129 |
-
f'text-transform:uppercase;letter-spacing:0.07em;">{h}</div>'
|
| 130 |
-
for h in ["Criterion", "Verdict", "Extracted Value", "Source & Tier", "Category"]
|
| 131 |
-
)
|
| 132 |
-
+ "</div>",
|
| 133 |
-
unsafe_allow_html=True,
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
for v in verdicts:
|
| 137 |
-
crit = crit_map.get(v["criterion_id"])
|
| 138 |
-
title = crit.title if crit else v["criterion_id"]
|
| 139 |
-
mb = mandatory_badge(crit.mandatory if crit else True)
|
| 140 |
-
cat = category_badge(crit.category if crit else "compliance")
|
| 141 |
-
extracted = v.get("extracted_value") or ""
|
| 142 |
-
src = v.get("source") or {}
|
| 143 |
-
|
| 144 |
-
src_html = '<span style="color:var(--text-color);opacity:0.3;">—</span>'
|
| 145 |
-
if src:
|
| 146 |
-
tier = ocr_tier_badge(src.get("source_type", "text_pdf"))
|
| 147 |
-
src_html = (
|
| 148 |
-
f'<span style="font-family:monospace;font-size:0.78rem;'
|
| 149 |
-
f'background:rgba(128,128,128,0.1);padding:2px 5px;border-radius:4px;'
|
| 150 |
-
f'border:1px solid rgba(128,128,128,0.2);'
|
| 151 |
-
f'color:var(--text-color);">{src.get("doc_name","")}</span>'
|
| 152 |
-
f' <span style="font-size:0.75rem;color:var(--text-color);opacity:0.5;">'
|
| 153 |
-
f'p{src.get("page","")}</span>'
|
| 154 |
-
f'<br><div style="margin-top:4px;">{tier}</div>'
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
extracted_cell = (
|
| 158 |
-
f'<span style="font-size:0.84rem;color:var(--text-color);">{extracted}</span>'
|
| 159 |
-
if extracted else
|
| 160 |
-
'<span style="color:var(--text-color);opacity:0.3;">—</span>'
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
st.markdown(
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
f'</div>',
|
| 174 |
unsafe_allow_html=True,
|
| 175 |
)
|
| 176 |
-
confidence_bar(v.get("combined_confidence", 0.0))
|
| 177 |
-
|
| 178 |
-
reason = v.get("reason", "")
|
| 179 |
-
snippet = (v.get("source") or {}).get("snippet", "")
|
| 180 |
-
if reason or snippet:
|
| 181 |
-
with st.expander("View reasoning & evidence", expanded=False):
|
| 182 |
-
if reason:
|
| 183 |
-
st.markdown(
|
| 184 |
-
f'<div style="background:rgba(37,99,235,0.08);'
|
| 185 |
-
f'border-left:3px solid #3B82F6;padding:10px 14px;'
|
| 186 |
-
f'border-radius:0 8px 8px 0;font-size:0.875rem;'
|
| 187 |
-
f'color:var(--text-color);"><strong>Reason:</strong> {reason}</div>',
|
| 188 |
-
unsafe_allow_html=True,
|
| 189 |
-
)
|
| 190 |
-
if snippet:
|
| 191 |
-
st.markdown(
|
| 192 |
-
f'<div style="background:rgba(245,158,11,0.08);'
|
| 193 |
-
f'border-left:3px solid #F59E0B;padding:10px 14px;'
|
| 194 |
-
f'border-radius:0 8px 8px 0;margin-top:8px;font-size:0.84rem;'
|
| 195 |
-
f'color:var(--text-color);font-style:italic;">'
|
| 196 |
-
f'“{snippet}”</div>',
|
| 197 |
-
unsafe_allow_html=True,
|
| 198 |
-
)
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
unsafe_allow_html=True,
|
| 119 |
)
|
| 120 |
|
| 121 |
+
with st.expander(f"View all {len(verdicts)} criteria", expanded=False):
|
| 122 |
+
# Column headers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
st.markdown(
|
| 124 |
+
'<div style="display:grid;grid-template-columns:2.5fr 1.6fr 1.8fr 2.2fr 1.4fr;'
|
| 125 |
+
'gap:8px;padding:6px 2px;border-top:1px solid rgba(128,128,128,0.15);'
|
| 126 |
+
'border-bottom:1px solid rgba(128,128,128,0.15);margin-bottom:4px;">'
|
| 127 |
+
+ "".join(
|
| 128 |
+
f'<div style="font-size:0.68rem;font-weight:700;color:var(--text-color);'
|
| 129 |
+
f'opacity:0.4;text-transform:uppercase;letter-spacing:0.07em;">{h}</div>'
|
| 130 |
+
for h in ["Criterion", "Verdict", "Extracted Value", "Source & Tier", "Category"]
|
| 131 |
+
)
|
| 132 |
+
+ "</div>",
|
|
|
|
| 133 |
unsafe_allow_html=True,
|
| 134 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
+
for v in verdicts:
|
| 137 |
+
crit = crit_map.get(v["criterion_id"])
|
| 138 |
+
title = crit.title if crit else v["criterion_id"]
|
| 139 |
+
mb = mandatory_badge(crit.mandatory if crit else True)
|
| 140 |
+
cat = category_badge(crit.category if crit else "compliance")
|
| 141 |
+
extracted = v.get("extracted_value") or ""
|
| 142 |
+
src = v.get("source") or {}
|
| 143 |
+
|
| 144 |
+
src_html = '<span style="color:var(--text-color);opacity:0.3;">—</span>'
|
| 145 |
+
if src:
|
| 146 |
+
tier = ocr_tier_badge(src.get("source_type", "text_pdf"))
|
| 147 |
+
src_html = (
|
| 148 |
+
f'<span style="font-family:monospace;font-size:0.78rem;'
|
| 149 |
+
f'background:rgba(128,128,128,0.1);padding:2px 5px;border-radius:4px;'
|
| 150 |
+
f'border:1px solid rgba(128,128,128,0.2);color:var(--text-color);">'
|
| 151 |
+
f'{src.get("doc_name","")}</span>'
|
| 152 |
+
f' <span style="font-size:0.75rem;color:var(--text-color);opacity:0.5;">'
|
| 153 |
+
f'p{src.get("page","")}</span>'
|
| 154 |
+
f'<br><div style="margin-top:4px;">{tier}</div>'
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
extracted_cell = (
|
| 158 |
+
f'<span style="font-size:0.84rem;color:var(--text-color);">{extracted}</span>'
|
| 159 |
+
if extracted else
|
| 160 |
+
'<span style="color:var(--text-color);opacity:0.3;">—</span>'
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
st.markdown(
|
| 164 |
+
f'<div style="display:grid;'
|
| 165 |
+
f'grid-template-columns:2.5fr 1.6fr 1.8fr 2.2fr 1.4fr;'
|
| 166 |
+
f'gap:8px;padding:10px 2px;align-items:start;">'
|
| 167 |
+
f'<div>{mb}<div style="font-size:0.85rem;font-weight:600;'
|
| 168 |
+
f'color:var(--text-color);margin-top:5px;">'
|
| 169 |
+
f'{v["criterion_id"]}: {title}</div></div>'
|
| 170 |
+
f'<div style="padding-top:2px;">{verdict_pill(v["verdict"])}</div>'
|
| 171 |
+
f'<div style="padding-top:4px;">{extracted_cell}</div>'
|
| 172 |
+
f'<div style="font-size:0.82rem;">{src_html}</div>'
|
| 173 |
+
f'<div style="padding-top:2px;">{cat}</div>'
|
| 174 |
+
f'</div>',
|
| 175 |
+
unsafe_allow_html=True,
|
| 176 |
+
)
|
| 177 |
+
confidence_bar(v.get("combined_confidence", 0.0))
|
| 178 |
+
|
| 179 |
+
reason = v.get("reason", "")
|
| 180 |
+
snippet = (v.get("source") or {}).get("snippet", "")
|
| 181 |
+
if reason or snippet:
|
| 182 |
+
with st.expander("View reasoning & evidence", expanded=False):
|
| 183 |
+
if reason:
|
| 184 |
+
st.markdown(
|
| 185 |
+
f'<div style="background:rgba(37,99,235,0.08);'
|
| 186 |
+
f'border-left:3px solid #3B82F6;padding:10px 14px;'
|
| 187 |
+
f'border-radius:0 8px 8px 0;font-size:0.875rem;'
|
| 188 |
+
f'color:var(--text-color);">'
|
| 189 |
+
f'<strong>Reason:</strong> {reason}</div>',
|
| 190 |
+
unsafe_allow_html=True,
|
| 191 |
+
)
|
| 192 |
+
if snippet:
|
| 193 |
+
st.markdown(
|
| 194 |
+
f'<div style="background:rgba(245,158,11,0.08);'
|
| 195 |
+
f'border-left:3px solid #F59E0B;padding:10px 14px;'
|
| 196 |
+
f'border-radius:0 8px 8px 0;margin-top:8px;'
|
| 197 |
+
f'font-size:0.84rem;color:var(--text-color);font-style:italic;">'
|
| 198 |
+
f'“{snippet}”</div>',
|
| 199 |
+
unsafe_allow_html=True,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
st.markdown(
|
| 203 |
+
'<hr style="margin:2px 0;border:none;'
|
| 204 |
+
'border-top:1px solid rgba(128,128,128,0.1);">',
|
| 205 |
+
unsafe_allow_html=True,
|
| 206 |
+
)
|
ui/tab_tender.py
CHANGED
|
@@ -78,11 +78,7 @@ def render() -> None:
|
|
| 78 |
if st.session_state.get("fallback_active"):
|
| 79 |
st.warning("⚠ Live API unavailable — showing pre-computed criteria.")
|
| 80 |
|
| 81 |
-
st.
|
| 82 |
-
f'<div style="font-size:0.9rem;font-weight:700;color:#0D1B2A;margin:1rem 0 0.5rem;">'
|
| 83 |
-
f'Extracted {len(criteria_data)} criteria</div>',
|
| 84 |
-
unsafe_allow_html=True,
|
| 85 |
-
)
|
| 86 |
|
| 87 |
for c in criteria_data:
|
| 88 |
icon = _CAT_ICONS.get(c["category"], "⚪")
|
|
@@ -95,17 +91,25 @@ def render() -> None:
|
|
| 95 |
rule_str += f' {rule["unit"]}'
|
| 96 |
rule_str += "`"
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
st.
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
st.
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
if st.session_state.get("fallback_active"):
|
| 79 |
st.warning("⚠ Live API unavailable — showing pre-computed criteria.")
|
| 80 |
|
| 81 |
+
st.caption(f"{len(criteria_data)} criteria — click × to remove any you don't want evaluated.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
for c in criteria_data:
|
| 84 |
icon = _CAT_ICONS.get(c["category"], "⚪")
|
|
|
|
| 91 |
rule_str += f' {rule["unit"]}'
|
| 92 |
rule_str += "`"
|
| 93 |
|
| 94 |
+
btn_col, exp_col = st.columns([0.04, 0.96])
|
| 95 |
+
with btn_col:
|
| 96 |
+
if st.button("×", key=f"rm_{c['id']}", help=f"Remove {c['id']}"):
|
| 97 |
+
st.session_state["criteria"] = [
|
| 98 |
+
x for x in criteria_data if x["id"] != c["id"]
|
| 99 |
+
]
|
| 100 |
+
st.rerun()
|
| 101 |
+
with exp_col:
|
| 102 |
+
with st.expander(
|
| 103 |
+
f'{icon} **{c["id"]}** — {c["title"]} · {mand_lbl}',
|
| 104 |
+
expanded=False,
|
| 105 |
+
):
|
| 106 |
+
col1, col2 = st.columns([2, 1])
|
| 107 |
+
with col1:
|
| 108 |
+
st.markdown(f"**Description:** {c['description']}")
|
| 109 |
+
st.markdown(f"**Rule:** {rule_str}")
|
| 110 |
+
if c.get("query_hints"):
|
| 111 |
+
hints = " · ".join(f"`{h}`" for h in c["query_hints"])
|
| 112 |
+
st.markdown(f"**Query hints:** {hints}")
|
| 113 |
+
with col2:
|
| 114 |
+
st.markdown(f"**Category:** {c['category'].capitalize()}")
|
| 115 |
+
st.markdown(f"**Source:** Page {c['source_page']}, Clause `{c['source_clause']}`")
|