File size: 10,247 Bytes
661eb14 b2ad034 b14fc84 b2ad034 1049372 aff4140 b2ad034 1049372 aff4140 b2ad034 661eb14 1049372 aff4140 1049372 b2ad034 1049372 01f6914 1049372 b2ad034 aff4140 b2ad034 1049372 b2ad034 aff4140 1049372 b2ad034 1049372 b2ad034 aff4140 1049372 b2ad034 1049372 b2ad034 1049372 aff4140 01f6914 b2ad034 1049372 b2ad034 1049372 aff4140 1049372 aff4140 1049372 aff4140 b14fc84 aff4140 1049372 8a64f5c 1049372 8a64f5c 1049372 8a64f5c ad20db7 8a64f5c | 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 | import streamlit as st
from core import bidder_processor, evaluator
from core.config import BIDDER_NAMES, DATA_DIR
from core.fallback import load_criteria
from core.schemas import Criterion
from ui.components import category_badge, confidence_bar, mandatory_badge, ocr_tier_badge, verdict_pill
_BIDDER_META = {
"bidder_a": ("Apex Constructions Pvt. Ltd.", "Clearly Eligible", "#22C55E"),
"bidder_b": ("BuildRight Enterprises", "Ineligible — Low Turnover", "#EF4444"),
"bidder_c": ("Shree Constructions & Services", "Needs Review — Scanned Cert", "#F59E0B"),
}
def _get_criteria() -> list[Criterion]:
data = st.session_state.get("criteria")
return [Criterion(**c) for c in data] if data else load_criteria()
def _overall(verdicts: list[dict], crit_map: dict) -> str:
mand = [v for v in verdicts if crit_map.get(v["criterion_id"]) and
crit_map[v["criterion_id"]].mandatory]
src = mand or verdicts
if any(v["verdict"] == "not_eligible" for v in src): return "not_eligible"
if any(v["verdict"] == "needs_review" for v in src): return "needs_review"
return "eligible"
def render() -> None:
st.markdown(
'<h2 style="font-weight:800;font-size:1.5rem;color:var(--text-color);">Bidder Evaluation</h2>'
'<p style="color:var(--text-color);opacity:0.6;font-size:0.875rem;margin-bottom:1rem;">'
'Evaluate each bidder against all extracted criteria.</p>',
unsafe_allow_html=True,
)
selected = st.multiselect(
"Select bidders",
options=list(BIDDER_NAMES.keys()),
default=list(BIDDER_NAMES.keys()),
format_func=lambda x: _BIDDER_META.get(x, (x, "", ""))[0],
)
criteria_loaded = bool(st.session_state.get("criteria"))
if not criteria_loaded:
st.info(
"**Criteria not loaded yet.** Go to **Tender Analysis** and click "
"**Extract Criteria**, or use **Load Pre-computed Demo** on the Overview tab."
)
if st.button("▶ Run Evaluation", type="primary", disabled=not criteria_loaded):
criteria = _get_criteria()
prog = st.progress(0, text="Starting…")
total = len(selected) * len(criteria)
done, vd = 0, {}
for bid in selected:
files = sorted(f for f in (DATA_DIR / "bidders" / bid).iterdir()
if f.suffix.lower() in {".pdf", ".png", ".jpg"})
with st.spinner(f"Indexing {_BIDDER_META.get(bid,(bid,'',''))[0]}…"):
bidder_processor.process_bidder(bid, files)
vlist = []
for c in criteria:
v = evaluator.evaluate(bid, c)
vlist.append(v.model_dump())
done += 1
prog.progress(done / total,
text=f"{c.id} · {_BIDDER_META.get(bid,(bid,'',''))[0]}")
vd[bid] = vlist
st.session_state["verdicts"] = vd
prog.empty()
st.success("Evaluation complete.")
st.rerun()
vdata = st.session_state.get("verdicts", {})
criteria = _get_criteria()
crit_map = {c.id: c for c in criteria}
if not vdata:
st.info("No results yet — click **Run Evaluation** above, or load the demo from Overview.")
return
if st.session_state.get("fallback_active"):
st.warning("⚠ Live API unavailable — showing pre-computed results.")
for bid in (selected or list(vdata.keys())):
if bid not in vdata:
continue
verdicts = vdata[bid]
name, tagline, accent = _BIDDER_META.get(bid, (bid, "", "#3B82F6"))
ov = _overall(verdicts, crit_map)
passed = sum(1 for v in verdicts if v["verdict"] == "eligible" and
crit_map.get(v["criterion_id"]) and crit_map[v["criterion_id"]].mandatory)
total_m = sum(1 for v in verdicts if crit_map.get(v["criterion_id"]) and
crit_map[v["criterion_id"]].mandatory)
st.markdown("<div style='height:10px'></div>", unsafe_allow_html=True)
with st.container(border=True):
# Header
st.markdown(
f'<div style="display:flex;justify-content:space-between;'
f'align-items:center;flex-wrap:wrap;gap:8px;padding:4px 0 12px;">'
f'<div style="display:flex;align-items:center;gap:10px;">'
f'<div style="width:40px;height:40px;border-radius:10px;'
f'background:{accent}22;border:1px solid {accent}44;display:flex;'
f'align-items:center;justify-content:center;font-size:1.1rem;">🏢</div>'
f'<div>'
f'<div style="font-weight:700;font-size:1rem;color:var(--text-color);">{name}</div>'
f'<div style="font-size:0.78rem;color:var(--text-color);opacity:0.5;margin-top:2px;">'
f'{tagline}</div></div></div>'
f'<div style="display:flex;align-items:center;gap:10px;">'
f'{verdict_pill(ov)}'
f'<span style="font-size:0.78rem;background:rgba(128,128,128,0.1);'
f'color:var(--text-color);opacity:0.7;padding:3px 10px;border-radius:20px;'
f'border:1px solid rgba(128,128,128,0.2);">'
f'{passed}/{total_m} mandatory passed</span>'
f'</div></div>',
unsafe_allow_html=True,
)
with st.expander(f"View all {len(verdicts)} criteria", expanded=False):
# Column headers
st.markdown(
'<div style="display:grid;grid-template-columns:2.5fr 1.6fr 1.8fr 2.2fr 1.4fr;'
'gap:8px;padding:6px 2px;border-top:1px solid rgba(128,128,128,0.15);'
'border-bottom:1px solid rgba(128,128,128,0.15);margin-bottom:4px;">'
+ "".join(
f'<div style="font-size:0.68rem;font-weight:700;color:var(--text-color);'
f'opacity:0.4;text-transform:uppercase;letter-spacing:0.07em;">{h}</div>'
for h in ["Criterion", "Verdict", "Extracted Value", "Source & Tier", "Category"]
)
+ "</div>",
unsafe_allow_html=True,
)
for v in verdicts:
crit = crit_map.get(v["criterion_id"])
title = crit.title if crit else v["criterion_id"]
mb = mandatory_badge(crit.mandatory if crit else True)
cat = category_badge(crit.category if crit else "compliance")
extracted = v.get("extracted_value") or ""
src = v.get("source") or {}
src_html = '<span style="color:var(--text-color);opacity:0.3;">—</span>'
if src:
tier = ocr_tier_badge(src.get("source_type", "text_pdf"))
src_html = (
f'<span style="font-family:monospace;font-size:0.78rem;'
f'background:rgba(128,128,128,0.1);padding:2px 5px;border-radius:4px;'
f'border:1px solid rgba(128,128,128,0.2);color:var(--text-color);">'
f'{src.get("doc_name","")}</span>'
f' <span style="font-size:0.75rem;color:var(--text-color);opacity:0.5;">'
f'p{src.get("page","")}</span>'
f'<br><div style="margin-top:4px;">{tier}</div>'
)
extracted_cell = (
f'<span style="font-size:0.84rem;color:var(--text-color);">{extracted}</span>'
if extracted else
'<span style="color:var(--text-color);opacity:0.3;">—</span>'
)
st.markdown(
f'<div style="display:grid;'
f'grid-template-columns:2.5fr 1.6fr 1.8fr 2.2fr 1.4fr;'
f'gap:8px;padding:10px 2px;align-items:start;">'
f'<div>{mb}<div style="font-size:0.85rem;font-weight:600;'
f'color:var(--text-color);margin-top:5px;">'
f'{v["criterion_id"]}: {title}</div></div>'
f'<div style="padding-top:2px;">{verdict_pill(v["verdict"])}</div>'
f'<div style="padding-top:4px;">{extracted_cell}</div>'
f'<div style="font-size:0.82rem;">{src_html}</div>'
f'<div style="padding-top:2px;">{cat}</div>'
f'</div>',
unsafe_allow_html=True,
)
confidence_bar(v.get("combined_confidence", 0.0))
reason = v.get("reason", "")
snippet = (v.get("source") or {}).get("snippet", "")
if reason:
st.markdown(
f'<div style="background:rgba(37,99,235,0.08);'
f'border-left:3px solid #3B82F6;padding:8px 12px;'
f'border-radius:0 6px 6px 0;font-size:0.82rem;'
f'color:var(--text-color);margin-top:2px;">'
f'<strong>Reason:</strong> {reason}</div>',
unsafe_allow_html=True,
)
if snippet:
st.markdown(
f'<div style="background:rgba(245,158,11,0.08);'
f'border-left:3px solid #F59E0B;padding:8px 12px;'
f'border-radius:0 6px 6px 0;margin-top:4px;'
f'font-size:0.81rem;color:var(--text-color);font-style:italic;">'
f'“{snippet}”</div>',
unsafe_allow_html=True,
)
st.markdown(
'<hr style="margin:2px 0;border:none;'
'border-top:1px solid rgba(128,128,128,0.1);">',
unsafe_allow_html=True,
)
|