Final sweep: beautiful UI, pitch deck, HF README, ARCHITECTURE.md
Browse filesUI overhaul:
- .streamlit/config.toml: Streamlit theme (navy primary, clean typography)
- ui/styles.py: comprehensive CSS — dark navy sidebar, metric cards with hover
lift, pill-style tab bar, gradient primary buttons, HTML verdict/category/OCR
tier badges, hero banner, custom confidence bars with green/amber/red coloring
- ui/components.py: HTML badge system (verdict_pill, category_badge,
ocr_tier_badge, mandatory_badge, confidence_bar with color thresholds)
- app.py: inject CSS, redesigned sidebar with glowing status dots and branding
- ui/tab_overview.py: hero banner, HTML KPI strip, styled pipeline stage cards
- ui/tab_bidders.py: HTML column headers, bidder header with pass/total count,
styled reason/snippet callout boxes
- ui/tab_review.py: HTML verdict display, friendly company names, styled cards
- ui/tab_interpretability.py: fix use_column_width for Streamlit 1.39.0
Submission:
- README.md: HuggingFace Spaces YAML frontmatter (sdk, emoji, colorFrom/To)
- ARCHITECTURE.md: full module responsibilities, OCR pipeline diagram,
confidence formula, threshold rules, audit schema, data flow
- scripts/generate_deck.py: 8-slide reportlab pitch deck
- deck/TenderIQ_Pitch.pdf: generated pitch deck
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .streamlit/config.toml +9 -0
- ARCHITECTURE.md +180 -0
- README.md +13 -0
- app.py +52 -24
- deck/TenderIQ_Pitch.pdf +3 -0
- scripts/generate_deck.py +435 -0
- ui/components.py +53 -21
- ui/styles.py +257 -0
- ui/tab_bidders.py +102 -44
- ui/tab_interpretability.py +2 -2
- ui/tab_overview.py +80 -107
- ui/tab_review.py +30 -5
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[theme]
|
| 2 |
+
primaryColor = "#2563EB"
|
| 3 |
+
backgroundColor = "#F8FAFC"
|
| 4 |
+
secondaryBackgroundColor = "#FFFFFF"
|
| 5 |
+
textColor = "#0D1B2A"
|
| 6 |
+
font = "sans serif"
|
| 7 |
+
|
| 8 |
+
[browser]
|
| 9 |
+
gatherUsageStats = false
|
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TenderIQ — Architecture
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
TenderIQ is a single-process Streamlit application that automates eligibility evaluation
|
| 6 |
+
of bidders against government tender criteria. All state is local: SQLite for the audit
|
| 7 |
+
log, ChromaDB (file-backed) for vector indices, and the filesystem for PDFs and cached
|
| 8 |
+
OCR results. The only external dependency is the DeepSeek API.
|
| 9 |
+
|
| 10 |
+
```
|
| 11 |
+
┌──────────────────────────────────────────────────────────────────────┐
|
| 12 |
+
│ Streamlit App (app.py) │
|
| 13 |
+
│ │
|
| 14 |
+
│ Tab 1: Overview Tab 2: Tender Analysis Tab 3: Bidder Evaluation │
|
| 15 |
+
│ Tab 4: Human Review Tab 5: Audit Log Tab 6: Interpretability │
|
| 16 |
+
└────────────────────────────┬─────────────────────────────────────────┘
|
| 17 |
+
│ calls
|
| 18 |
+
┌────────────────────┼────────────────────┐
|
| 19 |
+
▼ ▼ ▼
|
| 20 |
+
┌────────────┐ ┌──────────────────┐ ┌──────────────┐
|
| 21 |
+
│ criteria_ │ │ bidder_processor │ │ evaluator │
|
| 22 |
+
│ extractor │ │ ocr_pipeline │ │ fallback │
|
| 23 |
+
└────┬───────┘ └────┬─────────────┘ └──────┬───────┘
|
| 24 |
+
│ │ │
|
| 25 |
+
▼ ▼ ▼
|
| 26 |
+
┌──────────┐ ┌─────────────┐ ┌──────────────┐
|
| 27 |
+
│ DeepSeek │ │ ChromaDB │ │ DeepSeek LLM │
|
| 28 |
+
│ LLM │ │ (vectors) │◄────────│ (evaluate) │
|
| 29 |
+
└──────────┘ └─────────────┘ └──────────────┘
|
| 30 |
+
│ ▲
|
| 31 |
+
│ │ index chunks
|
| 32 |
+
│ ┌──────┴──────┐
|
| 33 |
+
│ │ 3-tier OCR │
|
| 34 |
+
│ │ PyMuPDF │
|
| 35 |
+
│ │ Tesseract │
|
| 36 |
+
│ │ Vision LLM │
|
| 37 |
+
│ └─────────────┘
|
| 38 |
+
│
|
| 39 |
+
▼
|
| 40 |
+
┌──────────┐
|
| 41 |
+
│ audit │ (SQLite, append-only)
|
| 42 |
+
└──────────┘
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## Module Responsibilities
|
| 48 |
+
|
| 49 |
+
| Module | Responsibility |
|
| 50 |
+
|---|---|
|
| 51 |
+
| `core/config.py` | Environment loading, constants, paths |
|
| 52 |
+
| `core/schemas.py` | Pydantic models: Criterion, Evidence, Verdict, AuditEntry |
|
| 53 |
+
| `core/prompts.py` | LLM prompt strings (criteria extraction, evaluation, OCR) |
|
| 54 |
+
| `core/llm_client.py` | DeepSeek API wrapper — `chat_json`, `chat_vision`, retry logic, `LLMUnavailable` |
|
| 55 |
+
| `core/pdf_utils.py` | PyMuPDF: text extraction, text-PDF detection, page rendering |
|
| 56 |
+
| `core/ocr_pipeline.py` | Three-tier OCR orchestrator, MD5-based result cache |
|
| 57 |
+
| `core/chunker.py` | Text chunking for tender and bidder documents |
|
| 58 |
+
| `core/vectorstore.py` | ChromaDB helpers: get/create collection, upsert, query |
|
| 59 |
+
| `core/criteria_extractor.py` | Stage 1: tender PDF → `list[Criterion]` |
|
| 60 |
+
| `core/bidder_processor.py` | Stage 2: bidder docs → chunks + evidence retrieval |
|
| 61 |
+
| `core/evaluator.py` | Stage 3: per-criterion verdict with combined confidence |
|
| 62 |
+
| `core/audit.py` | SQLite audit log: write and query |
|
| 63 |
+
| `core/fallback.py` | Load pre-computed JSON when LLM unavailable |
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## Three-Tier OCR Pipeline
|
| 68 |
+
|
| 69 |
+
The robustness centrepiece. Handles typed PDFs, scanned PDFs, and photographs of documents.
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
Input file
|
| 73 |
+
│
|
| 74 |
+
├─ Is image (PNG/JPG)? ──────────────────────────────────┐
|
| 75 |
+
│ │
|
| 76 |
+
└─ Is PDF? ▼
|
| 77 |
+
│ Tier 2: Tesseract
|
| 78 |
+
├─ is_text_pdf() == True │
|
| 79 |
+
│ │ mean_conf ≥ 0.65?
|
| 80 |
+
│ ▼ │
|
| 81 |
+
│ Tier 1: PyMuPDF Yes ──┘ No
|
| 82 |
+
│ confidence = 1.0 │
|
| 83 |
+
│ source_type = "text_pdf" ▼
|
| 84 |
+
│ Tier 3: DeepSeek Vision LLM
|
| 85 |
+
└─ is_text_pdf() == False confidence = 0.95
|
| 86 |
+
│ source_type = "vision_llm"
|
| 87 |
+
▼
|
| 88 |
+
Render pages → Tier 2
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Each `ExtractedPage` carries: `page`, `text`, `source_type`, `confidence`, `raw_tier_results`.
|
| 92 |
+
Results cached under `.ocr_cache/<md5>.json`.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## Confidence & Threshold Logic
|
| 97 |
+
|
| 98 |
+
Combined confidence weights LLM certainty against OCR quality:
|
| 99 |
+
|
| 100 |
+
| OCR Tier | Formula |
|
| 101 |
+
|---|---|
|
| 102 |
+
| `text_pdf` | `combined = llm_confidence` |
|
| 103 |
+
| `vision_llm` | `combined = 0.7 × llm_confidence + 0.3 × 0.95` |
|
| 104 |
+
| `tesseract` | `combined = 0.6 × llm_confidence + 0.4 × tesseract_conf` |
|
| 105 |
+
|
| 106 |
+
Safety threshold rules (applied in order):
|
| 107 |
+
|
| 108 |
+
1. LLM returns `needs_review` → keep (regardless of confidence)
|
| 109 |
+
2. `combined ≥ 0.80` → keep LLM verdict
|
| 110 |
+
3. `0.55 ≤ combined < 0.80` AND verdict is `not_eligible` → downgrade to `needs_review`
|
| 111 |
+
4. `combined < 0.55` → force `needs_review`
|
| 112 |
+
|
| 113 |
+
**The core safety guarantee:** a bidder is never silently disqualified at medium or low confidence.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## Fallback Strategy
|
| 118 |
+
|
| 119 |
+
Every live LLM call is wrapped in `try/except LLMUnavailable`. On failure:
|
| 120 |
+
|
| 121 |
+
1. `audit.log("precomputed_fallback_used", ...)` is written
|
| 122 |
+
2. `st.session_state["fallback_active"] = True` triggers the amber sidebar dot
|
| 123 |
+
3. Data is loaded from `data/precomputed/*.json` (committed to the repo)
|
| 124 |
+
|
| 125 |
+
This means the demo works even if the API is down, rate-limited, or the key is missing.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## Audit Log Schema
|
| 130 |
+
|
| 131 |
+
```sql
|
| 132 |
+
CREATE TABLE audit_log (
|
| 133 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 134 |
+
ts TEXT NOT NULL, -- UTC ISO timestamp
|
| 135 |
+
action TEXT NOT NULL, -- see action vocabulary below
|
| 136 |
+
actor TEXT NOT NULL, -- "system" or "officer"
|
| 137 |
+
model_version TEXT, -- e.g. "deepseek-chat@2026-05-07"
|
| 138 |
+
bidder_id TEXT,
|
| 139 |
+
criterion_id TEXT,
|
| 140 |
+
payload_json TEXT -- action-specific JSON payload
|
| 141 |
+
);
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
Action vocabulary: `criteria_extracted`, `bidder_processed`, `criterion_evaluated`,
|
| 145 |
+
`human_review_action`, `precomputed_fallback_used`, `vision_ocr_invoked`.
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## Data Flow (full pipeline)
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
1. Officer uploads tender PDF
|
| 153 |
+
│
|
| 154 |
+
▼
|
| 155 |
+
2. criteria_extractor.extract_criteria(pdf)
|
| 156 |
+
→ LLM reads tender text
|
| 157 |
+
→ Returns List[Criterion] with structured rules
|
| 158 |
+
→ Stored in st.session_state["criteria"]
|
| 159 |
+
│
|
| 160 |
+
▼
|
| 161 |
+
3. bidder_processor.process_bidder(bidder_id, files)
|
| 162 |
+
→ For each file: ocr_pipeline.extract_document()
|
| 163 |
+
→ chunker.chunk_bidder()
|
| 164 |
+
→ vectorstore.add_chunks("bidder_chunks", ...)
|
| 165 |
+
│
|
| 166 |
+
▼
|
| 167 |
+
4. evaluator.evaluate(bidder_id, criterion)
|
| 168 |
+
→ bidder_processor.gather_evidence() — semantic search in ChromaDB
|
| 169 |
+
→ LLM evaluates evidence against criterion rule
|
| 170 |
+
→ combined_confidence computed
|
| 171 |
+
→ threshold safety rules applied
|
| 172 |
+
→ Verdict returned + audit logged
|
| 173 |
+
│
|
| 174 |
+
▼
|
| 175 |
+
5. Verdicts stored in st.session_state["verdicts"]
|
| 176 |
+
→ Tab 3: evaluation matrix
|
| 177 |
+
→ Tab 4: needs_review items surfaced for human action
|
| 178 |
+
→ Tab 6: plain-English explanation + Q&A
|
| 179 |
+
→ Tab 5: full audit trail
|
| 180 |
+
```
|
|
@@ -1,3 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# TenderIQ — Explainable AI for Tender Evaluation
|
| 2 |
|
| 3 |
AI-powered eligibility evaluation of bidders against government tender criteria, built for the **CRPF Hackathon, Theme 3**.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: TenderIQ
|
| 3 |
+
emoji: ⚖️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.39.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
license: agpl-3.0
|
| 11 |
+
short_description: Explainable AI for Government Tender Evaluation (CRPF Hackathon)
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
# TenderIQ — Explainable AI for Tender Evaluation
|
| 15 |
|
| 16 |
AI-powered eligibility evaluation of bidders against government tender criteria, built for the **CRPF Hackathon, Theme 3**.
|
|
@@ -2,6 +2,7 @@ import shutil
|
|
| 2 |
|
| 3 |
import streamlit as st
|
| 4 |
|
|
|
|
| 5 |
from ui.tab_overview import render as render_overview
|
| 6 |
from ui.tab_tender import render as render_tender
|
| 7 |
from ui.tab_bidders import render as render_bidders
|
|
@@ -15,6 +16,8 @@ st.set_page_config(
|
|
| 15 |
layout="wide",
|
| 16 |
)
|
| 17 |
|
|
|
|
|
|
|
| 18 |
|
| 19 |
def _probe_llm() -> str:
|
| 20 |
"""Probe once per session; returns 'green', 'amber', or 'red'."""
|
|
@@ -36,7 +39,6 @@ def _probe_llm() -> str:
|
|
| 36 |
|
| 37 |
|
| 38 |
def _reset_demo() -> None:
|
| 39 |
-
"""Clear session, audit DB, ChromaDB, and OCR cache for a clean demo run."""
|
| 40 |
from core import audit
|
| 41 |
from core.config import CHROMA_DIR, OCR_CACHE_DIR
|
| 42 |
audit.clear()
|
|
@@ -49,64 +51,90 @@ def _reset_demo() -> None:
|
|
| 49 |
|
| 50 |
# ── Sidebar ──────────────────────────────────────────────────────────────────
|
| 51 |
with st.sidebar:
|
| 52 |
-
st.markdown(
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
st.divider()
|
| 55 |
|
| 56 |
status = _probe_llm()
|
| 57 |
if status == "green":
|
| 58 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
elif status == "amber":
|
| 60 |
-
st.markdown(
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
else:
|
| 63 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
st.caption("Using pre-computed fallback data.")
|
| 65 |
|
| 66 |
st.divider()
|
| 67 |
|
| 68 |
-
if st.button("Reset Session", use_container_width=True):
|
| 69 |
for key in list(st.session_state.keys()):
|
| 70 |
del st.session_state[key]
|
| 71 |
st.rerun()
|
| 72 |
|
| 73 |
-
if st.button("🗑
|
| 74 |
st.session_state["confirm_demo_reset"] = True
|
| 75 |
|
| 76 |
if st.session_state.get("confirm_demo_reset"):
|
| 77 |
-
st.warning("Clears audit log, vector index, OCR cache, and session.
|
| 78 |
-
|
| 79 |
-
if
|
| 80 |
_reset_demo()
|
| 81 |
st.rerun()
|
| 82 |
-
if
|
| 83 |
st.session_state.pop("confirm_demo_reset", None)
|
| 84 |
st.rerun()
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
# ── Tabs ─────────────────────────────────────────────────────────────────────
|
| 87 |
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
|
| 88 |
-
"Overview",
|
| 89 |
-
"Tender Analysis",
|
| 90 |
-
"Bidder Evaluation",
|
| 91 |
-
"Human Review",
|
| 92 |
-
"Audit Log",
|
| 93 |
-
"Interpretability",
|
| 94 |
])
|
| 95 |
|
| 96 |
with tab1:
|
| 97 |
render_overview()
|
| 98 |
-
|
| 99 |
with tab2:
|
| 100 |
render_tender()
|
| 101 |
-
|
| 102 |
with tab3:
|
| 103 |
render_bidders()
|
| 104 |
-
|
| 105 |
with tab4:
|
| 106 |
render_review()
|
| 107 |
-
|
| 108 |
with tab5:
|
| 109 |
render_audit()
|
| 110 |
-
|
| 111 |
with tab6:
|
| 112 |
render_interpretability()
|
|
|
|
| 2 |
|
| 3 |
import streamlit as st
|
| 4 |
|
| 5 |
+
from ui.styles import CSS
|
| 6 |
from ui.tab_overview import render as render_overview
|
| 7 |
from ui.tab_tender import render as render_tender
|
| 8 |
from ui.tab_bidders import render as render_bidders
|
|
|
|
| 16 |
layout="wide",
|
| 17 |
)
|
| 18 |
|
| 19 |
+
st.markdown(CSS, unsafe_allow_html=True)
|
| 20 |
+
|
| 21 |
|
| 22 |
def _probe_llm() -> str:
|
| 23 |
"""Probe once per session; returns 'green', 'amber', or 'red'."""
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
def _reset_demo() -> None:
|
|
|
|
| 42 |
from core import audit
|
| 43 |
from core.config import CHROMA_DIR, OCR_CACHE_DIR
|
| 44 |
audit.clear()
|
|
|
|
| 51 |
|
| 52 |
# ── Sidebar ──────────────────────────────────────────────────────────────────
|
| 53 |
with st.sidebar:
|
| 54 |
+
st.markdown(
|
| 55 |
+
"""<div style="padding:12px 4px 8px;text-align:center;">
|
| 56 |
+
<div style="font-size:2.4rem;line-height:1;">⚖️</div>
|
| 57 |
+
<div style="font-size:1.3rem;font-weight:800;color:#F1F5F9;
|
| 58 |
+
letter-spacing:-0.01em;margin-top:6px;">TenderIQ</div>
|
| 59 |
+
<div style="font-size:0.72rem;color:#94A3B8;margin-top:3px;
|
| 60 |
+
text-transform:uppercase;letter-spacing:0.08em;">
|
| 61 |
+
AI Tender Evaluation</div>
|
| 62 |
+
</div>""",
|
| 63 |
+
unsafe_allow_html=True,
|
| 64 |
+
)
|
| 65 |
st.divider()
|
| 66 |
|
| 67 |
status = _probe_llm()
|
| 68 |
if status == "green":
|
| 69 |
+
st.markdown(
|
| 70 |
+
'<div style="display:flex;align-items:center;gap:8px;padding:6px 0;">'
|
| 71 |
+
'<div style="width:10px;height:10px;border-radius:50%;background:#22C55E;'
|
| 72 |
+
'box-shadow:0 0 6px #22C55E;flex-shrink:0;"></div>'
|
| 73 |
+
'<span style="font-size:0.85rem;font-weight:600;">DeepSeek Connected</span></div>',
|
| 74 |
+
unsafe_allow_html=True,
|
| 75 |
+
)
|
| 76 |
elif status == "amber":
|
| 77 |
+
st.markdown(
|
| 78 |
+
'<div style="display:flex;align-items:center;gap:8px;padding:6px 0;">'
|
| 79 |
+
'<div style="width:10px;height:10px;border-radius:50%;background:#F59E0B;'
|
| 80 |
+
'box-shadow:0 0 6px #F59E0B;flex-shrink:0;"></div>'
|
| 81 |
+
'<span style="font-size:0.85rem;font-weight:600;">Pre-computed Mode</span></div>',
|
| 82 |
+
unsafe_allow_html=True,
|
| 83 |
+
)
|
| 84 |
+
st.warning("⚠ Showing pre-computed results.")
|
| 85 |
else:
|
| 86 |
+
st.markdown(
|
| 87 |
+
'<div style="display:flex;align-items:center;gap:8px;padding:6px 0;">'
|
| 88 |
+
'<div style="width:10px;height:10px;border-radius:50%;background:#EF4444;'
|
| 89 |
+
'box-shadow:0 0 6px #EF4444;flex-shrink:0;"></div>'
|
| 90 |
+
'<span style="font-size:0.85rem;font-weight:600;">No API Key</span></div>',
|
| 91 |
+
unsafe_allow_html=True,
|
| 92 |
+
)
|
| 93 |
st.caption("Using pre-computed fallback data.")
|
| 94 |
|
| 95 |
st.divider()
|
| 96 |
|
| 97 |
+
if st.button("↺ Reset Session", use_container_width=True):
|
| 98 |
for key in list(st.session_state.keys()):
|
| 99 |
del st.session_state[key]
|
| 100 |
st.rerun()
|
| 101 |
|
| 102 |
+
if st.button("🗑 Reset for Demo", use_container_width=True, type="secondary"):
|
| 103 |
st.session_state["confirm_demo_reset"] = True
|
| 104 |
|
| 105 |
if st.session_state.get("confirm_demo_reset"):
|
| 106 |
+
st.warning("Clears audit log, vector index, OCR cache, and session.")
|
| 107 |
+
c1, c2 = st.columns(2)
|
| 108 |
+
if c1.button("Yes, reset", type="primary", use_container_width=True):
|
| 109 |
_reset_demo()
|
| 110 |
st.rerun()
|
| 111 |
+
if c2.button("Cancel", use_container_width=True):
|
| 112 |
st.session_state.pop("confirm_demo_reset", None)
|
| 113 |
st.rerun()
|
| 114 |
|
| 115 |
+
st.divider()
|
| 116 |
+
st.caption("CRPF Hackathon · Theme 3\nExplainable AI for Government Procurement")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
# ── Tabs ─────────────────────────────────────────────────────────────────────
|
| 120 |
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
|
| 121 |
+
"🏠 Overview",
|
| 122 |
+
"📄 Tender Analysis",
|
| 123 |
+
"⚖️ Bidder Evaluation",
|
| 124 |
+
"👤 Human Review",
|
| 125 |
+
"📋 Audit Log",
|
| 126 |
+
"🔍 Interpretability",
|
| 127 |
])
|
| 128 |
|
| 129 |
with tab1:
|
| 130 |
render_overview()
|
|
|
|
| 131 |
with tab2:
|
| 132 |
render_tender()
|
|
|
|
| 133 |
with tab3:
|
| 134 |
render_bidders()
|
|
|
|
| 135 |
with tab4:
|
| 136 |
render_review()
|
|
|
|
| 137 |
with tab5:
|
| 138 |
render_audit()
|
|
|
|
| 139 |
with tab6:
|
| 140 |
render_interpretability()
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b0b09f58d390bbd8e074811a9ddef8bd64489db204f0278041f97658e3a29c80
|
| 3 |
+
size 18040
|
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate TenderIQ_Pitch.pdf — 8-slide pitch deck using reportlab."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 7 |
+
sys.path.insert(0, str(BASE_DIR))
|
| 8 |
+
|
| 9 |
+
from reportlab.lib import colors
|
| 10 |
+
from reportlab.lib.pagesizes import A4
|
| 11 |
+
from reportlab.lib.units import cm, mm
|
| 12 |
+
from reportlab.pdfgen.canvas import Canvas
|
| 13 |
+
|
| 14 |
+
W, H = A4
|
| 15 |
+
NAVY = colors.HexColor("#0D1B2A")
|
| 16 |
+
BLUE = colors.HexColor("#2563EB")
|
| 17 |
+
LBLUE = colors.HexColor("#DBEAFE")
|
| 18 |
+
GOLD = colors.HexColor("#F0A500")
|
| 19 |
+
WHITE = colors.white
|
| 20 |
+
GREY = colors.HexColor("#64748B")
|
| 21 |
+
LGREY = colors.HexColor("#F1F5F9")
|
| 22 |
+
GREEN = colors.HexColor("#059669")
|
| 23 |
+
RED = colors.HexColor("#DC2626")
|
| 24 |
+
AMBER = colors.HexColor("#D97706")
|
| 25 |
+
BORD = colors.HexColor("#E2E8F0")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _header_bar(c: Canvas, title: str, subtitle: str = "") -> None:
|
| 29 |
+
c.setFillColor(NAVY)
|
| 30 |
+
c.rect(0, H - 2.8*cm, W, 2.8*cm, fill=1, stroke=0)
|
| 31 |
+
c.setFillColor(GOLD)
|
| 32 |
+
c.rect(0, H - 2.85*cm, W, 0.18*cm, fill=1, stroke=0)
|
| 33 |
+
c.setFillColor(WHITE)
|
| 34 |
+
c.setFont("Helvetica-Bold", 18)
|
| 35 |
+
c.drawString(1.8*cm, H - 1.7*cm, title)
|
| 36 |
+
if subtitle:
|
| 37 |
+
c.setFont("Helvetica", 10)
|
| 38 |
+
c.setFillColor(colors.HexColor("#94A3B8"))
|
| 39 |
+
c.drawString(1.8*cm, H - 2.3*cm, subtitle)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _footer(c: Canvas, page: int, total: int = 8) -> None:
|
| 43 |
+
c.setFillColor(LGREY)
|
| 44 |
+
c.rect(0, 0, W, 1.0*cm, fill=1, stroke=0)
|
| 45 |
+
c.setFillColor(GREY)
|
| 46 |
+
c.setFont("Helvetica", 8)
|
| 47 |
+
c.drawString(1.8*cm, 0.35*cm, "TenderIQ · CRPF Hackathon Theme 3 · Explainable AI for Government Procurement")
|
| 48 |
+
c.drawRightString(W - 1.8*cm, 0.35*cm, f"{page} / {total}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _bullet(c: Canvas, x: float, y: float, text: str,
|
| 52 |
+
size: int = 10, indent: float = 0.5*cm) -> float:
|
| 53 |
+
c.setFillColor(BLUE)
|
| 54 |
+
c.circle(x + 0.15*cm, y + 0.3*cm, 0.12*cm, fill=1, stroke=0)
|
| 55 |
+
c.setFillColor(NAVY)
|
| 56 |
+
c.setFont("Helvetica", size)
|
| 57 |
+
lines = _wrap(text, 85 - int(indent / mm))
|
| 58 |
+
for i, line in enumerate(lines):
|
| 59 |
+
c.drawString(x + indent, y - i * (size + 3) * 0.035 * cm * 28.35 / 10, line)
|
| 60 |
+
return y - len(lines) * (size + 4) * 0.035 * cm * 28.35 / 10
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _wrap(text: str, width: int) -> list[str]:
|
| 64 |
+
words = text.split()
|
| 65 |
+
lines, cur = [], ""
|
| 66 |
+
for w in words:
|
| 67 |
+
if len(cur) + len(w) + 1 <= width:
|
| 68 |
+
cur = (cur + " " + w).strip()
|
| 69 |
+
else:
|
| 70 |
+
if cur:
|
| 71 |
+
lines.append(cur)
|
| 72 |
+
cur = w
|
| 73 |
+
if cur:
|
| 74 |
+
lines.append(cur)
|
| 75 |
+
return lines or [""]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _card(c: Canvas, x: float, y: float, w: float, h: float,
|
| 79 |
+
title: str, body: str, accent: colors.Color = BLUE) -> None:
|
| 80 |
+
c.setFillColor(WHITE)
|
| 81 |
+
c.setStrokeColor(BORD)
|
| 82 |
+
c.roundRect(x, y, w, h, 0.3*cm, fill=1, stroke=1)
|
| 83 |
+
c.setFillColor(accent)
|
| 84 |
+
c.roundRect(x, y + h - 0.35*cm, w, 0.35*cm, 0.3*cm, fill=1, stroke=0)
|
| 85 |
+
c.rect(x, y + h - 0.35*cm, w, 0.2*cm, fill=1, stroke=0)
|
| 86 |
+
c.setFillColor(WHITE)
|
| 87 |
+
c.setFont("Helvetica-Bold", 9)
|
| 88 |
+
c.drawString(x + 0.3*cm, y + h - 0.25*cm, title)
|
| 89 |
+
c.setFillColor(GREY)
|
| 90 |
+
c.setFont("Helvetica", 8.5)
|
| 91 |
+
lines = _wrap(body, int(w / (0.22*cm)))
|
| 92 |
+
for i, line in enumerate(lines[:5]):
|
| 93 |
+
c.drawString(x + 0.3*cm, y + h - 0.75*cm - i * 0.45*cm, line)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def slide_1_title(c: Canvas) -> None:
|
| 97 |
+
c.setFillColor(NAVY)
|
| 98 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 99 |
+
c.setFillColor(BLUE)
|
| 100 |
+
c.rect(0, 0, W, 0.5*cm, fill=1, stroke=0)
|
| 101 |
+
c.setFillColor(GOLD)
|
| 102 |
+
c.rect(0, 0.5*cm, W, 0.12*cm, fill=1, stroke=0)
|
| 103 |
+
|
| 104 |
+
c.setFillColor(WHITE)
|
| 105 |
+
c.setFont("Helvetica", 40)
|
| 106 |
+
c.drawCentredString(W / 2, H - 6*cm, "⚖️")
|
| 107 |
+
c.setFont("Helvetica-Bold", 36)
|
| 108 |
+
c.drawCentredString(W / 2, H - 8*cm, "TenderIQ")
|
| 109 |
+
c.setFont("Helvetica", 15)
|
| 110 |
+
c.setFillColor(colors.HexColor("#CBD5E1"))
|
| 111 |
+
c.drawCentredString(W / 2, H - 9.2*cm,
|
| 112 |
+
"Explainable AI for Government Tender Evaluation")
|
| 113 |
+
|
| 114 |
+
c.setFillColor(GOLD)
|
| 115 |
+
c.roundRect(W/2 - 5*cm, H - 11.5*cm, 10*cm, 1.1*cm, 0.3*cm, fill=1, stroke=0)
|
| 116 |
+
c.setFillColor(NAVY)
|
| 117 |
+
c.setFont("Helvetica-Bold", 11)
|
| 118 |
+
c.drawCentredString(W / 2, H - 11.0*cm, "CRPF Hackathon · Theme 3")
|
| 119 |
+
|
| 120 |
+
c.setFillColor(colors.HexColor("#64748B"))
|
| 121 |
+
c.setFont("Helvetica", 9)
|
| 122 |
+
c.drawCentredString(W / 2, H - 13.5*cm,
|
| 123 |
+
"Central Reserve Police Force · Ministry of Home Affairs")
|
| 124 |
+
c.drawCentredString(W / 2, H - 14.1*cm,
|
| 125 |
+
"AI-Based Tender Evaluation and Eligibility Analysis")
|
| 126 |
+
|
| 127 |
+
_footer(c, 1)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def slide_2_problem(c: Canvas) -> None:
|
| 131 |
+
_header_bar(c, "The Problem", "Manual tender evaluation is slow, inconsistent, and opaque")
|
| 132 |
+
_footer(c, 2)
|
| 133 |
+
|
| 134 |
+
y = H - 4.0*cm
|
| 135 |
+
c.setFont("Helvetica-Bold", 11)
|
| 136 |
+
c.setFillColor(NAVY)
|
| 137 |
+
c.drawString(1.8*cm, y, "Government procurement officers today must:")
|
| 138 |
+
y -= 0.6*cm
|
| 139 |
+
|
| 140 |
+
problems = [
|
| 141 |
+
"Manually read hundreds of pages of tender documents and bidder submissions",
|
| 142 |
+
"Identify eligibility criteria buried in legal language across multiple sections",
|
| 143 |
+
"Cross-check financial statements, certificates, and project records for each bidder",
|
| 144 |
+
"Handle scanned documents, photographs, and mixed-format submissions",
|
| 145 |
+
"Reach consistent decisions — yet two evaluators routinely disagree on the same bid",
|
| 146 |
+
"Produce an auditable trail for every decision, under compliance and RTI pressure",
|
| 147 |
+
]
|
| 148 |
+
for p in problems:
|
| 149 |
+
y = _bullet(c, 1.8*cm, y, p)
|
| 150 |
+
y -= 0.25*cm
|
| 151 |
+
|
| 152 |
+
y -= 0.3*cm
|
| 153 |
+
c.setFillColor(LBLUE)
|
| 154 |
+
c.roundRect(1.8*cm, y - 1.6*cm, W - 3.6*cm, 1.6*cm, 0.3*cm, fill=1, stroke=0)
|
| 155 |
+
c.setFillColor(BLUE)
|
| 156 |
+
c.setFont("Helvetica-Bold", 11)
|
| 157 |
+
c.drawCentredString(W / 2, y - 0.7*cm,
|
| 158 |
+
"For one tender, a committee may spend 3–5 days.")
|
| 159 |
+
c.setFont("Helvetica", 10)
|
| 160 |
+
c.setFillColor(GREY)
|
| 161 |
+
c.drawCentredString(W / 2, y - 1.15*cm,
|
| 162 |
+
"TenderIQ reduces this to minutes, with full explainability.")
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def slide_3_solution(c: Canvas) -> None:
|
| 166 |
+
_header_bar(c, "Our Solution", "TenderIQ automates evaluation while preserving human oversight")
|
| 167 |
+
_footer(c, 3)
|
| 168 |
+
|
| 169 |
+
pillars = [
|
| 170 |
+
(BLUE, "📄 Extract", "DeepSeek LLM reads the tender PDF and structures every eligibility criterion as JSON — category, rule, source clause, query hints."),
|
| 171 |
+
(GREEN, "🔍 OCR & Index","Three-tier pipeline handles any document: PyMuPDF → Tesseract → Vision LLM. All text indexed into ChromaDB with provenance."),
|
| 172 |
+
(AMBER, "⚖️ Evaluate", "Per-criterion vector search + LLM evaluation. Combined confidence score. Safety rule: never silent disqualification."),
|
| 173 |
+
(RED, "👤 Review", "Borderline verdicts surface in a human review queue with full evidence. Every action logged to SQLite for compliance."),
|
| 174 |
+
]
|
| 175 |
+
cw = (W - 3.6*cm) / 2
|
| 176 |
+
ch = 5.0*cm
|
| 177 |
+
positions = [
|
| 178 |
+
(1.8*cm, H - 9.5*cm),
|
| 179 |
+
(1.8*cm + cw + 0.4*cm, H - 9.5*cm),
|
| 180 |
+
(1.8*cm, H - 9.5*cm - ch - 0.5*cm),
|
| 181 |
+
(1.8*cm + cw + 0.4*cm, H - 9.5*cm - ch - 0.5*cm),
|
| 182 |
+
]
|
| 183 |
+
for (px, py), (color, title, body) in zip(positions, pillars):
|
| 184 |
+
_card(c, px, py, cw, ch, title, body, color)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def slide_4_architecture(c: Canvas) -> None:
|
| 188 |
+
_header_bar(c, "Architecture", "Single-process Streamlit app — no separate services")
|
| 189 |
+
_footer(c, 4)
|
| 190 |
+
|
| 191 |
+
boxes = [
|
| 192 |
+
(1.8*cm, H - 5.5*cm, 5.0*cm, 1.2*cm, "Tender PDF", LBLUE, BLUE),
|
| 193 |
+
(8.5*cm, H - 5.5*cm, 5.5*cm, 1.2*cm, "Criteria (JSON)", LBLUE, BLUE),
|
| 194 |
+
(15.5*cm, H - 5.5*cm, 4.5*cm, 1.2*cm, "ChromaDB Index", LGREY, GREY),
|
| 195 |
+
(1.8*cm, H - 9.0*cm, 5.0*cm, 1.2*cm, "Bidder Docs", LGREY, GREY),
|
| 196 |
+
(8.5*cm, H - 9.0*cm, 5.5*cm, 1.2*cm, "OCR Pipeline ×3", colors.HexColor("#FDF4FF"), colors.HexColor("#7E22CE")),
|
| 197 |
+
(15.5*cm, H - 9.0*cm, 4.5*cm, 1.2*cm, "Verdicts", colors.HexColor("#F0FDF4"), GREEN),
|
| 198 |
+
(5.5*cm, H - 13.0*cm, 10*cm, 1.2*cm, "SQLite Audit Log", colors.HexColor("#FFFBEB"), AMBER),
|
| 199 |
+
]
|
| 200 |
+
for bx, by, bw, bh, label, fill, stroke in boxes:
|
| 201 |
+
c.setFillColor(fill)
|
| 202 |
+
c.setStrokeColor(stroke)
|
| 203 |
+
c.roundRect(bx, by, bw, bh, 0.25*cm, fill=1, stroke=1)
|
| 204 |
+
c.setFillColor(stroke)
|
| 205 |
+
c.setFont("Helvetica-Bold", 9)
|
| 206 |
+
c.drawCentredString(bx + bw / 2, by + 0.4*cm, label)
|
| 207 |
+
|
| 208 |
+
arrows = [
|
| 209 |
+
(6.8*cm, H - 4.95*cm, 8.5*cm, H - 4.95*cm),
|
| 210 |
+
(8.5*cm + 5.5*cm, H - 4.95*cm, 15.5*cm, H - 4.95*cm),
|
| 211 |
+
(1.8*cm + 5.0*cm, H - 8.45*cm, 8.5*cm, H - 8.45*cm),
|
| 212 |
+
(8.5*cm + 5.5*cm, H - 8.45*cm, 15.5*cm, H - 8.45*cm),
|
| 213 |
+
(15.5*cm + 2.25*cm, H - 9.0*cm, 15.5*cm + 2.25*cm, H - 5.5*cm - 1.2*cm),
|
| 214 |
+
(W / 2, H - 9.0*cm - 0, W / 2, H - 13.0*cm + 1.2*cm),
|
| 215 |
+
]
|
| 216 |
+
c.setStrokeColor(GREY)
|
| 217 |
+
c.setLineWidth(1)
|
| 218 |
+
for x1, y1, x2, y2 in arrows:
|
| 219 |
+
c.line(x1, y1, x2, y2)
|
| 220 |
+
|
| 221 |
+
c.setFont("Helvetica", 8.5)
|
| 222 |
+
c.setFillColor(GREY)
|
| 223 |
+
c.drawCentredString(W / 2, H - 14.5*cm,
|
| 224 |
+
"DeepSeek API · ChromaDB (embedded) · SQLite · No external services")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def slide_5_ocr(c: Canvas) -> None:
|
| 228 |
+
_header_bar(c, "Three-Tier OCR Pipeline",
|
| 229 |
+
"Handles typed PDFs, scanned documents, and photographs")
|
| 230 |
+
_footer(c, 5)
|
| 231 |
+
|
| 232 |
+
tiers = [
|
| 233 |
+
(BLUE, "Tier 1 — PyMuPDF",
|
| 234 |
+
"Cost: free, instant\nTrigger: document is a typed/digital PDF\nConfidence: 1.0 (lossless)\nOutput: exact text with page numbers"),
|
| 235 |
+
(colors.HexColor("#7E22CE"), "Tier 2 — Tesseract OCR",
|
| 236 |
+
"Cost: free, fast\nTrigger: scanned PDF or image file\nConfidence: mean of per-word scores\nOutput: extracted text (quality varies)"),
|
| 237 |
+
(AMBER, "Tier 3 — DeepSeek Vision LLM",
|
| 238 |
+
"Cost: API call, slower\nTrigger: Tesseract confidence < 65%\nConfidence: 0.95\nOutput: faithfully transcribed text\nAudit: vision_ocr_invoked logged"),
|
| 239 |
+
]
|
| 240 |
+
tw = (W - 4.0*cm) / 3
|
| 241 |
+
for i, (color, title, body) in enumerate(tiers):
|
| 242 |
+
x = 1.8*cm + i * (tw + 0.3*cm)
|
| 243 |
+
y = H - 9.0*cm
|
| 244 |
+
c.setFillColor(color)
|
| 245 |
+
c.roundRect(x, y, tw, 5.5*cm, 0.3*cm, fill=1, stroke=0)
|
| 246 |
+
c.setFillColor(WHITE)
|
| 247 |
+
c.setFont("Helvetica-Bold", 10)
|
| 248 |
+
c.drawCentredString(x + tw / 2, y + 5.0*cm, title)
|
| 249 |
+
c.setFont("Helvetica", 9)
|
| 250 |
+
for j, line in enumerate(body.split("\n")):
|
| 251 |
+
c.drawString(x + 0.4*cm, y + 4.2*cm - j * 0.5*cm, line)
|
| 252 |
+
|
| 253 |
+
y = H - 11.0*cm
|
| 254 |
+
c.setFillColor(LGREY)
|
| 255 |
+
c.roundRect(1.8*cm, y - 1.8*cm, W - 3.6*cm, 1.8*cm, 0.3*cm, fill=1, stroke=0)
|
| 256 |
+
c.setFillColor(NAVY)
|
| 257 |
+
c.setFont("Helvetica-Bold", 10)
|
| 258 |
+
c.drawCentredString(W / 2, y - 0.65*cm, "Demo: Bidder C submits a blurry, rotated CA certificate scan")
|
| 259 |
+
c.setFont("Helvetica", 9)
|
| 260 |
+
c.setFillColor(GREY)
|
| 261 |
+
c.drawCentredString(W / 2, y - 1.2*cm,
|
| 262 |
+
"Tesseract confidence ~55% → Vision LLM transcribes correctly → combined confidence 0.58 → needs_review")
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def slide_6_explainability(c: Canvas) -> None:
|
| 266 |
+
_header_bar(c, "Explainability & Compliance",
|
| 267 |
+
"Every verdict is traceable to a document, page, and model decision")
|
| 268 |
+
_footer(c, 6)
|
| 269 |
+
|
| 270 |
+
features = [
|
| 271 |
+
("Criterion-level verdicts",
|
| 272 |
+
"Each (bidder × criterion) pair has an independent verdict with extracted value, source document, page number, OCR tier, LLM confidence, and plain-English reason."),
|
| 273 |
+
("Never silent disqualification",
|
| 274 |
+
"The safety threshold rule: if combined confidence is 0.55–0.80 and the LLM says not_eligible, the verdict is downgraded to needs_review and surfaced for human review."),
|
| 275 |
+
("Full audit trail",
|
| 276 |
+
"Every action is logged to SQLite: criteria_extracted, bidder_processed, criterion_evaluated, human_review_action, vision_ocr_invoked, precomputed_fallback_used."),
|
| 277 |
+
("Interpretability tab",
|
| 278 |
+
"Plain-English explanation of each verdict with inline PDF page previews. LLM-powered Q&A lets officers ask specific questions with source citations."),
|
| 279 |
+
("Human review queue",
|
| 280 |
+
"Flagged verdicts show the evidence snippet, extracted value, source page, and OCR tier badge. Officers Approve / Edit & Approve / Reject with audit logging."),
|
| 281 |
+
("Pre-computed fallback",
|
| 282 |
+
"If the API is unavailable, pre-computed JSON is served transparently. The sidebar shows an amber dot and a banner. No silent failures."),
|
| 283 |
+
]
|
| 284 |
+
col_w = (W - 4.0*cm) / 2
|
| 285 |
+
for i, (title, body) in enumerate(features):
|
| 286 |
+
col = i % 2
|
| 287 |
+
row = i // 2
|
| 288 |
+
x = 1.8*cm + col * (col_w + 0.4*cm)
|
| 289 |
+
y = H - 4.5*cm - row * 2.8*cm
|
| 290 |
+
c.setFillColor(WHITE)
|
| 291 |
+
c.setStrokeColor(BORD)
|
| 292 |
+
c.roundRect(x, y - 2.0*cm, col_w, 2.0*cm, 0.25*cm, fill=1, stroke=1)
|
| 293 |
+
c.setFillColor(BLUE)
|
| 294 |
+
c.setFont("Helvetica-Bold", 9)
|
| 295 |
+
c.drawString(x + 0.3*cm, y - 0.45*cm, title)
|
| 296 |
+
c.setFillColor(GREY)
|
| 297 |
+
c.setFont("Helvetica", 8)
|
| 298 |
+
lines = _wrap(body, int(col_w / (0.22*cm)))
|
| 299 |
+
for j, line in enumerate(lines[:3]):
|
| 300 |
+
c.drawString(x + 0.3*cm, y - 0.9*cm - j * 0.38*cm, line)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def slide_7_demo(c: Canvas) -> None:
|
| 304 |
+
_header_bar(c, "Demo: Three Test Scenarios",
|
| 305 |
+
"Mock CRPF tender with 5 criteria evaluated against 3 realistic bidders")
|
| 306 |
+
_footer(c, 7)
|
| 307 |
+
|
| 308 |
+
scenarios = [
|
| 309 |
+
(GREEN, "✅ Bidder A — Eligible",
|
| 310 |
+
"Apex Constructions Pvt. Ltd.",
|
| 311 |
+
[
|
| 312 |
+
"C1 Turnover: INR 6.37 Cr avg — exceeds 5 Cr threshold",
|
| 313 |
+
"C2 Projects: 5 completed including CRPF barracks (2024)",
|
| 314 |
+
"C3 GST: GSTIN 27AABCA1234F1Z5, Active",
|
| 315 |
+
"C4 ISO 9001:2015: Valid through June 2027",
|
| 316 |
+
"C5 Paramilitary: CRPF Camp Pune project on record",
|
| 317 |
+
]),
|
| 318 |
+
(RED, "❌ Bidder B — Not Eligible",
|
| 319 |
+
"BuildRight Enterprises",
|
| 320 |
+
[
|
| 321 |
+
"C1 Turnover: INR 1.5 Cr avg — BELOW 5 Cr threshold",
|
| 322 |
+
"C2 Projects: 4 completed — passes",
|
| 323 |
+
"C3 GST: GSTIN 29AABCB5678G1Z3, Active",
|
| 324 |
+
"C4 ISO 9001:2015: Valid through August 2027",
|
| 325 |
+
"C5 Paramilitary: No relevant experience",
|
| 326 |
+
]),
|
| 327 |
+
(AMBER, "⚠️ Bidder C — Needs Review",
|
| 328 |
+
"Shree Constructions & Services",
|
| 329 |
+
[
|
| 330 |
+
"C1 Turnover: Scanned cert → Tesseract 55% → Vision LLM",
|
| 331 |
+
" INR 5.4 Cr found, but borderline — human review required",
|
| 332 |
+
"C2 Projects: Exactly 3 — borderline meets threshold",
|
| 333 |
+
"C3 GST: GSTIN 24AABCC9012H1Z1, Active",
|
| 334 |
+
"C4 ISO 9001:2015: Valid through September 2027",
|
| 335 |
+
]),
|
| 336 |
+
]
|
| 337 |
+
cw = (W - 4.0*cm) / 3
|
| 338 |
+
for i, (color, title, company, bullets) in enumerate(scenarios):
|
| 339 |
+
x = 1.8*cm + i * (cw + 0.3*cm)
|
| 340 |
+
y_top = H - 3.8*cm
|
| 341 |
+
|
| 342 |
+
c.setFillColor(color)
|
| 343 |
+
c.roundRect(x, y_top - 0.9*cm, cw, 0.9*cm, 0.25*cm, fill=1, stroke=0)
|
| 344 |
+
c.setFillColor(WHITE)
|
| 345 |
+
c.setFont("Helvetica-Bold", 9)
|
| 346 |
+
c.drawCentredString(x + cw / 2, y_top - 0.55*cm, title)
|
| 347 |
+
|
| 348 |
+
c.setFillColor(WHITE)
|
| 349 |
+
c.setStrokeColor(BORD)
|
| 350 |
+
c.roundRect(x, y_top - 8.5*cm, cw, 7.6*cm, 0.25*cm, fill=1, stroke=1)
|
| 351 |
+
c.rect(x, y_top - 0.9*cm, cw, 0.2*cm, fill=1, stroke=0)
|
| 352 |
+
|
| 353 |
+
c.setFillColor(GREY)
|
| 354 |
+
c.setFont("Helvetica-Oblique", 8)
|
| 355 |
+
c.drawString(x + 0.3*cm, y_top - 1.35*cm, company)
|
| 356 |
+
|
| 357 |
+
c.setFont("Helvetica", 8)
|
| 358 |
+
c.setFillColor(NAVY)
|
| 359 |
+
for j, b in enumerate(bullets):
|
| 360 |
+
c.drawString(x + 0.3*cm, y_top - 2.0*cm - j * 0.5*cm, b)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def slide_8_stack(c: Canvas) -> None:
|
| 364 |
+
_header_bar(c, "Technology Stack & Impact",
|
| 365 |
+
"Built for the hackathon — deployable to Streamlit Cloud or HuggingFace Spaces in minutes")
|
| 366 |
+
_footer(c, 8)
|
| 367 |
+
|
| 368 |
+
stack = [
|
| 369 |
+
("UI & Orchestration", "Streamlit 1.39", "Single-process app, tabs, session state"),
|
| 370 |
+
("LLM", "DeepSeek API", "chat_json + chat_vision (OpenAI-compatible)"),
|
| 371 |
+
("OCR Tier 1", "PyMuPDF 1.24", "Lossless text extraction from digital PDFs"),
|
| 372 |
+
("OCR Tier 2", "Tesseract", "Open-source OCR for scanned documents"),
|
| 373 |
+
("OCR Tier 3", "DeepSeek Vision","Multimodal LLM for low-confidence scans"),
|
| 374 |
+
("Vector Store", "ChromaDB 0.5", "Embedded, file-backed, all-MiniLM-L6-v2"),
|
| 375 |
+
("Schemas", "Pydantic v2", "Strict validation of all LLM outputs"),
|
| 376 |
+
("Audit Log", "SQLite", "Append-only, exportable as CSV"),
|
| 377 |
+
]
|
| 378 |
+
c.setFillColor(NAVY)
|
| 379 |
+
c.setFont("Helvetica-Bold", 9)
|
| 380 |
+
col_x = [1.8*cm, 6.5*cm, 11.5*cm]
|
| 381 |
+
for x, lbl in zip(col_x, ["Component", "Technology", "Role"]):
|
| 382 |
+
c.drawString(x, H - 4.2*cm, lbl)
|
| 383 |
+
c.setStrokeColor(BORD)
|
| 384 |
+
c.line(1.8*cm, H - 4.4*cm, W - 1.8*cm, H - 4.4*cm)
|
| 385 |
+
|
| 386 |
+
for i, (comp, tech, role) in enumerate(stack):
|
| 387 |
+
y = H - 4.9*cm - i * 0.6*cm
|
| 388 |
+
if i % 2 == 0:
|
| 389 |
+
c.setFillColor(LGREY)
|
| 390 |
+
c.rect(1.8*cm, y - 0.1*cm, W - 3.6*cm, 0.55*cm, fill=1, stroke=0)
|
| 391 |
+
c.setFillColor(NAVY); c.setFont("Helvetica-Bold", 8.5); c.drawString(1.8*cm + 0.2*cm, y + 0.2*cm, comp)
|
| 392 |
+
c.setFillColor(BLUE); c.setFont("Helvetica", 8.5); c.drawString(6.5*cm + 0.2*cm, y + 0.2*cm, tech)
|
| 393 |
+
c.setFillColor(GREY); c.setFont("Helvetica", 8.5); c.drawString(11.5*cm + 0.2*cm, y + 0.2*cm, role)
|
| 394 |
+
|
| 395 |
+
y_impact = H - 10.5*cm
|
| 396 |
+
c.setFillColor(NAVY)
|
| 397 |
+
c.roundRect(1.8*cm, y_impact - 2.8*cm, W - 3.6*cm, 2.8*cm, 0.3*cm, fill=1, stroke=0)
|
| 398 |
+
c.setFillColor(GOLD)
|
| 399 |
+
c.setFont("Helvetica-Bold", 11)
|
| 400 |
+
c.drawCentredString(W / 2, y_impact - 0.7*cm, "Business Impact")
|
| 401 |
+
impacts = [
|
| 402 |
+
"⏱ Days of manual evaluation → minutes of automated processing",
|
| 403 |
+
"📋 Criterion-level audit trail satisfies RTI and compliance requirements",
|
| 404 |
+
"🔍 Every verdict traceable to a document, page, OCR tier, and model version",
|
| 405 |
+
]
|
| 406 |
+
c.setFont("Helvetica", 9.5)
|
| 407 |
+
c.setFillColor(colors.HexColor("#CBD5E1"))
|
| 408 |
+
for j, imp in enumerate(impacts):
|
| 409 |
+
c.drawString(2.5*cm, y_impact - 1.35*cm - j * 0.5*cm, imp)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def main() -> None:
|
| 413 |
+
out = BASE_DIR / "deck" / "TenderIQ_Pitch.pdf"
|
| 414 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 415 |
+
|
| 416 |
+
c = Canvas(str(out), pagesize=A4)
|
| 417 |
+
slides = [
|
| 418 |
+
slide_1_title,
|
| 419 |
+
slide_2_problem,
|
| 420 |
+
slide_3_solution,
|
| 421 |
+
slide_4_architecture,
|
| 422 |
+
slide_5_ocr,
|
| 423 |
+
slide_6_explainability,
|
| 424 |
+
slide_7_demo,
|
| 425 |
+
slide_8_stack,
|
| 426 |
+
]
|
| 427 |
+
for fn in slides:
|
| 428 |
+
fn(c)
|
| 429 |
+
c.showPage()
|
| 430 |
+
c.save()
|
| 431 |
+
print(f"Deck saved: {out} ({len(slides)} slides)")
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
if __name__ == "__main__":
|
| 435 |
+
main()
|
|
@@ -2,32 +2,64 @@ import streamlit as st
|
|
| 2 |
|
| 3 |
|
| 4 |
def verdict_pill(verdict: str) -> str:
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
-
def
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def ocr_tier_badge(source_type: str) -> str:
|
| 18 |
-
|
| 19 |
-
"text_pdf":
|
| 20 |
-
"tesseract":
|
| 21 |
-
"vision_llm": "👁
|
| 22 |
}
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
if
|
| 28 |
-
return "
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
|
| 4 |
def verdict_pill(verdict: str) -> str:
|
| 5 |
+
cfg = {
|
| 6 |
+
"eligible": ("tiq-eligible", "✅ Eligible"),
|
| 7 |
+
"not_eligible": ("tiq-not-elig", "❌ Not Eligible"),
|
| 8 |
+
"needs_review": ("tiq-review", "⚠️ Needs Review"),
|
| 9 |
+
}
|
| 10 |
+
cls, label = cfg.get(verdict, ("tiq-review", verdict))
|
| 11 |
+
return f'<span class="tiq-badge {cls}">{label}</span>'
|
| 12 |
|
| 13 |
|
| 14 |
+
def category_badge(category: str) -> str:
|
| 15 |
+
cfg = {
|
| 16 |
+
"financial": ("tiq-cat-fin", "💰 Financial"),
|
| 17 |
+
"technical": ("tiq-cat-tech", "🔧 Technical"),
|
| 18 |
+
"compliance": ("tiq-cat-comp", "📋 Compliance"),
|
| 19 |
+
}
|
| 20 |
+
cls, label = cfg.get(category, ("tiq-cat-comp", category))
|
| 21 |
+
return f'<span class="tiq-badge {cls}">{label}</span>'
|
| 22 |
|
| 23 |
|
| 24 |
def ocr_tier_badge(source_type: str) -> str:
|
| 25 |
+
cfg = {
|
| 26 |
+
"text_pdf": ("tiq-ocr-text", "📄 Typed PDF"),
|
| 27 |
+
"tesseract": ("tiq-ocr-tess", "🔍 Tesseract"),
|
| 28 |
+
"vision_llm": ("tiq-ocr-vision", "👁 Vision LLM"),
|
| 29 |
}
|
| 30 |
+
cls, label = cfg.get(source_type, ("tiq-ocr-text", source_type))
|
| 31 |
+
return f'<span class="tiq-badge {cls}">{label}</span>'
|
| 32 |
|
| 33 |
|
| 34 |
+
def mandatory_badge(mandatory: bool) -> str:
|
| 35 |
+
if mandatory:
|
| 36 |
+
return '<span class="tiq-badge tiq-mand">🔴 Mandatory</span>'
|
| 37 |
+
return '<span class="tiq-badge tiq-optional">🟡 Optional</span>'
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def confidence_bar(value: float, label: str = "Confidence") -> None:
|
| 41 |
+
pct = min(max(value, 0.0), 1.0)
|
| 42 |
+
color = "#22C55E" if pct >= 0.8 else "#F59E0B" if pct >= 0.55 else "#EF4444"
|
| 43 |
+
st.markdown(
|
| 44 |
+
f"""<div style="margin:4px 0 8px;">
|
| 45 |
+
<div style="display:flex;justify-content:space-between;
|
| 46 |
+
font-size:0.75rem;color:#64748B;margin-bottom:3px;">
|
| 47 |
+
<span>{label}</span><span style="font-weight:600;color:{color};">{pct:.0%}</span>
|
| 48 |
+
</div>
|
| 49 |
+
<div style="background:#E2E8F0;border-radius:6px;height:7px;overflow:hidden;">
|
| 50 |
+
<div style="width:{pct*100:.1f}%;background:{color};
|
| 51 |
+
height:100%;border-radius:6px;
|
| 52 |
+
transition:width 0.4s ease;"></div>
|
| 53 |
+
</div></div>""",
|
| 54 |
+
unsafe_allow_html=True,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def section_header(title: str, subtitle: str = "") -> None:
|
| 59 |
+
st.markdown(
|
| 60 |
+
f'<div class="tiq-section-header">'
|
| 61 |
+
f'<div style="font-size:1.1rem;font-weight:700;color:#0D1B2A;">{title}</div>'
|
| 62 |
+
+ (f'<div style="font-size:0.82rem;color:#64748B;margin-top:2px;">{subtitle}</div>' if subtitle else "")
|
| 63 |
+
+ "</div>",
|
| 64 |
+
unsafe_allow_html=True,
|
| 65 |
+
)
|
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CSS = """
|
| 2 |
+
<style>
|
| 3 |
+
/* ================================================================
|
| 4 |
+
TenderIQ — Professional Theme
|
| 5 |
+
================================================================ */
|
| 6 |
+
|
| 7 |
+
/* ── Global ──────────────────────────────────────────────────── */
|
| 8 |
+
.main .block-container {
|
| 9 |
+
padding-top: 1.5rem !important;
|
| 10 |
+
padding-bottom: 2rem !important;
|
| 11 |
+
max-width: 1200px;
|
| 12 |
+
}
|
| 13 |
+
h1 { font-weight: 800 !important; color: #0D1B2A !important; letter-spacing: -0.02em; }
|
| 14 |
+
h2 { font-weight: 700 !important; color: #0D1B2A !important; }
|
| 15 |
+
h3 { font-weight: 600 !important; color: #0D1B2A !important; }
|
| 16 |
+
h4 { font-weight: 600 !important; color: #1E3A5F !important; }
|
| 17 |
+
p { color: #374151; line-height: 1.7; }
|
| 18 |
+
code {
|
| 19 |
+
background: #EFF6FF !important;
|
| 20 |
+
color: #1E40AF !important;
|
| 21 |
+
padding: 2px 6px !important;
|
| 22 |
+
border-radius: 4px !important;
|
| 23 |
+
font-size: 0.85em !important;
|
| 24 |
+
border: 1px solid #DBEAFE;
|
| 25 |
+
}
|
| 26 |
+
hr { border-color: #E2E8F0 !important; margin: 1.25rem 0 !important; }
|
| 27 |
+
|
| 28 |
+
/* ── Sidebar ─────────────────────────────────────────────────── */
|
| 29 |
+
[data-testid="stSidebar"] {
|
| 30 |
+
background: linear-gradient(175deg, #0D1B2A 0%, #1E3A5F 100%) !important;
|
| 31 |
+
border-right: 1px solid #1E3A5F;
|
| 32 |
+
}
|
| 33 |
+
[data-testid="stSidebar"] p,
|
| 34 |
+
[data-testid="stSidebar"] span,
|
| 35 |
+
[data-testid="stSidebar"] label,
|
| 36 |
+
[data-testid="stSidebar"] div {
|
| 37 |
+
color: #CBD5E1 !important;
|
| 38 |
+
}
|
| 39 |
+
[data-testid="stSidebar"] h1,
|
| 40 |
+
[data-testid="stSidebar"] h2,
|
| 41 |
+
[data-testid="stSidebar"] h3,
|
| 42 |
+
[data-testid="stSidebar"] strong {
|
| 43 |
+
color: #F1F5F9 !important;
|
| 44 |
+
}
|
| 45 |
+
[data-testid="stSidebar"] .stButton > button {
|
| 46 |
+
background: rgba(255,255,255,0.07) !important;
|
| 47 |
+
border: 1px solid rgba(255,255,255,0.15) !important;
|
| 48 |
+
color: #E2E8F0 !important;
|
| 49 |
+
border-radius: 8px !important;
|
| 50 |
+
font-weight: 500 !important;
|
| 51 |
+
transition: all 0.2s ease !important;
|
| 52 |
+
width: 100%;
|
| 53 |
+
}
|
| 54 |
+
[data-testid="stSidebar"] .stButton > button:hover {
|
| 55 |
+
background: rgba(255,255,255,0.14) !important;
|
| 56 |
+
border-color: rgba(255,255,255,0.3) !important;
|
| 57 |
+
color: #FFFFFF !important;
|
| 58 |
+
}
|
| 59 |
+
[data-testid="stSidebar"] [data-testid="stDivider"] { border-color: rgba(255,255,255,0.12) !important; }
|
| 60 |
+
[data-testid="stSidebar"] .stAlert {
|
| 61 |
+
background: rgba(245,158,11,0.15) !important;
|
| 62 |
+
border: 1px solid rgba(245,158,11,0.3) !important;
|
| 63 |
+
border-radius: 8px !important;
|
| 64 |
+
}
|
| 65 |
+
[data-testid="stSidebar"] [data-testid="stCaptionContainer"] p { color: #94A3B8 !important; }
|
| 66 |
+
|
| 67 |
+
/* ── Tabs ────────────────────────────────────────────────────── */
|
| 68 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 69 |
+
background: #F1F5F9 !important;
|
| 70 |
+
border-radius: 12px !important;
|
| 71 |
+
padding: 5px !important;
|
| 72 |
+
gap: 2px !important;
|
| 73 |
+
border: 1px solid #E2E8F0;
|
| 74 |
+
}
|
| 75 |
+
.stTabs [data-baseweb="tab"] {
|
| 76 |
+
border-radius: 8px !important;
|
| 77 |
+
padding: 8px 18px !important;
|
| 78 |
+
font-weight: 500 !important;
|
| 79 |
+
font-size: 0.875rem !important;
|
| 80 |
+
color: #64748B !important;
|
| 81 |
+
border: none !important;
|
| 82 |
+
background: transparent !important;
|
| 83 |
+
transition: all 0.15s ease !important;
|
| 84 |
+
}
|
| 85 |
+
.stTabs [data-baseweb="tab"]:hover { color: #1E3A5F !important; background: rgba(255,255,255,0.6) !important; }
|
| 86 |
+
.stTabs [aria-selected="true"] {
|
| 87 |
+
background: #FFFFFF !important;
|
| 88 |
+
color: #0D1B2A !important;
|
| 89 |
+
box-shadow: 0 1px 6px rgba(0,0,0,0.1) !important;
|
| 90 |
+
font-weight: 600 !important;
|
| 91 |
+
}
|
| 92 |
+
.stTabs [data-baseweb="tab-highlight"] { display: none !important; }
|
| 93 |
+
.stTabs [data-baseweb="tab-border"] { display: none !important; }
|
| 94 |
+
|
| 95 |
+
/* ── Buttons ─────────────────────────────────────────────────── */
|
| 96 |
+
.stButton > button {
|
| 97 |
+
border-radius: 8px !important;
|
| 98 |
+
font-weight: 500 !important;
|
| 99 |
+
font-size: 0.875rem !important;
|
| 100 |
+
transition: all 0.2s ease !important;
|
| 101 |
+
border: 1px solid #E2E8F0 !important;
|
| 102 |
+
}
|
| 103 |
+
.stButton > button[kind="primary"] {
|
| 104 |
+
background: linear-gradient(135deg, #1E3A5F 0%, #2563EB 100%) !important;
|
| 105 |
+
color: #FFFFFF !important;
|
| 106 |
+
border: none !important;
|
| 107 |
+
box-shadow: 0 2px 8px rgba(37,99,235,0.3) !important;
|
| 108 |
+
}
|
| 109 |
+
.stButton > button[kind="primary"]:hover {
|
| 110 |
+
box-shadow: 0 4px 16px rgba(37,99,235,0.45) !important;
|
| 111 |
+
transform: translateY(-1px) !important;
|
| 112 |
+
}
|
| 113 |
+
.stButton > button[kind="secondary"] { color: #374151 !important; }
|
| 114 |
+
.stButton > button[kind="secondary"]:hover { background: #F1F5F9 !important; }
|
| 115 |
+
|
| 116 |
+
/* ── Metric cards ────────────────────────────────────────────── */
|
| 117 |
+
[data-testid="metric-container"] {
|
| 118 |
+
background: #FFFFFF !important;
|
| 119 |
+
border: 1px solid #E2E8F0 !important;
|
| 120 |
+
border-radius: 12px !important;
|
| 121 |
+
padding: 20px !important;
|
| 122 |
+
box-shadow: 0 1px 4px rgba(0,0,0,0.06) !important;
|
| 123 |
+
transition: box-shadow 0.2s, transform 0.2s;
|
| 124 |
+
}
|
| 125 |
+
[data-testid="metric-container"]:hover {
|
| 126 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important;
|
| 127 |
+
transform: translateY(-1px);
|
| 128 |
+
}
|
| 129 |
+
[data-testid="stMetricValue"] { font-size: 2.2rem !important; font-weight: 800 !important; color: #0D1B2A !important; }
|
| 130 |
+
[data-testid="stMetricLabel"] { font-size: 0.75rem !important; font-weight: 600 !important; color: #64748B !important; text-transform: uppercase; letter-spacing: 0.06em; }
|
| 131 |
+
[data-testid="stMetricDelta"] { font-size: 0.8rem !important; }
|
| 132 |
+
|
| 133 |
+
/* ── Bordered containers (cards) ─────────────────────────────── */
|
| 134 |
+
[data-testid="stVerticalBlockBorderWrapper"] {
|
| 135 |
+
border-radius: 12px !important;
|
| 136 |
+
border: 1px solid #E2E8F0 !important;
|
| 137 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.05) !important;
|
| 138 |
+
overflow: hidden;
|
| 139 |
+
transition: box-shadow 0.2s;
|
| 140 |
+
}
|
| 141 |
+
[data-testid="stVerticalBlockBorderWrapper"]:hover {
|
| 142 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.09) !important;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* ── Expanders ───────────────────────────────────────────────── */
|
| 146 |
+
[data-testid="stExpander"] {
|
| 147 |
+
border: 1px solid #E2E8F0 !important;
|
| 148 |
+
border-radius: 10px !important;
|
| 149 |
+
overflow: hidden !important;
|
| 150 |
+
margin-bottom: 4px !important;
|
| 151 |
+
}
|
| 152 |
+
[data-testid="stExpander"] summary {
|
| 153 |
+
font-weight: 500 !important;
|
| 154 |
+
color: #374151 !important;
|
| 155 |
+
padding: 10px 14px !important;
|
| 156 |
+
background: #F8FAFC;
|
| 157 |
+
}
|
| 158 |
+
[data-testid="stExpander"] summary:hover { background: #F1F5F9 !important; }
|
| 159 |
+
|
| 160 |
+
/* ── Alerts ──────────────────────────────────────────────────── */
|
| 161 |
+
[data-testid="stAlert"] { border-radius: 10px !important; border: none !important; }
|
| 162 |
+
div[data-testid="stAlert"][data-baseweb="notification"][kind="info"] {
|
| 163 |
+
background: #EFF6FF !important; border-left: 4px solid #3B82F6 !important;
|
| 164 |
+
}
|
| 165 |
+
div[data-testid="stAlert"][data-baseweb="notification"][kind="success"] {
|
| 166 |
+
background: #F0FDF4 !important; border-left: 4px solid #22C55E !important;
|
| 167 |
+
}
|
| 168 |
+
div[data-testid="stAlert"][data-baseweb="notification"][kind="warning"] {
|
| 169 |
+
background: #FFFBEB !important; border-left: 4px solid #F59E0B !important;
|
| 170 |
+
}
|
| 171 |
+
div[data-testid="stAlert"][data-baseweb="notification"][kind="error"] {
|
| 172 |
+
background: #FEF2F2 !important; border-left: 4px solid #EF4444 !important;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* ── Progress bar ────────────────────────────────────────────── */
|
| 176 |
+
.stProgress > div > div > div { border-radius: 6px !important; }
|
| 177 |
+
.stProgress > div > div > div > div {
|
| 178 |
+
background: linear-gradient(90deg, #1E3A5F, #2563EB) !important;
|
| 179 |
+
border-radius: 6px !important;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* ── Forms (inputs, selects) ─────────────────────────────────── */
|
| 183 |
+
[data-baseweb="input"],
|
| 184 |
+
[data-baseweb="select"],
|
| 185 |
+
[data-baseweb="textarea"] {
|
| 186 |
+
border-radius: 8px !important;
|
| 187 |
+
border-color: #E2E8F0 !important;
|
| 188 |
+
}
|
| 189 |
+
[data-baseweb="input"]:focus-within,
|
| 190 |
+
[data-baseweb="select"]:focus-within {
|
| 191 |
+
border-color: #2563EB !important;
|
| 192 |
+
box-shadow: 0 0 0 3px rgba(37,99,235,0.1) !important;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* ── Dataframe ───────────────────────────────────────────────── */
|
| 196 |
+
[data-testid="stDataFrame"] {
|
| 197 |
+
border-radius: 10px !important;
|
| 198 |
+
overflow: hidden !important;
|
| 199 |
+
border: 1px solid #E2E8F0 !important;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* ── Caption ─────────────────────────────────────────────────── */
|
| 203 |
+
[data-testid="stCaptionContainer"] p { color: #94A3B8 !important; font-size: 0.8rem !important; }
|
| 204 |
+
|
| 205 |
+
/* ── Spinner ─────────────────────────────────────────────────── */
|
| 206 |
+
.stSpinner > div { border-top-color: #2563EB !important; }
|
| 207 |
+
|
| 208 |
+
/* ── Hide Streamlit chrome ───────────────────────────────────── */
|
| 209 |
+
#MainMenu { visibility: hidden !important; }
|
| 210 |
+
footer { visibility: hidden !important; }
|
| 211 |
+
header { visibility: hidden !important; }
|
| 212 |
+
|
| 213 |
+
/* ── Verdict badge utility classes ───────────────────────────── */
|
| 214 |
+
.tiq-badge {
|
| 215 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 216 |
+
padding: 4px 12px; border-radius: 20px;
|
| 217 |
+
font-size: 0.8rem; font-weight: 600; white-space: nowrap;
|
| 218 |
+
}
|
| 219 |
+
.tiq-eligible { background:#D1FAE5; color:#065F46; border:1px solid #A7F3D0; }
|
| 220 |
+
.tiq-not-elig { background:#FEE2E2; color:#991B1B; border:1px solid #FECACA; }
|
| 221 |
+
.tiq-review { background:#FEF3C7; color:#92400E; border:1px solid #FDE68A; }
|
| 222 |
+
.tiq-cat-fin { background:#DBEAFE; color:#1E40AF; border:1px solid #BFDBFE; }
|
| 223 |
+
.tiq-cat-tech { background:#DCFCE7; color:#166534; border:1px solid #BBF7D0; }
|
| 224 |
+
.tiq-cat-comp { background:#FEF3C7; color:#92400E; border:1px solid #FDE68A; }
|
| 225 |
+
.tiq-ocr-text { background:#F1F5F9; color:#475569; border:1px solid #CBD5E1; }
|
| 226 |
+
.tiq-ocr-tess { background:#FDF4FF; color:#7E22CE; border:1px solid #E9D5FF; }
|
| 227 |
+
.tiq-ocr-vision { background:#FFF7ED; color:#C2410C; border:1px solid #FED7AA; }
|
| 228 |
+
.tiq-mand { background:#FEE2E2; color:#991B1B; border:1px solid #FECACA; }
|
| 229 |
+
.tiq-optional { background:#FFFBEB; color:#92400E; border:1px solid #FDE68A; }
|
| 230 |
+
|
| 231 |
+
/* ── Hero banner ─────────────────────────────────────────────── */
|
| 232 |
+
.tiq-hero {
|
| 233 |
+
background: linear-gradient(135deg, #0D1B2A 0%, #1E3A5F 50%, #2563EB 100%);
|
| 234 |
+
border-radius: 16px; padding: 2.5rem 2rem; margin-bottom: 1.5rem;
|
| 235 |
+
color: white;
|
| 236 |
+
}
|
| 237 |
+
.tiq-hero h1 { color: #FFFFFF !important; margin: 0; font-size: 2rem; }
|
| 238 |
+
.tiq-hero p { color: #CBD5E1 !important; margin: 0.5rem 0 0; font-size: 1.05rem; }
|
| 239 |
+
|
| 240 |
+
/* ── Kpi strip ───────────────────────────────────────────────── */
|
| 241 |
+
.tiq-kpi {
|
| 242 |
+
background: #FFFFFF; border-radius: 12px; padding: 18px 20px;
|
| 243 |
+
border: 1px solid #E2E8F0;
|
| 244 |
+
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
| 245 |
+
text-align: center;
|
| 246 |
+
}
|
| 247 |
+
.tiq-kpi-val { font-size: 2rem; font-weight: 800; color: #0D1B2A; }
|
| 248 |
+
.tiq-kpi-lbl { font-size: 0.72rem; font-weight: 600; color: #64748B;
|
| 249 |
+
text-transform: uppercase; letter-spacing: 0.07em; margin-top: 2px; }
|
| 250 |
+
|
| 251 |
+
/* ── Section header ─────────────────────────────────────────── */
|
| 252 |
+
.tiq-section-header {
|
| 253 |
+
border-left: 4px solid #2563EB; padding-left: 12px;
|
| 254 |
+
margin: 1.5rem 0 1rem;
|
| 255 |
+
}
|
| 256 |
+
</style>
|
| 257 |
+
"""
|
|
@@ -4,12 +4,20 @@ from core import bidder_processor, evaluator
|
|
| 4 |
from core.config import BIDDER_NAMES, DATA_DIR
|
| 5 |
from core.fallback import load_criteria
|
| 6 |
from core.schemas import Criterion
|
| 7 |
-
from ui.components import
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
_BIDDER_LABELS = {
|
| 10 |
-
"bidder_a": "
|
| 11 |
-
"bidder_b": "
|
| 12 |
-
"bidder_c": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
|
|
@@ -21,11 +29,11 @@ def _get_criteria() -> list[Criterion]:
|
|
| 21 |
|
| 22 |
|
| 23 |
def _overall_verdict(verdicts: list[dict], crit_map: dict) -> str:
|
| 24 |
-
|
| 25 |
-
|
| 26 |
crit_map[v["criterion_id"]].mandatory]
|
| 27 |
if not mandatory:
|
| 28 |
-
mandatory = verdicts
|
| 29 |
if any(v["verdict"] == "not_eligible" for v in mandatory):
|
| 30 |
return "not_eligible"
|
| 31 |
if any(v["verdict"] == "needs_review" for v in mandatory):
|
|
@@ -35,18 +43,19 @@ def _overall_verdict(verdicts: list[dict], crit_map: dict) -> str:
|
|
| 35 |
|
| 36 |
def render() -> None:
|
| 37 |
st.header("Bidder Evaluation")
|
|
|
|
| 38 |
|
| 39 |
selected = st.multiselect(
|
| 40 |
"Select bidders to evaluate",
|
| 41 |
-
options=
|
| 42 |
-
default=
|
| 43 |
-
format_func=lambda x: _BIDDER_LABELS.get(x, x),
|
| 44 |
)
|
| 45 |
|
| 46 |
-
if st.button("Run Evaluation", type="primary"):
|
| 47 |
criteria = _get_criteria()
|
| 48 |
verdicts_dict: dict = {}
|
| 49 |
-
progress = st.progress(0, text="Starting
|
| 50 |
total = len(selected) * len(criteria)
|
| 51 |
done = 0
|
| 52 |
for bidder_id in selected:
|
|
@@ -54,7 +63,7 @@ def render() -> None:
|
|
| 54 |
f for f in (DATA_DIR / "bidders" / bidder_id).iterdir()
|
| 55 |
if f.suffix.lower() in {".pdf", ".png", ".jpg"}
|
| 56 |
)
|
| 57 |
-
with st.spinner(f"Processing {
|
| 58 |
bidder_processor.process_bidder(bidder_id, files)
|
| 59 |
verdicts_list = []
|
| 60 |
for c in criteria:
|
|
@@ -62,17 +71,21 @@ def render() -> None:
|
|
| 62 |
verdicts_list.append(v.model_dump())
|
| 63 |
done += 1
|
| 64 |
progress.progress(done / total,
|
| 65 |
-
text=f"Evaluated {c.id}
|
| 66 |
verdicts_dict[bidder_id] = verdicts_list
|
| 67 |
st.session_state["verdicts"] = verdicts_dict
|
| 68 |
progress.empty()
|
| 69 |
-
st.success("Evaluation complete.")
|
| 70 |
st.rerun()
|
| 71 |
|
| 72 |
verdicts_data = st.session_state.get("verdicts", {})
|
| 73 |
criteria = _get_criteria()
|
| 74 |
crit_map = {c.id: c for c in criteria}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
if st.session_state.get("fallback_active"):
|
| 77 |
st.warning("⚠ Live API unavailable — showing pre-computed results.")
|
| 78 |
|
|
@@ -81,58 +94,103 @@ def render() -> None:
|
|
| 81 |
continue
|
| 82 |
verdicts = verdicts_data[bidder_id]
|
| 83 |
overall = _overall_verdict(verdicts, crit_map)
|
| 84 |
-
|
| 85 |
-
friendly =
|
| 86 |
-
|
| 87 |
-
if crit_map.get(v["criterion_id"]) and
|
| 88 |
-
crit_map[v["criterion_id"]].mandatory)
|
| 89 |
passed = sum(1 for v in verdicts
|
| 90 |
if v["verdict"] == "eligible" and
|
| 91 |
crit_map.get(v["criterion_id"]) and
|
| 92 |
crit_map[v["criterion_id"]].mandatory)
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
with st.container(border=True):
|
|
|
|
| 95 |
st.markdown(
|
| 96 |
-
f"
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
unsafe_allow_html=True,
|
| 100 |
)
|
| 101 |
|
| 102 |
# Column headers
|
| 103 |
-
hcols = st.columns([3, 2, 2,
|
| 104 |
-
hcols[
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
for v in verdicts:
|
| 112 |
crit = crit_map.get(v["criterion_id"])
|
| 113 |
crit_title = crit.title if crit else v["criterion_id"]
|
| 114 |
-
|
| 115 |
cat = category_badge(crit.category if crit else "compliance")
|
| 116 |
|
| 117 |
-
cols = st.columns([3, 2, 2,
|
| 118 |
-
cols[0].markdown(
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
if v.get("source"):
|
| 122 |
src = v["source"]
|
| 123 |
tier = ocr_tier_badge(src["source_type"])
|
| 124 |
-
cols[3].markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
else:
|
| 126 |
-
cols[3].markdown("
|
| 127 |
-
cols[4].markdown(cat)
|
| 128 |
|
| 129 |
-
|
| 130 |
-
confidence_bar(conf)
|
| 131 |
|
| 132 |
if v.get("reason") or (v.get("source") and v["source"].get("snippet")):
|
| 133 |
-
with st.expander("
|
| 134 |
if v.get("reason"):
|
| 135 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
if v.get("source") and v["source"].get("snippet"):
|
| 137 |
-
st.markdown(
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from core.config import BIDDER_NAMES, DATA_DIR
|
| 5 |
from core.fallback import load_criteria
|
| 6 |
from core.schemas import Criterion
|
| 7 |
+
from ui.components import (
|
| 8 |
+
category_badge, confidence_bar, mandatory_badge,
|
| 9 |
+
ocr_tier_badge, section_header, verdict_pill,
|
| 10 |
+
)
|
| 11 |
|
| 12 |
_BIDDER_LABELS = {
|
| 13 |
+
"bidder_a": "Apex Constructions Pvt. Ltd.",
|
| 14 |
+
"bidder_b": "BuildRight Enterprises",
|
| 15 |
+
"bidder_c": "Shree Constructions & Services",
|
| 16 |
+
}
|
| 17 |
+
_BIDDER_SUBLABELS = {
|
| 18 |
+
"bidder_a": "Clearly Eligible",
|
| 19 |
+
"bidder_b": "Ineligible — Turnover Below Threshold",
|
| 20 |
+
"bidder_c": "Needs Review — Scanned Certificate",
|
| 21 |
}
|
| 22 |
|
| 23 |
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def _overall_verdict(verdicts: list[dict], crit_map: dict) -> str:
|
| 32 |
+
mandatory = [v for v in verdicts
|
| 33 |
+
if crit_map.get(v["criterion_id"]) and
|
| 34 |
crit_map[v["criterion_id"]].mandatory]
|
| 35 |
if not mandatory:
|
| 36 |
+
mandatory = verdicts
|
| 37 |
if any(v["verdict"] == "not_eligible" for v in mandatory):
|
| 38 |
return "not_eligible"
|
| 39 |
if any(v["verdict"] == "needs_review" for v in mandatory):
|
|
|
|
| 43 |
|
| 44 |
def render() -> None:
|
| 45 |
st.header("Bidder Evaluation")
|
| 46 |
+
st.caption("Run the full evaluation pipeline or load pre-computed results from the Overview tab.")
|
| 47 |
|
| 48 |
selected = st.multiselect(
|
| 49 |
"Select bidders to evaluate",
|
| 50 |
+
options=list(BIDDER_NAMES.keys()),
|
| 51 |
+
default=list(BIDDER_NAMES.keys()),
|
| 52 |
+
format_func=lambda x: f"{_BIDDER_LABELS.get(x, x)} — {_BIDDER_SUBLABELS.get(x, '')}",
|
| 53 |
)
|
| 54 |
|
| 55 |
+
if st.button("▶ Run Evaluation", type="primary"):
|
| 56 |
criteria = _get_criteria()
|
| 57 |
verdicts_dict: dict = {}
|
| 58 |
+
progress = st.progress(0, text="Starting…")
|
| 59 |
total = len(selected) * len(criteria)
|
| 60 |
done = 0
|
| 61 |
for bidder_id in selected:
|
|
|
|
| 63 |
f for f in (DATA_DIR / "bidders" / bidder_id).iterdir()
|
| 64 |
if f.suffix.lower() in {".pdf", ".png", ".jpg"}
|
| 65 |
)
|
| 66 |
+
with st.spinner(f"Processing {_BIDDER_LABELS.get(bidder_id, bidder_id)}…"):
|
| 67 |
bidder_processor.process_bidder(bidder_id, files)
|
| 68 |
verdicts_list = []
|
| 69 |
for c in criteria:
|
|
|
|
| 71 |
verdicts_list.append(v.model_dump())
|
| 72 |
done += 1
|
| 73 |
progress.progress(done / total,
|
| 74 |
+
text=f"Evaluated {c.id} · {_BIDDER_LABELS.get(bidder_id, bidder_id)}")
|
| 75 |
verdicts_dict[bidder_id] = verdicts_list
|
| 76 |
st.session_state["verdicts"] = verdicts_dict
|
| 77 |
progress.empty()
|
| 78 |
+
st.success("Evaluation complete. Results saved.")
|
| 79 |
st.rerun()
|
| 80 |
|
| 81 |
verdicts_data = st.session_state.get("verdicts", {})
|
| 82 |
criteria = _get_criteria()
|
| 83 |
crit_map = {c.id: c for c in criteria}
|
| 84 |
|
| 85 |
+
if not verdicts_data:
|
| 86 |
+
st.info("No results yet. Click **Run Evaluation** above, or load the demo from the Overview tab.")
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
if st.session_state.get("fallback_active"):
|
| 90 |
st.warning("⚠ Live API unavailable — showing pre-computed results.")
|
| 91 |
|
|
|
|
| 94 |
continue
|
| 95 |
verdicts = verdicts_data[bidder_id]
|
| 96 |
overall = _overall_verdict(verdicts, crit_map)
|
| 97 |
+
op = verdict_pill(overall)
|
| 98 |
+
friendly = _BIDDER_LABELS.get(bidder_id, bidder_id)
|
| 99 |
+
sublabel = _BIDDER_SUBLABELS.get(bidder_id, "")
|
|
|
|
|
|
|
| 100 |
passed = sum(1 for v in verdicts
|
| 101 |
if v["verdict"] == "eligible" and
|
| 102 |
crit_map.get(v["criterion_id"]) and
|
| 103 |
crit_map[v["criterion_id"]].mandatory)
|
| 104 |
+
total_mand = sum(1 for v in verdicts
|
| 105 |
+
if crit_map.get(v["criterion_id"]) and
|
| 106 |
+
crit_map[v["criterion_id"]].mandatory)
|
| 107 |
|
| 108 |
with st.container(border=True):
|
| 109 |
+
# Bidder header
|
| 110 |
st.markdown(
|
| 111 |
+
f"""<div style="display:flex;justify-content:space-between;
|
| 112 |
+
align-items:center;flex-wrap:wrap;gap:8px;
|
| 113 |
+
padding:4px 0 12px;">
|
| 114 |
+
<div>
|
| 115 |
+
<div style="font-size:1.05rem;font-weight:700;color:#0D1B2A;">{friendly}</div>
|
| 116 |
+
<div style="font-size:0.8rem;color:#64748B;margin-top:2px;">{sublabel}</div>
|
| 117 |
+
</div>
|
| 118 |
+
<div style="display:flex;align-items:center;gap:12px;">
|
| 119 |
+
{op}
|
| 120 |
+
<span style="font-size:0.78rem;color:#94A3B8;background:#F1F5F9;
|
| 121 |
+
padding:3px 10px;border-radius:20px;">
|
| 122 |
+
{passed}/{total_mand} mandatory passed
|
| 123 |
+
</span>
|
| 124 |
+
</div>
|
| 125 |
+
</div>""",
|
| 126 |
unsafe_allow_html=True,
|
| 127 |
)
|
| 128 |
|
| 129 |
# Column headers
|
| 130 |
+
hcols = st.columns([3, 2, 2, 3, 2])
|
| 131 |
+
for col, lbl in zip(hcols, ["Criterion", "Verdict", "Extracted Value",
|
| 132 |
+
"Source & OCR Tier", "Category"]):
|
| 133 |
+
col.markdown(
|
| 134 |
+
f'<div style="font-size:0.72rem;font-weight:700;color:#94A3B8;'
|
| 135 |
+
f'text-transform:uppercase;letter-spacing:0.06em;padding-bottom:4px;">'
|
| 136 |
+
f'{lbl}</div>',
|
| 137 |
+
unsafe_allow_html=True,
|
| 138 |
+
)
|
| 139 |
+
st.markdown('<hr style="margin:0 0 8px;border-color:#F1F5F9;">', unsafe_allow_html=True)
|
| 140 |
|
| 141 |
for v in verdicts:
|
| 142 |
crit = crit_map.get(v["criterion_id"])
|
| 143 |
crit_title = crit.title if crit else v["criterion_id"]
|
| 144 |
+
mb = mandatory_badge(crit.mandatory if crit else True)
|
| 145 |
cat = category_badge(crit.category if crit else "compliance")
|
| 146 |
|
| 147 |
+
cols = st.columns([3, 2, 2, 3, 2])
|
| 148 |
+
cols[0].markdown(
|
| 149 |
+
f'{mb} <span style="font-weight:600;font-size:0.88rem;">'
|
| 150 |
+
f'{v["criterion_id"]}</span>'
|
| 151 |
+
f'<div style="font-size:0.8rem;color:#374151;margin-top:2px;">{crit_title}</div>',
|
| 152 |
+
unsafe_allow_html=True,
|
| 153 |
+
)
|
| 154 |
+
cols[1].markdown(verdict_pill(v["verdict"]), unsafe_allow_html=True)
|
| 155 |
+
extracted = v.get("extracted_value") or ""
|
| 156 |
+
extracted_html = (
|
| 157 |
+
f'<span style="font-size:0.85rem;color:#374151;">{extracted}</span>'
|
| 158 |
+
if extracted else
|
| 159 |
+
'<span style="color:#9CA3AF;">—</span>'
|
| 160 |
+
)
|
| 161 |
+
cols[2].markdown(extracted_html, unsafe_allow_html=True)
|
| 162 |
if v.get("source"):
|
| 163 |
src = v["source"]
|
| 164 |
tier = ocr_tier_badge(src["source_type"])
|
| 165 |
+
cols[3].markdown(
|
| 166 |
+
f'<span style="font-size:0.82rem;font-family:monospace;'
|
| 167 |
+
f'background:#F8FAFC;padding:2px 6px;border-radius:4px;'
|
| 168 |
+
f'border:1px solid #E2E8F0;">{src["doc_name"]}</span>'
|
| 169 |
+
f' p{src["page"]}<br>{tier}',
|
| 170 |
+
unsafe_allow_html=True,
|
| 171 |
+
)
|
| 172 |
else:
|
| 173 |
+
cols[3].markdown('<span style="color:#9CA3AF;">—</span>', unsafe_allow_html=True)
|
| 174 |
+
cols[4].markdown(cat, unsafe_allow_html=True)
|
| 175 |
|
| 176 |
+
confidence_bar(v.get("combined_confidence", 0.0))
|
|
|
|
| 177 |
|
| 178 |
if v.get("reason") or (v.get("source") and v["source"].get("snippet")):
|
| 179 |
+
with st.expander("View details", expanded=False):
|
| 180 |
if v.get("reason"):
|
| 181 |
+
st.markdown(
|
| 182 |
+
f'<div style="background:#F8FAFC;border-left:3px solid #3B82F6;'
|
| 183 |
+
f'padding:10px 14px;border-radius:0 6px 6px 0;'
|
| 184 |
+
f'font-size:0.88rem;color:#374151;">'
|
| 185 |
+
f'<strong>Reason:</strong> {v["reason"]}</div>',
|
| 186 |
+
unsafe_allow_html=True,
|
| 187 |
+
)
|
| 188 |
if v.get("source") and v["source"].get("snippet"):
|
| 189 |
+
st.markdown(
|
| 190 |
+
f'<div style="background:#FFFBEB;border-left:3px solid #F59E0B;'
|
| 191 |
+
f'padding:10px 14px;border-radius:0 6px 6px 0;margin-top:8px;'
|
| 192 |
+
f'font-size:0.85rem;color:#374151;font-style:italic;">'
|
| 193 |
+
f'"{v["source"]["snippet"]}"</div>',
|
| 194 |
+
unsafe_allow_html=True,
|
| 195 |
+
)
|
| 196 |
+
st.markdown('<hr style="margin:6px 0;border-color:#F1F5F9;">', unsafe_allow_html=True)
|
|
@@ -262,13 +262,13 @@ def render() -> None:
|
|
| 262 |
try:
|
| 263 |
img = render_page_to_image(doc_path, page_no)
|
| 264 |
st.image(img, caption=f"{doc_name} — Page {page_no}",
|
| 265 |
-
|
| 266 |
except Exception:
|
| 267 |
st.caption("Page preview unavailable.")
|
| 268 |
elif doc_path.exists() and doc_path.suffix.lower() in {".png", ".jpg"}:
|
| 269 |
with st.expander(f"View source image ({doc_name})", expanded=False):
|
| 270 |
st.image(str(doc_path), caption=doc_name,
|
| 271 |
-
|
| 272 |
|
| 273 |
st.divider()
|
| 274 |
|
|
|
|
| 262 |
try:
|
| 263 |
img = render_page_to_image(doc_path, page_no)
|
| 264 |
st.image(img, caption=f"{doc_name} — Page {page_no}",
|
| 265 |
+
use_column_width=True)
|
| 266 |
except Exception:
|
| 267 |
st.caption("Page preview unavailable.")
|
| 268 |
elif doc_path.exists() and doc_path.suffix.lower() in {".png", ".jpg"}:
|
| 269 |
with st.expander(f"View source image ({doc_name})", expanded=False):
|
| 270 |
st.image(str(doc_path), caption=doc_name,
|
| 271 |
+
use_column_width=True)
|
| 272 |
|
| 273 |
st.divider()
|
| 274 |
|
|
@@ -6,14 +6,20 @@ from core.fallback import load_criteria
|
|
| 6 |
|
| 7 |
|
| 8 |
def render() -> None:
|
| 9 |
-
|
| 10 |
st.markdown(
|
| 11 |
-
"
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
)
|
| 14 |
-
st.divider()
|
| 15 |
|
| 16 |
-
# KPI
|
| 17 |
criteria_count = len(st.session_state.get("criteria", load_criteria()))
|
| 18 |
verdicts = st.session_state.get("verdicts", {})
|
| 19 |
bidders_evaluated = len(verdicts)
|
|
@@ -24,120 +30,87 @@ def render() -> None:
|
|
| 24 |
audit_entries = len(audit.query())
|
| 25 |
|
| 26 |
c1, c2, c3, c4 = st.columns(4)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
st.divider()
|
| 33 |
|
| 34 |
-
# Architecture
|
| 35 |
-
st.
|
| 36 |
-
st.markdown("""
|
| 37 |
-
```
|
| 38 |
-
┌─────────────────────────────────────────────────────────────────────┐
|
| 39 |
-
│ TenderIQ Pipeline │
|
| 40 |
-
└─────────────────────────────────────────────────────────────────────┘
|
| 41 |
-
|
| 42 |
-
📄 Tender PDF 📁 Bidder Documents
|
| 43 |
-
│ (PDFs, scans, photos)
|
| 44 |
-
│ │
|
| 45 |
-
▼ ▼
|
| 46 |
-
┌───────────┐ ┌────────────────────────┐
|
| 47 |
-
│ DeepSeek │ │ 3-Tier OCR Pipeline │
|
| 48 |
-
│ LLM │ │ ① PyMuPDF (typed) │
|
| 49 |
-
│ (Stage 1) │ │ ② Tesseract (scans) │
|
| 50 |
-
└───────────┘ │ ③ Vision LLM (poor) │
|
| 51 |
-
│ └────────────────────────┘
|
| 52 |
-
│ │
|
| 53 |
-
▼ ▼
|
| 54 |
-
┌───────────┐ ┌────────────────────────┐
|
| 55 |
-
│ Criteria │ │ ChromaDB Vector │
|
| 56 |
-
│ C1 – C5 │ │ Index (per bidder) │
|
| 57 |
-
│ (JSON) │ └────────────────────────┘
|
| 58 |
-
└───────────┘ │
|
| 59 |
-
│ │ semantic search
|
| 60 |
-
└──────────────────┬───────────────────┘
|
| 61 |
-
│
|
| 62 |
-
▼
|
| 63 |
-
┌─────────────────────┐
|
| 64 |
-
│ DeepSeek LLM │
|
| 65 |
-
│ (Stage 3 eval) │
|
| 66 |
-
│ │
|
| 67 |
-
│ evidence → verdict │
|
| 68 |
-
│ + confidence score │
|
| 69 |
-
└─────────────────────┘
|
| 70 |
-
│
|
| 71 |
-
┌─────────────┴──────────────┐
|
| 72 |
-
│ │
|
| 73 |
-
▼ ▼
|
| 74 |
-
confidence ≥ 0.80 confidence < 0.80
|
| 75 |
-
verdict kept downgraded to
|
| 76 |
-
needs_review
|
| 77 |
-
│
|
| 78 |
-
▼
|
| 79 |
-
┌─────────────────┐
|
| 80 |
-
│ Human Review │
|
| 81 |
-
│ Queue (Tab 4) │
|
| 82 |
-
└─────────────────┘
|
| 83 |
-
│
|
| 84 |
-
▼
|
| 85 |
-
┌─────────────────┐
|
| 86 |
-
│ Audit Log │
|
| 87 |
-
│ (every action) │
|
| 88 |
-
└─────────────────┘
|
| 89 |
-
```
|
| 90 |
-
""")
|
| 91 |
-
|
| 92 |
-
st.divider()
|
| 93 |
|
| 94 |
-
st.subheader("Pipeline Stages")
|
| 95 |
col_a, col_b = st.columns(2)
|
| 96 |
with col_a:
|
| 97 |
st.markdown("""
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
with col_b:
|
| 111 |
st.markdown("""
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
""
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
st.divider()
|
| 125 |
|
| 126 |
-
|
|
|
|
|
|
|
| 127 |
col1, col2 = st.columns(2)
|
| 128 |
with col1:
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
st.
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
| 142 |
with col2:
|
| 143 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
def render() -> None:
|
| 9 |
+
# Hero banner
|
| 10 |
st.markdown(
|
| 11 |
+
"""<div class="tiq-hero">
|
| 12 |
+
<h1>⚖️ TenderIQ</h1>
|
| 13 |
+
<p>Explainable AI for Government Tender Evaluation ·
|
| 14 |
+
CRPF Hackathon Theme 3</p>
|
| 15 |
+
<p style="font-size:0.88rem;margin-top:8px;color:#94A3B8;">
|
| 16 |
+
Automated eligibility evaluation with criterion-level explainability,
|
| 17 |
+
three-tier OCR for scanned documents, and a complete audit trail.</p>
|
| 18 |
+
</div>""",
|
| 19 |
+
unsafe_allow_html=True,
|
| 20 |
)
|
|
|
|
| 21 |
|
| 22 |
+
# KPI strip
|
| 23 |
criteria_count = len(st.session_state.get("criteria", load_criteria()))
|
| 24 |
verdicts = st.session_state.get("verdicts", {})
|
| 25 |
bidders_evaluated = len(verdicts)
|
|
|
|
| 30 |
audit_entries = len(audit.query())
|
| 31 |
|
| 32 |
c1, c2, c3, c4 = st.columns(4)
|
| 33 |
+
for col, val, lbl in [
|
| 34 |
+
(c1, criteria_count, "Criteria Extracted"),
|
| 35 |
+
(c2, bidders_evaluated, "Bidders Evaluated"),
|
| 36 |
+
(c3, mandatory_checked, "Criteria Checked"),
|
| 37 |
+
(c4, audit_entries, "Audit Entries"),
|
| 38 |
+
]:
|
| 39 |
+
col.markdown(
|
| 40 |
+
f'<div class="tiq-kpi">'
|
| 41 |
+
f'<div class="tiq-kpi-val">{val}</div>'
|
| 42 |
+
f'<div class="tiq-kpi-lbl">{lbl}</div>'
|
| 43 |
+
f'</div>',
|
| 44 |
+
unsafe_allow_html=True,
|
| 45 |
+
)
|
| 46 |
|
| 47 |
st.divider()
|
| 48 |
|
| 49 |
+
# Architecture
|
| 50 |
+
st.markdown('<div class="tiq-section-header"><div style="font-size:1.1rem;font-weight:700;color:#0D1B2A;">Pipeline Architecture</div></div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
|
|
|
| 52 |
col_a, col_b = st.columns(2)
|
| 53 |
with col_a:
|
| 54 |
st.markdown("""
|
| 55 |
+
<div style="background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:20px;">
|
| 56 |
+
<div style="font-weight:700;color:#1E3A5F;margin-bottom:12px;">📥 Ingestion</div>
|
| 57 |
+
|
| 58 |
+
<div style="display:flex;align-items:flex-start;gap:10px;margin-bottom:10px;">
|
| 59 |
+
<div style="background:#DBEAFE;color:#1E40AF;border-radius:6px;padding:3px 8px;font-size:0.75rem;font-weight:700;flex-shrink:0;">1</div>
|
| 60 |
+
<div><strong>Extract Criteria</strong><br><span style="font-size:0.82rem;color:#64748B;">DeepSeek LLM reads the full tender PDF and returns structured JSON — category, mandatory flag, rule, source clause, query hints.</span></div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div style="display:flex;align-items:flex-start;gap:10px;">
|
| 64 |
+
<div style="background:#DBEAFE;color:#1E40AF;border-radius:6px;padding:3px 8px;font-size:0.75rem;font-weight:700;flex-shrink:0;">2</div>
|
| 65 |
+
<div><strong>Three-Tier OCR</strong><br><span style="font-size:0.82rem;color:#64748B;">
|
| 66 |
+
📄 PyMuPDF → 🔍 Tesseract → 👁 Vision LLM.<br>
|
| 67 |
+
Each page records its tier and confidence score.
|
| 68 |
+
Chunks indexed into ChromaDB with full provenance.</span></div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
""", unsafe_allow_html=True)
|
| 72 |
+
|
| 73 |
with col_b:
|
| 74 |
st.markdown("""
|
| 75 |
+
<div style="background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:20px;">
|
| 76 |
+
<div style="font-weight:700;color:#1E3A5F;margin-bottom:12px;">⚖️ Evaluation & Oversight</div>
|
| 77 |
+
|
| 78 |
+
<div style="display:flex;align-items:flex-start;gap:10px;margin-bottom:10px;">
|
| 79 |
+
<div style="background:#DCFCE7;color:#166534;border-radius:6px;padding:3px 8px;font-size:0.75rem;font-weight:700;flex-shrink:0;">3</div>
|
| 80 |
+
<div><strong>Evaluate per Criterion</strong><br><span style="font-size:0.82rem;color:#64748B;">Semantic search retrieves top-k evidence chunks. DeepSeek returns verdict + confidence. Safety rule: borderline "not eligible" is downgraded to "needs review" — never silent disqualification.</span></div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div style="display:flex;align-items:flex-start;gap:10px;">
|
| 84 |
+
<div style="background:#FEF3C7;color:#92400E;border-radius:6px;padding:3px 8px;font-size:0.75rem;font-weight:700;flex-shrink:0;">4</div>
|
| 85 |
+
<div><strong>Human Review & Audit</strong><br><span style="font-size:0.82rem;color:#64748B;">Flagged verdicts surface with full evidence. Every action — extraction, OCR, evaluation, review — is logged to SQLite with timestamp, model version, and payload.</span></div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
""", unsafe_allow_html=True)
|
| 89 |
|
| 90 |
st.divider()
|
| 91 |
|
| 92 |
+
# Quick start
|
| 93 |
+
st.markdown('<div class="tiq-section-header"><div style="font-size:1.1rem;font-weight:700;color:#0D1B2A;">Quick Start</div></div>', unsafe_allow_html=True)
|
| 94 |
+
|
| 95 |
col1, col2 = st.columns(2)
|
| 96 |
with col1:
|
| 97 |
+
with st.container(border=True):
|
| 98 |
+
st.markdown("**🚀 Pre-computed Demo**")
|
| 99 |
+
st.caption("Instantly load realistic results for all 3 bidders — no API key needed.")
|
| 100 |
+
if st.button("Load Pre-computed Demo", type="primary", use_container_width=True):
|
| 101 |
+
from core.fallback import load_criteria as lc, load_evaluation
|
| 102 |
+
criteria = lc()
|
| 103 |
+
st.session_state["criteria"] = [c.model_dump() for c in criteria]
|
| 104 |
+
verdicts_dict: dict = {}
|
| 105 |
+
for bidder_id in BIDDER_NAMES:
|
| 106 |
+
verdicts_dict[bidder_id] = [
|
| 107 |
+
load_evaluation(bidder_id, c.id).model_dump() for c in criteria
|
| 108 |
+
]
|
| 109 |
+
st.session_state["verdicts"] = verdicts_dict
|
| 110 |
+
st.success("Loaded. Navigate to Bidder Evaluation or Interpretability.")
|
| 111 |
+
st.rerun()
|
| 112 |
with col2:
|
| 113 |
+
with st.container(border=True):
|
| 114 |
+
st.markdown("**⚡ Live Pipeline**")
|
| 115 |
+
st.caption("Upload a tender PDF, run extraction and evaluation against the DeepSeek API.")
|
| 116 |
+
st.info("Set `DEEPSEEK_API_KEY` in `.env`, then use the Tender Analysis tab.")
|
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
|
| 3 |
from core import audit
|
|
|
|
| 4 |
from core.fallback import load_criteria
|
| 5 |
from core.schemas import Criterion
|
| 6 |
from ui.components import confidence_bar, verdict_pill
|
|
@@ -46,15 +47,39 @@ def render() -> None:
|
|
| 46 |
|
| 47 |
with st.container(border=True):
|
| 48 |
col1, col2 = st.columns([3, 1])
|
|
|
|
| 49 |
with col1:
|
| 50 |
-
st.markdown(
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
if v.get("extracted_value"):
|
| 53 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
if v.get("reason"):
|
| 55 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
if v.get("source") and v["source"].get("snippet"):
|
| 57 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
with col2:
|
| 59 |
conf = v.get("combined_confidence", 0.0)
|
| 60 |
confidence_bar(conf, "Certainty in assessment")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
|
| 3 |
from core import audit
|
| 4 |
+
from core.config import BIDDER_NAMES
|
| 5 |
from core.fallback import load_criteria
|
| 6 |
from core.schemas import Criterion
|
| 7 |
from ui.components import confidence_bar, verdict_pill
|
|
|
|
| 47 |
|
| 48 |
with st.container(border=True):
|
| 49 |
col1, col2 = st.columns([3, 1])
|
| 50 |
+
friendly = BIDDER_NAMES.get(bidder_id, bidder_id)
|
| 51 |
with col1:
|
| 52 |
+
st.markdown(
|
| 53 |
+
f'<div style="font-weight:700;font-size:1rem;color:#0D1B2A;">'
|
| 54 |
+
f'{friendly}</div>'
|
| 55 |
+
f'<div style="font-size:0.85rem;color:#64748B;margin-top:2px;">'
|
| 56 |
+
f'{v["criterion_id"]}: {crit_title}</div>',
|
| 57 |
+
unsafe_allow_html=True,
|
| 58 |
+
)
|
| 59 |
+
st.markdown(verdict_pill(v["verdict"]), unsafe_allow_html=True)
|
| 60 |
if v.get("extracted_value"):
|
| 61 |
+
st.markdown(
|
| 62 |
+
f'<div style="margin-top:6px;font-size:0.85rem;">'
|
| 63 |
+
f'<strong>Extracted value:</strong> '
|
| 64 |
+
f'<code>{v["extracted_value"]}</code></div>',
|
| 65 |
+
unsafe_allow_html=True,
|
| 66 |
+
)
|
| 67 |
if v.get("reason"):
|
| 68 |
+
st.markdown(
|
| 69 |
+
f'<div style="background:#FFFBEB;border-left:3px solid #F59E0B;'
|
| 70 |
+
f'padding:8px 12px;border-radius:0 6px 6px 0;margin-top:8px;'
|
| 71 |
+
f'font-size:0.85rem;color:#374151;">'
|
| 72 |
+
f'<strong>Reason:</strong> {v["reason"]}</div>',
|
| 73 |
+
unsafe_allow_html=True,
|
| 74 |
+
)
|
| 75 |
if v.get("source") and v["source"].get("snippet"):
|
| 76 |
+
st.markdown(
|
| 77 |
+
f'<div style="background:#F8FAFC;border:1px solid #E2E8F0;'
|
| 78 |
+
f'padding:8px 12px;border-radius:6px;margin-top:6px;'
|
| 79 |
+
f'font-size:0.82rem;color:#374151;font-style:italic;">'
|
| 80 |
+
f'"{v["source"]["snippet"]}"</div>',
|
| 81 |
+
unsafe_allow_html=True,
|
| 82 |
+
)
|
| 83 |
with col2:
|
| 84 |
conf = v.get("combined_confidence", 0.0)
|
| 85 |
confidence_bar(conf, "Certainty in assessment")
|