Spaces:
No application file
No application file
Upload 6 files
Browse files- HF_politico_AI.code-workspace +8 -0
- app.py +187 -0
- dockerfile +22 -0
- requirements.txt +8 -0
- static/mnt_politicoai_gui.html +1591 -0
- static/politicoai_gui.html +0 -0
HF_politico_AI.code-workspace
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"folders": [
|
| 3 |
+
{
|
| 4 |
+
"path": "."
|
| 5 |
+
}
|
| 6 |
+
],
|
| 7 |
+
"settings": {}
|
| 8 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PoliticoAI – Hugging Face Space edition
|
| 3 |
+
Light-weight API that mirrors the full contract from the Production Guide.
|
| 4 |
+
Data: small synthetic sample so the demo works instantly.
|
| 5 |
+
"""
|
| 6 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File
|
| 7 |
+
from fastapi.staticfiles import StaticFiles
|
| 8 |
+
from fastapi.responses import JSONResponse
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
from typing import List, Dict, Any
|
| 13 |
+
|
| 14 |
+
# ------------------------------------------------------------------
|
| 15 |
+
# 1. tiny synthetic dataset (mirrors the schema in the Guide)
|
| 16 |
+
# ------------------------------------------------------------------
|
| 17 |
+
PRECINCT_STORE: List[Dict[str, Any]] = [
|
| 18 |
+
{
|
| 19 |
+
"id": "P01",
|
| 20 |
+
"name": "Urban Ward 1",
|
| 21 |
+
"county": "Demo County",
|
| 22 |
+
"classification": "Urban",
|
| 23 |
+
"registered": 3500,
|
| 24 |
+
"latest_d_pct": 63.0,
|
| 25 |
+
"base_turnout": 0.70,
|
| 26 |
+
"results": {
|
| 27 |
+
2020: {"d_pct": 65, "turnout": 72},
|
| 28 |
+
2022: {"d_pct": 60, "turnout": 50},
|
| 29 |
+
2024: {"d_pct": 63, "turnout": 70},
|
| 30 |
+
},
|
| 31 |
+
"demographics": {
|
| 32 |
+
"pct_college": 68,
|
| 33 |
+
"pct_under_35": 40,
|
| 34 |
+
"pct_35_64": 45,
|
| 35 |
+
"pct_65plus": 15,
|
| 36 |
+
"median_income": 48000,
|
| 37 |
+
"pct_white": 55,
|
| 38 |
+
"pct_black": 20,
|
| 39 |
+
"pct_hispanic": 20,
|
| 40 |
+
"pct_asian": 5,
|
| 41 |
+
},
|
| 42 |
+
"centroid": [-75.47, 40.61],
|
| 43 |
+
}
|
| 44 |
+
for i in range(1, 11) # 10 precincts
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
for p in PRECINCT_STORE:
|
| 48 |
+
p["id"] = f"P{i:02d}"
|
| 49 |
+
p["name"] = f"Precinct {i:02d}"
|
| 50 |
+
p["latest_d_pct"] = 45 + np.random.normal(0, 5) # competitive
|
| 51 |
+
p["base_turnout"] = 0.65 + np.random.normal(0, 0.05)
|
| 52 |
+
|
| 53 |
+
# ------------------------------------------------------------------
|
| 54 |
+
# 2. FastAPI boiler-plate
|
| 55 |
+
# ------------------------------------------------------------------
|
| 56 |
+
app = FastAPI(title="PoliticoAI API", version="1.0")
|
| 57 |
+
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
| 58 |
+
|
| 59 |
+
# ------------------------------------------------------------------
|
| 60 |
+
# 3. Models (match the Production Guide contract)
|
| 61 |
+
# ------------------------------------------------------------------
|
| 62 |
+
class SimulationParams(BaseModel):
|
| 63 |
+
iterations: int = 10000
|
| 64 |
+
environment: str = "neutral"
|
| 65 |
+
polling_error: float = 3.5
|
| 66 |
+
turnout_variability: float = 5.0
|
| 67 |
+
|
| 68 |
+
class SegmentParams(BaseModel):
|
| 69 |
+
primary: str = "age"
|
| 70 |
+
secondary: str = "none"
|
| 71 |
+
metric: str = "size"
|
| 72 |
+
|
| 73 |
+
# ------------------------------------------------------------------
|
| 74 |
+
# 4. API endpoints (exact contract from Guide appendix)
|
| 75 |
+
# ------------------------------------------------------------------
|
| 76 |
+
@app.get("/api/health")
|
| 77 |
+
def health():
|
| 78 |
+
return {"status": "ok"}
|
| 79 |
+
|
| 80 |
+
@app.get("/api/precincts")
|
| 81 |
+
def get_precincts():
|
| 82 |
+
# strip heavy geometry if present, send only numbers
|
| 83 |
+
return [
|
| 84 |
+
{
|
| 85 |
+
"id": p["id"],
|
| 86 |
+
"name": p["name"],
|
| 87 |
+
"classification": p["classification"],
|
| 88 |
+
"registered_voters": p["registered"],
|
| 89 |
+
"latest_d_pct": p["latest_d_pct"],
|
| 90 |
+
"base_turnout": p["base_turnout"],
|
| 91 |
+
"demographics": p["demographics"],
|
| 92 |
+
}
|
| 93 |
+
for p in PRECINCT_STORE
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
@app.post("/api/simulation/montecarlo")
|
| 97 |
+
def run_monte_carlo(params: SimulationParams):
|
| 98 |
+
env_map = {"neutral": 0, "d3": 3, "r3": -3, "d5": 5, "r5": -5}
|
| 99 |
+
env_shift = env_map.get(params.environment, 0)
|
| 100 |
+
margins = []
|
| 101 |
+
for _ in range(params.iterations):
|
| 102 |
+
env_noise = np.random.normal(env_shift, params.polling_error)
|
| 103 |
+
total_d, total_r = 0, 0
|
| 104 |
+
for pct in PRECINCT_STORE:
|
| 105 |
+
base_d = pct["latest_d_pct"] / 100
|
| 106 |
+
adj_d = np.clip(base_d + env_noise / 100, 0.05, 0.95)
|
| 107 |
+
turnout = np.clip(
|
| 108 |
+
np.random.normal(pct["base_turnout"], params.turnout_variability / 100),
|
| 109 |
+
0.2, 0.95,
|
| 110 |
+
)
|
| 111 |
+
votes = int(pct["registered"] * turnout)
|
| 112 |
+
total_d += int(votes * adj_d)
|
| 113 |
+
total_r += votes - int(votes * adj_d)
|
| 114 |
+
margins.append((total_d - total_r) / (total_d + total_r) * 100)
|
| 115 |
+
margins = np.array(margins)
|
| 116 |
+
win_prob = float((margins > 0).mean() * 100)
|
| 117 |
+
return {
|
| 118 |
+
"win_prob": win_prob,
|
| 119 |
+
"expected_margin": float(margins.mean()),
|
| 120 |
+
"recount_prob": float((np.abs(margins) < 0.5).mean() * 100),
|
| 121 |
+
"margins": margins.tolist(),
|
| 122 |
+
"ci_95": [float(np.percentile(margins, 2.5)), float(np.percentile(margins, 97.5))],
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
@app.post("/api/demographics/segment")
|
| 126 |
+
def segment_demographics(params: SegmentParams):
|
| 127 |
+
# lightweight demo segmentation
|
| 128 |
+
segments = ["Under 35", "35-64", "65+"]
|
| 129 |
+
treemap = {
|
| 130 |
+
"labels": ["District"] + segments,
|
| 131 |
+
"parents": [""] + ["District"] * len(segments),
|
| 132 |
+
"values": [3500, 4000, 2500],
|
| 133 |
+
}
|
| 134 |
+
matrix = [
|
| 135 |
+
{"segment": f"Age: {seg}", "size": 3500 if seg == "Under 35" else 4000 if seg == "35-64" else 2500,
|
| 136 |
+
"turnout": "69%", "lean": "D+2", "persuadability": "High", "strategy": "Persuade"}
|
| 137 |
+
for seg in segments
|
| 138 |
+
]
|
| 139 |
+
return {"treemap": treemap, "targeting_matrix": matrix}
|
| 140 |
+
|
| 141 |
+
@app.post("/api/precincts/analyze")
|
| 142 |
+
def analyze_precincts():
|
| 143 |
+
# minimal clustering demo
|
| 144 |
+
metrics = []
|
| 145 |
+
for p in PRECINCT_STORE:
|
| 146 |
+
pvi = p["latest_d_pct"] - 50
|
| 147 |
+
swing = np.random.uniform(1, 8)
|
| 148 |
+
to_delta = np.random.uniform(15, 30)
|
| 149 |
+
net = (swing + to_delta) * p["registered"] / 1000
|
| 150 |
+
metrics.append(
|
| 151 |
+
{
|
| 152 |
+
"id": p["id"],
|
| 153 |
+
"name": p["name"],
|
| 154 |
+
"pvi": round(pvi, 1),
|
| 155 |
+
"swingScore": round(swing, 1),
|
| 156 |
+
"turnoutDelta": round(to_delta, 1),
|
| 157 |
+
"net": round(net),
|
| 158 |
+
"tier": "High Swing" if swing > 4.5 else "Blue Base" if pvi > 0 else "Red Lean",
|
| 159 |
+
}
|
| 160 |
+
)
|
| 161 |
+
metrics = sorted(metrics, key=lambda x: x["net"], reverse=True)
|
| 162 |
+
return {"scatter_data": metrics, "ranking": metrics[:15], "table": metrics[:20]}
|
| 163 |
+
|
| 164 |
+
@app.post("/api/upload/results")
|
| 165 |
+
async def upload_results(file: UploadFile = File(...)):
|
| 166 |
+
# accept any CSV, store in memory (or persist to disk if you like)
|
| 167 |
+
df = pd.read_csv(file.file)
|
| 168 |
+
# here you would validate, clean, merge into PRECINCT_STORE, etc.
|
| 169 |
+
return {"rows": len(df), "columns": list(df.columns)}
|
| 170 |
+
|
| 171 |
+
# ------------------------------------------------------------------
|
| 172 |
+
# 5. CORS — Spaces needs this for browser access
|
| 173 |
+
# ------------------------------------------------------------------
|
| 174 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 175 |
+
app.add_middleware(
|
| 176 |
+
CORSMiddleware,
|
| 177 |
+
allow_origins=["*"], # restrict in prod
|
| 178 |
+
allow_methods=["*"],
|
| 179 |
+
allow_headers=["*"],
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# ------------------------------------------------------------------
|
| 183 |
+
# 6. Entry-point guard (lets uvicorn import without running)
|
| 184 |
+
# ------------------------------------------------------------------
|
| 185 |
+
if __name__ == "__main__":
|
| 186 |
+
import uvicorn
|
| 187 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
# system deps for geopandas / spatial libs (optional but recommended)
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
libgdal-dev libspatialindex-dev gcc g++ \
|
| 6 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# back-end code
|
| 14 |
+
COPY app.py .
|
| 15 |
+
|
| 16 |
+
# front-end GUI
|
| 17 |
+
COPY static ./static
|
| 18 |
+
|
| 19 |
+
# expose port that Spaces expects
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.0
|
| 3 |
+
pandas==2.2.0
|
| 4 |
+
numpy==2.0.0
|
| 5 |
+
scipy==1.14.0
|
| 6 |
+
scikit-learn==1.5.0
|
| 7 |
+
plotly==5.22.0
|
| 8 |
+
python-multipart==0.0.9
|
static/mnt_politicoai_gui.html
ADDED
|
@@ -0,0 +1,1591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>PoliticoAI - Campaign Strategy Platform</title>
|
| 7 |
+
|
| 8 |
+
<!-- Bootstrap CSS -->
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
+
<!-- Bootstrap Icons -->
|
| 11 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
| 12 |
+
<!-- Plotly -->
|
| 13 |
+
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
| 14 |
+
|
| 15 |
+
<style>
|
| 16 |
+
:root {
|
| 17 |
+
--primary-blue: #1e40af;
|
| 18 |
+
--primary-red: #dc2626;
|
| 19 |
+
--sidebar-width: 260px;
|
| 20 |
+
--header-height: 60px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 25 |
+
background: #f8fafc;
|
| 26 |
+
overflow-x: hidden;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Header */
|
| 30 |
+
.app-header {
|
| 31 |
+
position: fixed;
|
| 32 |
+
top: 0;
|
| 33 |
+
left: 0;
|
| 34 |
+
right: 0;
|
| 35 |
+
height: var(--header-height);
|
| 36 |
+
background: linear-gradient(135deg, var(--primary-blue) 0%, #3b82f6 100%);
|
| 37 |
+
color: white;
|
| 38 |
+
display: flex;
|
| 39 |
+
align-items: center;
|
| 40 |
+
padding: 0 24px;
|
| 41 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 42 |
+
z-index: 1000;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.app-header h1 {
|
| 46 |
+
font-size: 24px;
|
| 47 |
+
font-weight: 700;
|
| 48 |
+
margin: 0;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.app-header .badge {
|
| 52 |
+
margin-left: 12px;
|
| 53 |
+
font-size: 11px;
|
| 54 |
+
font-weight: 600;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Sidebar */
|
| 58 |
+
.sidebar {
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: var(--header-height);
|
| 61 |
+
left: 0;
|
| 62 |
+
width: var(--sidebar-width);
|
| 63 |
+
height: calc(100vh - var(--header-height));
|
| 64 |
+
background: white;
|
| 65 |
+
border-right: 1px solid #e2e8f0;
|
| 66 |
+
overflow-y: auto;
|
| 67 |
+
z-index: 999;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.nav-section {
|
| 71 |
+
padding: 24px 0;
|
| 72 |
+
border-bottom: 1px solid #e2e8f0;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.nav-section-title {
|
| 76 |
+
padding: 0 20px 8px;
|
| 77 |
+
font-size: 11px;
|
| 78 |
+
font-weight: 700;
|
| 79 |
+
text-transform: uppercase;
|
| 80 |
+
color: #64748b;
|
| 81 |
+
letter-spacing: 0.5px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.nav-item {
|
| 85 |
+
padding: 10px 20px;
|
| 86 |
+
cursor: pointer;
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
color: #475569;
|
| 90 |
+
transition: all 0.2s;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.nav-item:hover {
|
| 94 |
+
background: #f1f5f9;
|
| 95 |
+
color: var(--primary-blue);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.nav-item.active {
|
| 99 |
+
background: #eff6ff;
|
| 100 |
+
color: var(--primary-blue);
|
| 101 |
+
border-left: 3px solid var(--primary-blue);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.nav-item i {
|
| 105 |
+
width: 20px;
|
| 106 |
+
margin-right: 12px;
|
| 107 |
+
font-size: 18px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Main Content */
|
| 111 |
+
.main-content {
|
| 112 |
+
margin-left: var(--sidebar-width);
|
| 113 |
+
margin-top: var(--header-height);
|
| 114 |
+
padding: 32px;
|
| 115 |
+
min-height: calc(100vh - var(--header-height));
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.content-section {
|
| 119 |
+
display: none;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.content-section.active {
|
| 123 |
+
display: block;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Cards */
|
| 127 |
+
.card {
|
| 128 |
+
border: none;
|
| 129 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 130 |
+
border-radius: 8px;
|
| 131 |
+
margin-bottom: 24px;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.card-header {
|
| 135 |
+
background: white;
|
| 136 |
+
border-bottom: 1px solid #e2e8f0;
|
| 137 |
+
padding: 20px 24px;
|
| 138 |
+
font-weight: 600;
|
| 139 |
+
color: #1e293b;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.card-body {
|
| 143 |
+
padding: 24px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* Metrics Grid */
|
| 147 |
+
.metrics-grid {
|
| 148 |
+
display: grid;
|
| 149 |
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
| 150 |
+
gap: 20px;
|
| 151 |
+
margin-bottom: 32px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.metric-card {
|
| 155 |
+
background: white;
|
| 156 |
+
padding: 24px;
|
| 157 |
+
border-radius: 8px;
|
| 158 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 159 |
+
border-left: 4px solid;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.metric-card.blue {
|
| 163 |
+
border-left-color: var(--primary-blue);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.metric-card.red {
|
| 167 |
+
border-left-color: var(--primary-red);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.metric-card.purple {
|
| 171 |
+
border-left-color: #9333ea;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.metric-card.green {
|
| 175 |
+
border-left-color: #16a34a;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.metric-label {
|
| 179 |
+
font-size: 13px;
|
| 180 |
+
font-weight: 600;
|
| 181 |
+
color: #64748b;
|
| 182 |
+
text-transform: uppercase;
|
| 183 |
+
letter-spacing: 0.5px;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.metric-value {
|
| 187 |
+
font-size: 32px;
|
| 188 |
+
font-weight: 700;
|
| 189 |
+
color: #1e293b;
|
| 190 |
+
margin-top: 8px;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.metric-change {
|
| 194 |
+
font-size: 13px;
|
| 195 |
+
margin-top: 8px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.metric-change.positive {
|
| 199 |
+
color: #16a34a;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.metric-change.negative {
|
| 203 |
+
color: #dc2626;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/* Forms */
|
| 207 |
+
.form-label {
|
| 208 |
+
font-weight: 600;
|
| 209 |
+
font-size: 14px;
|
| 210 |
+
color: #334155;
|
| 211 |
+
margin-bottom: 8px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.form-control, .form-select {
|
| 215 |
+
border: 1px solid #cbd5e1;
|
| 216 |
+
border-radius: 6px;
|
| 217 |
+
padding: 10px 14px;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.form-control:focus, .form-select:focus {
|
| 221 |
+
border-color: var(--primary-blue);
|
| 222 |
+
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
/* Buttons */
|
| 226 |
+
.btn {
|
| 227 |
+
padding: 10px 20px;
|
| 228 |
+
border-radius: 6px;
|
| 229 |
+
font-weight: 600;
|
| 230 |
+
transition: all 0.2s;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.btn-primary {
|
| 234 |
+
background: var(--primary-blue);
|
| 235 |
+
border: none;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.btn-primary:hover {
|
| 239 |
+
background: #1e3a8a;
|
| 240 |
+
transform: translateY(-1px);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.btn-success {
|
| 244 |
+
background: #16a34a;
|
| 245 |
+
border: none;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.btn-danger {
|
| 249 |
+
background: var(--primary-red);
|
| 250 |
+
border: none;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Tables */
|
| 254 |
+
.table {
|
| 255 |
+
font-size: 14px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.table thead th {
|
| 259 |
+
background: #f8fafc;
|
| 260 |
+
color: #475569;
|
| 261 |
+
font-weight: 600;
|
| 262 |
+
border-bottom: 2px solid #e2e8f0;
|
| 263 |
+
text-transform: uppercase;
|
| 264 |
+
font-size: 12px;
|
| 265 |
+
letter-spacing: 0.5px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.table tbody tr:hover {
|
| 269 |
+
background: #f8fafc;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* Visualization Container */
|
| 273 |
+
.viz-container {
|
| 274 |
+
background: white;
|
| 275 |
+
border-radius: 8px;
|
| 276 |
+
padding: 24px;
|
| 277 |
+
margin-top: 24px;
|
| 278 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
/* Alerts */
|
| 282 |
+
.alert {
|
| 283 |
+
border: none;
|
| 284 |
+
border-radius: 6px;
|
| 285 |
+
border-left: 4px solid;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.alert-info {
|
| 289 |
+
border-left-color: #3b82f6;
|
| 290 |
+
background: #eff6ff;
|
| 291 |
+
color: #1e40af;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.alert-success {
|
| 295 |
+
border-left-color: #16a34a;
|
| 296 |
+
background: #f0fdf4;
|
| 297 |
+
color: #15803d;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.alert-warning {
|
| 301 |
+
border-left-color: #f59e0b;
|
| 302 |
+
background: #fffbeb;
|
| 303 |
+
color: #b45309;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* Loading Spinner */
|
| 307 |
+
.spinner {
|
| 308 |
+
display: none;
|
| 309 |
+
text-align: center;
|
| 310 |
+
padding: 40px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.spinner.active {
|
| 314 |
+
display: block;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* Range Slider Custom Style */
|
| 318 |
+
input[type="range"] {
|
| 319 |
+
width: 100%;
|
| 320 |
+
height: 6px;
|
| 321 |
+
border-radius: 3px;
|
| 322 |
+
background: #e2e8f0;
|
| 323 |
+
outline: none;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 327 |
+
-webkit-appearance: none;
|
| 328 |
+
appearance: none;
|
| 329 |
+
width: 20px;
|
| 330 |
+
height: 20px;
|
| 331 |
+
border-radius: 50%;
|
| 332 |
+
background: var(--primary-blue);
|
| 333 |
+
cursor: pointer;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
input[type="range"]::-moz-range-thumb {
|
| 337 |
+
width: 20px;
|
| 338 |
+
height: 20px;
|
| 339 |
+
border-radius: 50%;
|
| 340 |
+
background: var(--primary-blue);
|
| 341 |
+
cursor: pointer;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.slider-value {
|
| 345 |
+
display: inline-block;
|
| 346 |
+
min-width: 50px;
|
| 347 |
+
text-align: center;
|
| 348 |
+
font-weight: 600;
|
| 349 |
+
color: var(--primary-blue);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* File Upload */
|
| 353 |
+
.file-upload {
|
| 354 |
+
border: 2px dashed #cbd5e1;
|
| 355 |
+
border-radius: 8px;
|
| 356 |
+
padding: 40px;
|
| 357 |
+
text-align: center;
|
| 358 |
+
background: #f8fafc;
|
| 359 |
+
cursor: pointer;
|
| 360 |
+
transition: all 0.2s;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.file-upload:hover {
|
| 364 |
+
border-color: var(--primary-blue);
|
| 365 |
+
background: #eff6ff;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.file-upload i {
|
| 369 |
+
font-size: 48px;
|
| 370 |
+
color: #94a3b8;
|
| 371 |
+
margin-bottom: 16px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Progress Bar */
|
| 375 |
+
.progress {
|
| 376 |
+
height: 8px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
background: #e2e8f0;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.progress-bar {
|
| 382 |
+
background: var(--primary-blue);
|
| 383 |
+
border-radius: 4px;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
/* Tabs */
|
| 387 |
+
.nav-tabs {
|
| 388 |
+
border-bottom: 2px solid #e2e8f0;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.nav-tabs .nav-link {
|
| 392 |
+
border: none;
|
| 393 |
+
color: #64748b;
|
| 394 |
+
font-weight: 600;
|
| 395 |
+
padding: 12px 24px;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.nav-tabs .nav-link.active {
|
| 399 |
+
color: var(--primary-blue);
|
| 400 |
+
border-bottom: 2px solid var(--primary-blue);
|
| 401 |
+
margin-bottom: -2px;
|
| 402 |
+
}
|
| 403 |
+
</style>
|
| 404 |
+
</head>
|
| 405 |
+
<body>
|
| 406 |
+
<!-- Header -->
|
| 407 |
+
<div class="app-header">
|
| 408 |
+
<h1><i class="bi bi-graph-up-arrow"></i> PoliticoAI</h1>
|
| 409 |
+
<span class="badge bg-light text-primary">Campaign Strategy Platform</span>
|
| 410 |
+
</div>
|
| 411 |
+
|
| 412 |
+
<!-- Sidebar Navigation -->
|
| 413 |
+
<div class="sidebar">
|
| 414 |
+
<div class="nav-section">
|
| 415 |
+
<div class="nav-section-title">Analytics</div>
|
| 416 |
+
<div class="nav-item active" data-section="dashboard">
|
| 417 |
+
<i class="bi bi-speedometer2"></i>
|
| 418 |
+
<span>Dashboard</span>
|
| 419 |
+
</div>
|
| 420 |
+
<div class="nav-item" data-section="demographics">
|
| 421 |
+
<i class="bi bi-people"></i>
|
| 422 |
+
<span>Demographics</span>
|
| 423 |
+
</div>
|
| 424 |
+
<div class="nav-item" data-section="precincts">
|
| 425 |
+
<i class="bi bi-map"></i>
|
| 426 |
+
<span>Precinct Analysis</span>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
|
| 430 |
+
<div class="nav-section">
|
| 431 |
+
<div class="nav-section-title">Modeling</div>
|
| 432 |
+
<div class="nav-item" data-section="simulation">
|
| 433 |
+
<i class="bi bi-cpu"></i>
|
| 434 |
+
<span>Monte Carlo</span>
|
| 435 |
+
</div>
|
| 436 |
+
<div class="nav-item" data-section="scenarios">
|
| 437 |
+
<i class="bi bi-sliders"></i>
|
| 438 |
+
<span>Scenarios</span>
|
| 439 |
+
</div>
|
| 440 |
+
<div class="nav-item" data-section="forecasting">
|
| 441 |
+
<i class="bi bi-graph-up"></i>
|
| 442 |
+
<span>Forecasting</span>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
|
| 446 |
+
<div class="nav-section">
|
| 447 |
+
<div class="nav-section-title">Strategy</div>
|
| 448 |
+
<div class="nav-item" data-section="targeting">
|
| 449 |
+
<i class="bi bi-bullseye"></i>
|
| 450 |
+
<span>Voter Targeting</span>
|
| 451 |
+
</div>
|
| 452 |
+
<div class="nav-item" data-section="gotv">
|
| 453 |
+
<i class="bi bi-megaphone"></i>
|
| 454 |
+
<span>GOTV Planning</span>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="nav-item" data-section="messaging">
|
| 457 |
+
<i class="bi bi-chat-square-text"></i>
|
| 458 |
+
<span>Messaging</span>
|
| 459 |
+
</div>
|
| 460 |
+
</div>
|
| 461 |
+
|
| 462 |
+
<div class="nav-section">
|
| 463 |
+
<div class="nav-section-title">Data</div>
|
| 464 |
+
<div class="nav-item" data-section="upload">
|
| 465 |
+
<i class="bi bi-upload"></i>
|
| 466 |
+
<span>Data Upload</span>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="nav-item" data-section="export">
|
| 469 |
+
<i class="bi bi-download"></i>
|
| 470 |
+
<span>Export</span>
|
| 471 |
+
</div>
|
| 472 |
+
</div>
|
| 473 |
+
</div>
|
| 474 |
+
|
| 475 |
+
<!-- Main Content -->
|
| 476 |
+
<div class="main-content">
|
| 477 |
+
|
| 478 |
+
<!-- Dashboard Section -->
|
| 479 |
+
<div id="dashboard" class="content-section active">
|
| 480 |
+
<h2>Campaign Dashboard</h2>
|
| 481 |
+
<p class="text-muted mb-4">Real-time overview of district performance and strategic metrics</p>
|
| 482 |
+
|
| 483 |
+
<div class="metrics-grid">
|
| 484 |
+
<div class="metric-card blue">
|
| 485 |
+
<div class="metric-label">Current Win Probability</div>
|
| 486 |
+
<div class="metric-value" id="win-prob">--</div>
|
| 487 |
+
<div class="metric-change positive" id="win-prob-change">
|
| 488 |
+
<i class="bi bi-arrow-up"></i> +5.2% vs. baseline
|
| 489 |
+
</div>
|
| 490 |
+
</div>
|
| 491 |
+
|
| 492 |
+
<div class="metric-card purple">
|
| 493 |
+
<div class="metric-label">Expected Margin</div>
|
| 494 |
+
<div class="metric-value" id="expected-margin">--</div>
|
| 495 |
+
<div class="metric-change" id="margin-change">
|
| 496 |
+
±2.1% (95% CI)
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
|
| 500 |
+
<div class="metric-card green">
|
| 501 |
+
<div class="metric-label">High-Value Precincts</div>
|
| 502 |
+
<div class="metric-value" id="high-value-precincts">--</div>
|
| 503 |
+
<div class="metric-change">
|
| 504 |
+
<i class="bi bi-geo-alt"></i> Top quartile by net value
|
| 505 |
+
</div>
|
| 506 |
+
</div>
|
| 507 |
+
|
| 508 |
+
<div class="metric-card red">
|
| 509 |
+
<div class="metric-label">Voter Contact Target</div>
|
| 510 |
+
<div class="metric-value" id="contact-target">--</div>
|
| 511 |
+
<div class="metric-change">
|
| 512 |
+
<i class="bi bi-person-check"></i> Doors + phones
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
</div>
|
| 516 |
+
|
| 517 |
+
<div class="row">
|
| 518 |
+
<div class="col-lg-8">
|
| 519 |
+
<div class="card">
|
| 520 |
+
<div class="card-header">
|
| 521 |
+
<i class="bi bi-map-fill"></i> District Map - Partisan Lean (PVI)
|
| 522 |
+
</div>
|
| 523 |
+
<div class="card-body">
|
| 524 |
+
<div id="district-map" style="height: 400px;"></div>
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
</div>
|
| 528 |
+
|
| 529 |
+
<div class="col-lg-4">
|
| 530 |
+
<div class="card">
|
| 531 |
+
<div class="card-header">
|
| 532 |
+
<i class="bi bi-pie-chart-fill"></i> Demographic Breakdown
|
| 533 |
+
</div>
|
| 534 |
+
<div class="card-body">
|
| 535 |
+
<div id="demo-pie" style="height: 400px;"></div>
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
|
| 541 |
+
<div class="card">
|
| 542 |
+
<div class="card-header">
|
| 543 |
+
<i class="bi bi-graph-up"></i> Win Probability Over Time
|
| 544 |
+
</div>
|
| 545 |
+
<div class="card-body">
|
| 546 |
+
<div id="prob-timeline" style="height: 300px;"></div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
|
| 551 |
+
<!-- Demographics Section -->
|
| 552 |
+
<div id="demographics" class="content-section">
|
| 553 |
+
<h2>Demographic Segmentation</h2>
|
| 554 |
+
<p class="text-muted mb-4">Analyze voter universe by age, education, race, income, and geography</p>
|
| 555 |
+
|
| 556 |
+
<div class="card">
|
| 557 |
+
<div class="card-header">
|
| 558 |
+
<i class="bi bi-sliders"></i> Segmentation Controls
|
| 559 |
+
</div>
|
| 560 |
+
<div class="card-body">
|
| 561 |
+
<div class="row">
|
| 562 |
+
<div class="col-md-4">
|
| 563 |
+
<label class="form-label">Primary Dimension</label>
|
| 564 |
+
<select class="form-select" id="demo-primary">
|
| 565 |
+
<option value="age">Age Cohort</option>
|
| 566 |
+
<option value="education">Education Level</option>
|
| 567 |
+
<option value="race">Race/Ethnicity</option>
|
| 568 |
+
<option value="income">Income Quintile</option>
|
| 569 |
+
<option value="geography">Geography (Urban/Suburban/Rural)</option>
|
| 570 |
+
</select>
|
| 571 |
+
</div>
|
| 572 |
+
<div class="col-md-4">
|
| 573 |
+
<label class="form-label">Secondary Dimension</label>
|
| 574 |
+
<select class="form-select" id="demo-secondary">
|
| 575 |
+
<option value="none">None</option>
|
| 576 |
+
<option value="age">Age Cohort</option>
|
| 577 |
+
<option value="education">Education Level</option>
|
| 578 |
+
<option value="race">Race/Ethnicity</option>
|
| 579 |
+
<option value="income">Income Quintile</option>
|
| 580 |
+
</select>
|
| 581 |
+
</div>
|
| 582 |
+
<div class="col-md-4">
|
| 583 |
+
<label class="form-label">Metric to Display</label>
|
| 584 |
+
<select class="form-select" id="demo-metric">
|
| 585 |
+
<option value="size">Segment Size</option>
|
| 586 |
+
<option value="turnout">Turnout Rate</option>
|
| 587 |
+
<option value="lean">Partisan Lean</option>
|
| 588 |
+
<option value="persuadability">Persuadability</option>
|
| 589 |
+
</select>
|
| 590 |
+
</div>
|
| 591 |
+
</div>
|
| 592 |
+
<div class="mt-3">
|
| 593 |
+
<button class="btn btn-primary" onclick="generateDemoSegmentation()">
|
| 594 |
+
<i class="bi bi-play-fill"></i> Generate Segmentation
|
| 595 |
+
</button>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
</div>
|
| 599 |
+
|
| 600 |
+
<div class="viz-container">
|
| 601 |
+
<div id="demo-treemap" style="height: 500px;"></div>
|
| 602 |
+
</div>
|
| 603 |
+
|
| 604 |
+
<div class="card">
|
| 605 |
+
<div class="card-header">
|
| 606 |
+
<i class="bi bi-table"></i> Targeting Matrix
|
| 607 |
+
</div>
|
| 608 |
+
<div class="card-body">
|
| 609 |
+
<div class="table-responsive">
|
| 610 |
+
<table class="table table-hover" id="targeting-matrix">
|
| 611 |
+
<thead>
|
| 612 |
+
<tr>
|
| 613 |
+
<th>Segment</th>
|
| 614 |
+
<th>Size</th>
|
| 615 |
+
<th>Turnout Rate</th>
|
| 616 |
+
<th>Partisan Lean</th>
|
| 617 |
+
<th>Persuadability</th>
|
| 618 |
+
<th>Strategy</th>
|
| 619 |
+
</tr>
|
| 620 |
+
</thead>
|
| 621 |
+
<tbody>
|
| 622 |
+
<tr>
|
| 623 |
+
<td colspan="6" class="text-center text-muted">
|
| 624 |
+
Run segmentation to populate table
|
| 625 |
+
</td>
|
| 626 |
+
</tr>
|
| 627 |
+
</tbody>
|
| 628 |
+
</table>
|
| 629 |
+
</div>
|
| 630 |
+
</div>
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
|
| 634 |
+
<!-- Precinct Analysis Section -->
|
| 635 |
+
<div id="precincts" class="content-section">
|
| 636 |
+
<h2>Precinct-Level Analysis</h2>
|
| 637 |
+
<p class="text-muted mb-4">Deep dive into precinct metrics, clustering, and strategic prioritization</p>
|
| 638 |
+
|
| 639 |
+
<div class="alert alert-info">
|
| 640 |
+
<i class="bi bi-info-circle"></i>
|
| 641 |
+
<strong>Analysis Level:</strong> This module computes PVI, swing scores, elasticity, turnout metrics, and strategic value for every precinct.
|
| 642 |
+
</div>
|
| 643 |
+
|
| 644 |
+
<div class="card">
|
| 645 |
+
<div class="card-header">
|
| 646 |
+
<i class="bi bi-gear"></i> Analysis Parameters
|
| 647 |
+
</div>
|
| 648 |
+
<div class="card-body">
|
| 649 |
+
<div class="row">
|
| 650 |
+
<div class="col-md-4">
|
| 651 |
+
<label class="form-label">Clustering Method</label>
|
| 652 |
+
<select class="form-select" id="cluster-method">
|
| 653 |
+
<option value="kmeans">K-Means</option>
|
| 654 |
+
<option value="hierarchical">Hierarchical (Ward)</option>
|
| 655 |
+
</select>
|
| 656 |
+
</div>
|
| 657 |
+
<div class="col-md-4">
|
| 658 |
+
<label class="form-label">Number of Clusters</label>
|
| 659 |
+
<select class="form-select" id="num-clusters">
|
| 660 |
+
<option value="4">4 Tiers</option>
|
| 661 |
+
<option value="5">5 Tiers</option>
|
| 662 |
+
<option value="6">6 Tiers</option>
|
| 663 |
+
<option value="8">8 Tiers</option>
|
| 664 |
+
</select>
|
| 665 |
+
</div>
|
| 666 |
+
<div class="col-md-4">
|
| 667 |
+
<label class="form-label">Priority Metric</label>
|
| 668 |
+
<select class="form-select" id="priority-metric">
|
| 669 |
+
<option value="net">Net Mobilization Score</option>
|
| 670 |
+
<option value="persuasion">Persuasion Value</option>
|
| 671 |
+
<option value="mobilization">Mobilization Value</option>
|
| 672 |
+
<option value="competitiveness">Competitiveness</option>
|
| 673 |
+
</select>
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
<div class="mt-3">
|
| 677 |
+
<button class="btn btn-primary" onclick="analyzePrecincts()">
|
| 678 |
+
<i class="bi bi-play-fill"></i> Run Precinct Analysis
|
| 679 |
+
</button>
|
| 680 |
+
</div>
|
| 681 |
+
</div>
|
| 682 |
+
</div>
|
| 683 |
+
|
| 684 |
+
<div class="row">
|
| 685 |
+
<div class="col-lg-6">
|
| 686 |
+
<div class="viz-container">
|
| 687 |
+
<h5>PVI vs. Swing Score (by Cluster)</h5>
|
| 688 |
+
<div id="precinct-scatter" style="height: 400px;"></div>
|
| 689 |
+
</div>
|
| 690 |
+
</div>
|
| 691 |
+
<div class="col-lg-6">
|
| 692 |
+
<div class="viz-container">
|
| 693 |
+
<h5>Precinct Priority Ranking</h5>
|
| 694 |
+
<div id="precinct-ranking" style="height: 400px;"></div>
|
| 695 |
+
</div>
|
| 696 |
+
</div>
|
| 697 |
+
</div>
|
| 698 |
+
|
| 699 |
+
<div class="card">
|
| 700 |
+
<div class="card-header">
|
| 701 |
+
<i class="bi bi-list-ol"></i> Top 20 Precincts by Strategic Value
|
| 702 |
+
</div>
|
| 703 |
+
<div class="card-body">
|
| 704 |
+
<div class="table-responsive">
|
| 705 |
+
<table class="table table-sm" id="precinct-table">
|
| 706 |
+
<thead>
|
| 707 |
+
<tr>
|
| 708 |
+
<th>Rank</th>
|
| 709 |
+
<th>Precinct</th>
|
| 710 |
+
<th>PVI</th>
|
| 711 |
+
<th>Swing</th>
|
| 712 |
+
<th>TO Delta</th>
|
| 713 |
+
<th>Net Value</th>
|
| 714 |
+
<th>Tier</th>
|
| 715 |
+
</tr>
|
| 716 |
+
</thead>
|
| 717 |
+
<tbody>
|
| 718 |
+
<tr>
|
| 719 |
+
<td colspan="7" class="text-center text-muted">
|
| 720 |
+
Run analysis to populate table
|
| 721 |
+
</td>
|
| 722 |
+
</tr>
|
| 723 |
+
</tbody>
|
| 724 |
+
</table>
|
| 725 |
+
</div>
|
| 726 |
+
</div>
|
| 727 |
+
</div>
|
| 728 |
+
</div>
|
| 729 |
+
|
| 730 |
+
<!-- Monte Carlo Simulation Section -->
|
| 731 |
+
<div id="simulation" class="content-section">
|
| 732 |
+
<h2>Monte Carlo Simulation</h2>
|
| 733 |
+
<p class="text-muted mb-4">Run thousands of election simulations to estimate win probability and margin distribution</p>
|
| 734 |
+
|
| 735 |
+
<div class="card">
|
| 736 |
+
<div class="card-header">
|
| 737 |
+
<i class="bi bi-sliders"></i> Simulation Parameters
|
| 738 |
+
</div>
|
| 739 |
+
<div class="card-body">
|
| 740 |
+
<div class="row">
|
| 741 |
+
<div class="col-md-6">
|
| 742 |
+
<label class="form-label">Number of Iterations</label>
|
| 743 |
+
<select class="form-select" id="mc-iterations">
|
| 744 |
+
<option value="1000">1,000 (Quick)</option>
|
| 745 |
+
<option value="5000">5,000 (Standard)</option>
|
| 746 |
+
<option value="10000" selected>10,000 (Recommended)</option>
|
| 747 |
+
<option value="50000">50,000 (Tight Race)</option>
|
| 748 |
+
</select>
|
| 749 |
+
</div>
|
| 750 |
+
<div class="col-md-6">
|
| 751 |
+
<label class="form-label">Baseline Environment</label>
|
| 752 |
+
<select class="form-select" id="mc-environment">
|
| 753 |
+
<option value="neutral">Neutral / Even</option>
|
| 754 |
+
<option value="d3">D+3 Environment</option>
|
| 755 |
+
<option value="r3">R+3 Environment</option>
|
| 756 |
+
<option value="d5">D+5 Wave</option>
|
| 757 |
+
<option value="r5">R+5 Wave</option>
|
| 758 |
+
</select>
|
| 759 |
+
</div>
|
| 760 |
+
</div>
|
| 761 |
+
|
| 762 |
+
<div class="row mt-3">
|
| 763 |
+
<div class="col-md-6">
|
| 764 |
+
<label class="form-label">
|
| 765 |
+
Polling Error (σ): <span class="slider-value" id="polling-error-val">3.5</span>%
|
| 766 |
+
</label>
|
| 767 |
+
<input type="range" id="polling-error" min="1" max="6" step="0.5" value="3.5"
|
| 768 |
+
oninput="document.getElementById('polling-error-val').textContent = this.value">
|
| 769 |
+
</div>
|
| 770 |
+
<div class="col-md-6">
|
| 771 |
+
<label class="form-label">
|
| 772 |
+
Turnout Variability: <span class="slider-value" id="turnout-var-val">5</span>%
|
| 773 |
+
</label>
|
| 774 |
+
<input type="range" id="turnout-var" min="2" max="10" step="1" value="5"
|
| 775 |
+
oninput="document.getElementById('turnout-var-val').textContent = this.value">
|
| 776 |
+
</div>
|
| 777 |
+
</div>
|
| 778 |
+
|
| 779 |
+
<div class="mt-4">
|
| 780 |
+
<button class="btn btn-primary btn-lg" onclick="runMonteCarloSimulation()">
|
| 781 |
+
<i class="bi bi-play-circle-fill"></i> Run Simulation
|
| 782 |
+
</button>
|
| 783 |
+
<button class="btn btn-secondary" onclick="resetSimulation()">
|
| 784 |
+
<i class="bi bi-arrow-clockwise"></i> Reset
|
| 785 |
+
</button>
|
| 786 |
+
</div>
|
| 787 |
+
|
| 788 |
+
<div id="mc-spinner" class="spinner">
|
| 789 |
+
<div class="spinner-border text-primary" role="status"></div>
|
| 790 |
+
<p class="mt-3">Running simulation...</p>
|
| 791 |
+
</div>
|
| 792 |
+
</div>
|
| 793 |
+
</div>
|
| 794 |
+
|
| 795 |
+
<div class="row">
|
| 796 |
+
<div class="col-lg-4">
|
| 797 |
+
<div class="metric-card blue">
|
| 798 |
+
<div class="metric-label">Win Probability</div>
|
| 799 |
+
<div class="metric-value" id="mc-win-prob">--</div>
|
| 800 |
+
</div>
|
| 801 |
+
</div>
|
| 802 |
+
<div class="col-lg-4">
|
| 803 |
+
<div class="metric-card purple">
|
| 804 |
+
<div class="metric-label">Expected Margin</div>
|
| 805 |
+
<div class="metric-value" id="mc-margin">--</div>
|
| 806 |
+
</div>
|
| 807 |
+
</div>
|
| 808 |
+
<div class="col-lg-4">
|
| 809 |
+
<div class="metric-card green">
|
| 810 |
+
<div class="metric-label">Recount Probability</div>
|
| 811 |
+
<div class="metric-value" id="mc-recount">--</div>
|
| 812 |
+
</div>
|
| 813 |
+
</div>
|
| 814 |
+
</div>
|
| 815 |
+
|
| 816 |
+
<div class="viz-container">
|
| 817 |
+
<h5>Margin Distribution (10,000 Simulations)</h5>
|
| 818 |
+
<div id="mc-histogram" style="height: 400px;"></div>
|
| 819 |
+
</div>
|
| 820 |
+
</div>
|
| 821 |
+
|
| 822 |
+
<!-- Scenarios Section -->
|
| 823 |
+
<div id="scenarios" class="content-section">
|
| 824 |
+
<h2>Scenario Builder</h2>
|
| 825 |
+
<p class="text-muted mb-4">Model "what if" scenarios with demographic shifts, turnout changes, and third-party effects</p>
|
| 826 |
+
|
| 827 |
+
<div class="card">
|
| 828 |
+
<div class="card-header">
|
| 829 |
+
<i class="bi bi-plus-circle"></i> Build Scenario
|
| 830 |
+
</div>
|
| 831 |
+
<div class="card-body">
|
| 832 |
+
<div class="row">
|
| 833 |
+
<div class="col-md-6">
|
| 834 |
+
<label class="form-label">Scenario Name</label>
|
| 835 |
+
<input type="text" class="form-control" id="scenario-name" placeholder="e.g., High Youth Turnout">
|
| 836 |
+
</div>
|
| 837 |
+
<div class="col-md-6">
|
| 838 |
+
<label class="form-label">Compare Against</label>
|
| 839 |
+
<select class="form-select" id="scenario-baseline">
|
| 840 |
+
<option value="2024">2024 Baseline</option>
|
| 841 |
+
<option value="2022">2022 Midterm</option>
|
| 842 |
+
<option value="2020">2020 Presidential</option>
|
| 843 |
+
</select>
|
| 844 |
+
</div>
|
| 845 |
+
</div>
|
| 846 |
+
|
| 847 |
+
<hr class="my-4">
|
| 848 |
+
|
| 849 |
+
<h6 class="mb-3">Turnout Adjustments (by demographic)</h6>
|
| 850 |
+
<div class="row">
|
| 851 |
+
<div class="col-md-6">
|
| 852 |
+
<label class="form-label">
|
| 853 |
+
18-29 Turnout: <span class="slider-value" id="turnout-18-29-val">0</span>%
|
| 854 |
+
</label>
|
| 855 |
+
<input type="range" id="turnout-18-29" min="-30" max="30" step="5" value="0"
|
| 856 |
+
oninput="document.getElementById('turnout-18-29-val').textContent = (this.value > 0 ? '+' : '') + this.value">
|
| 857 |
+
</div>
|
| 858 |
+
<div class="col-md-6">
|
| 859 |
+
<label class="form-label">
|
| 860 |
+
College+ Turnout: <span class="slider-value" id="turnout-college-val">0</span>%
|
| 861 |
+
</label>
|
| 862 |
+
<input type="range" id="turnout-college" min="-30" max="30" step="5" value="0"
|
| 863 |
+
oninput="document.getElementById('turnout-college-val').textContent = (this.value > 0 ? '+' : '') + this.value">
|
| 864 |
+
</div>
|
| 865 |
+
</div>
|
| 866 |
+
|
| 867 |
+
<h6 class="mb-3 mt-4">Partisan Shifts (by demographic)</h6>
|
| 868 |
+
<div class="row">
|
| 869 |
+
<div class="col-md-6">
|
| 870 |
+
<label class="form-label">
|
| 871 |
+
Hispanic D Margin: <span class="slider-value" id="hispanic-shift-val">0</span> pts
|
| 872 |
+
</label>
|
| 873 |
+
<input type="range" id="hispanic-shift" min="-15" max="15" step="1" value="0"
|
| 874 |
+
oninput="document.getElementById('hispanic-shift-val').textContent = (this.value > 0 ? '+' : '') + this.value">
|
| 875 |
+
</div>
|
| 876 |
+
<div class="col-md-6">
|
| 877 |
+
<label class="form-label">
|
| 878 |
+
Suburban Women D Margin: <span class="slider-value" id="subwomen-shift-val">0</span> pts
|
| 879 |
+
</label>
|
| 880 |
+
<input type="range" id="subwomen-shift" min="-15" max="15" step="1" value="0"
|
| 881 |
+
oninput="document.getElementById('subwomen-shift-val').textContent = (this.value > 0 ? '+' : '') + this.value">
|
| 882 |
+
</div>
|
| 883 |
+
</div>
|
| 884 |
+
|
| 885 |
+
<div class="mt-4">
|
| 886 |
+
<button class="btn btn-success" onclick="runScenario()">
|
| 887 |
+
<i class="bi bi-calculator"></i> Calculate Scenario
|
| 888 |
+
</button>
|
| 889 |
+
</div>
|
| 890 |
+
</div>
|
| 891 |
+
</div>
|
| 892 |
+
|
| 893 |
+
<div class="card">
|
| 894 |
+
<div class="card-header">
|
| 895 |
+
<i class="bi bi-bar-chart-line"></i> Scenario Comparison
|
| 896 |
+
</div>
|
| 897 |
+
<div class="card-body">
|
| 898 |
+
<div id="scenario-comparison" style="height: 400px;"></div>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
</div>
|
| 902 |
+
|
| 903 |
+
<!-- Voter Targeting Section -->
|
| 904 |
+
<div id="targeting" class="content-section">
|
| 905 |
+
<h2>Voter Targeting</h2>
|
| 906 |
+
<p class="text-muted mb-4">Build persuasion and mobilization universes with data-driven segment prioritization</p>
|
| 907 |
+
|
| 908 |
+
<div class="card">
|
| 909 |
+
<div class="card-header">
|
| 910 |
+
<i class="bi bi-funnel"></i> Build Universe
|
| 911 |
+
</div>
|
| 912 |
+
<div class="card-body">
|
| 913 |
+
<div class="row">
|
| 914 |
+
<div class="col-md-4">
|
| 915 |
+
<label class="form-label">Universe Type</label>
|
| 916 |
+
<select class="form-select" id="universe-type">
|
| 917 |
+
<option value="persuasion">Persuasion</option>
|
| 918 |
+
<option value="mobilization">Mobilization</option>
|
| 919 |
+
<option value="combined">Combined</option>
|
| 920 |
+
</select>
|
| 921 |
+
</div>
|
| 922 |
+
<div class="col-md-4">
|
| 923 |
+
<label class="form-label">Competitiveness Threshold</label>
|
| 924 |
+
<select class="form-select" id="comp-threshold">
|
| 925 |
+
<option value="0.7">High (>70% competitive)</option>
|
| 926 |
+
<option value="0.5" selected>Medium (>50%)</option>
|
| 927 |
+
<option value="0.3">Low (>30%)</option>
|
| 928 |
+
</select>
|
| 929 |
+
</div>
|
| 930 |
+
<div class="col-md-4">
|
| 931 |
+
<label class="form-label">Min Support Score</label>
|
| 932 |
+
<select class="form-select" id="support-score">
|
| 933 |
+
<option value="3">Lean Support (3+)</option>
|
| 934 |
+
<option value="4" selected>Likely Support (4+)</option>
|
| 935 |
+
<option value="5">Strong Support (5)</option>
|
| 936 |
+
</select>
|
| 937 |
+
</div>
|
| 938 |
+
</div>
|
| 939 |
+
<div class="mt-3">
|
| 940 |
+
<button class="btn btn-primary" onclick="buildUniverse()">
|
| 941 |
+
<i class="bi bi-play-fill"></i> Generate Universe
|
| 942 |
+
</button>
|
| 943 |
+
</div>
|
| 944 |
+
</div>
|
| 945 |
+
</div>
|
| 946 |
+
|
| 947 |
+
<div class="metrics-grid">
|
| 948 |
+
<div class="metric-card blue">
|
| 949 |
+
<div class="metric-label">Universe Size</div>
|
| 950 |
+
<div class="metric-value" id="universe-size">--</div>
|
| 951 |
+
</div>
|
| 952 |
+
<div class="metric-card purple">
|
| 953 |
+
<div class="metric-label">Contact Rate Goal</div>
|
| 954 |
+
<div class="metric-value" id="contact-rate">--</div>
|
| 955 |
+
</div>
|
| 956 |
+
<div class="metric-card green">
|
| 957 |
+
<div class="metric-label">Estimated Vote Yield</div>
|
| 958 |
+
<div class="metric-value" id="vote-yield">--</div>
|
| 959 |
+
</div>
|
| 960 |
+
</div>
|
| 961 |
+
|
| 962 |
+
<div class="card">
|
| 963 |
+
<div class="card-header">
|
| 964 |
+
<i class="bi bi-people-fill"></i> Segment Performance
|
| 965 |
+
</div>
|
| 966 |
+
<div class="card-body">
|
| 967 |
+
<div id="segment-performance" style="height: 400px;"></div>
|
| 968 |
+
</div>
|
| 969 |
+
</div>
|
| 970 |
+
</div>
|
| 971 |
+
|
| 972 |
+
<!-- GOTV Section -->
|
| 973 |
+
<div id="gotv" class="content-section">
|
| 974 |
+
<h2>GOTV Planning</h2>
|
| 975 |
+
<p class="text-muted mb-4">Optimize canvass turf, phone banks, and volunteer deployment</p>
|
| 976 |
+
|
| 977 |
+
<div class="alert alert-warning">
|
| 978 |
+
<i class="bi bi-exclamation-triangle"></i>
|
| 979 |
+
<strong>GOTV Window:</strong> Most effective in final 10-14 days. Focus on identified supporters only.
|
| 980 |
+
</div>
|
| 981 |
+
|
| 982 |
+
<div class="card">
|
| 983 |
+
<div class="card-header">
|
| 984 |
+
<i class="bi bi-calendar-check"></i> Resource Allocation
|
| 985 |
+
</div>
|
| 986 |
+
<div class="card-body">
|
| 987 |
+
<div class="row">
|
| 988 |
+
<div class="col-md-4">
|
| 989 |
+
<label class="form-label">Volunteer Hours Available</label>
|
| 990 |
+
<input type="number" class="form-control" id="volunteer-hours" value="500">
|
| 991 |
+
</div>
|
| 992 |
+
<div class="col-md-4">
|
| 993 |
+
<label class="form-label">Phone Bank Capacity</label>
|
| 994 |
+
<input type="number" class="form-control" id="phone-capacity" value="10000">
|
| 995 |
+
</div>
|
| 996 |
+
<div class="col-md-4">
|
| 997 |
+
<label class="form-label">Days Until Election</label>
|
| 998 |
+
<input type="number" class="form-control" id="days-to-election" value="14">
|
| 999 |
+
</div>
|
| 1000 |
+
</div>
|
| 1001 |
+
|
| 1002 |
+
<div class="row mt-3">
|
| 1003 |
+
<div class="col-md-6">
|
| 1004 |
+
<label class="form-label">Contact Mix</label>
|
| 1005 |
+
<select class="form-select" id="contact-mix">
|
| 1006 |
+
<option value="balanced">Balanced (50/50 doors/phones)</option>
|
| 1007 |
+
<option value="door-heavy">Door-Heavy (70/30)</option>
|
| 1008 |
+
<option value="phone-heavy">Phone-Heavy (30/70)</option>
|
| 1009 |
+
</select>
|
| 1010 |
+
</div>
|
| 1011 |
+
<div class="col-md-6">
|
| 1012 |
+
<label class="form-label">Priority Mode</label>
|
| 1013 |
+
<select class="form-select" id="priority-mode">
|
| 1014 |
+
<option value="net">Net Value (Balanced)</option>
|
| 1015 |
+
<option value="persuasion">Persuasion Focus</option>
|
| 1016 |
+
<option value="mobilization">Turnout Focus</option>
|
| 1017 |
+
</select>
|
| 1018 |
+
</div>
|
| 1019 |
+
</div>
|
| 1020 |
+
|
| 1021 |
+
<div class="mt-4">
|
| 1022 |
+
<button class="btn btn-success btn-lg" onclick="optimizeTurf()">
|
| 1023 |
+
<i class="bi bi-geo-alt-fill"></i> Optimize Turf Cutting
|
| 1024 |
+
</button>
|
| 1025 |
+
</div>
|
| 1026 |
+
</div>
|
| 1027 |
+
</div>
|
| 1028 |
+
|
| 1029 |
+
<div class="card">
|
| 1030 |
+
<div class="card-header">
|
| 1031 |
+
<i class="bi bi-map"></i> Recommended Precinct Allocation
|
| 1032 |
+
</div>
|
| 1033 |
+
<div class="card-body">
|
| 1034 |
+
<div class="table-responsive">
|
| 1035 |
+
<table class="table" id="turf-allocation">
|
| 1036 |
+
<thead>
|
| 1037 |
+
<tr>
|
| 1038 |
+
<th>Priority</th>
|
| 1039 |
+
<th>Precinct</th>
|
| 1040 |
+
<th>Doors</th>
|
| 1041 |
+
<th>Phone Attempts</th>
|
| 1042 |
+
<th>Vol. Hours</th>
|
| 1043 |
+
<th>Est. Votes</th>
|
| 1044 |
+
</tr>
|
| 1045 |
+
</thead>
|
| 1046 |
+
<tbody>
|
| 1047 |
+
<tr>
|
| 1048 |
+
<td colspan="6" class="text-center text-muted">
|
| 1049 |
+
Run optimization to generate allocation
|
| 1050 |
+
</td>
|
| 1051 |
+
</tr>
|
| 1052 |
+
</tbody>
|
| 1053 |
+
</table>
|
| 1054 |
+
</div>
|
| 1055 |
+
</div>
|
| 1056 |
+
</div>
|
| 1057 |
+
</div>
|
| 1058 |
+
|
| 1059 |
+
<!-- Messaging Section -->
|
| 1060 |
+
<div id="messaging" class="content-section">
|
| 1061 |
+
<h2>Messaging Strategy</h2>
|
| 1062 |
+
<p class="text-muted mb-4">Issue salience mapping and message-to-market fit analysis</p>
|
| 1063 |
+
|
| 1064 |
+
<div class="card">
|
| 1065 |
+
<div class="card-header">
|
| 1066 |
+
<i class="bi bi-list-check"></i> Issue Priorities by Segment
|
| 1067 |
+
</div>
|
| 1068 |
+
<div class="card-body">
|
| 1069 |
+
<div class="table-responsive">
|
| 1070 |
+
<table class="table">
|
| 1071 |
+
<thead>
|
| 1072 |
+
<tr>
|
| 1073 |
+
<th>Issue</th>
|
| 1074 |
+
<th>Young Urban</th>
|
| 1075 |
+
<th>Suburban Swing</th>
|
| 1076 |
+
<th>Rural Senior</th>
|
| 1077 |
+
<th>Overall</th>
|
| 1078 |
+
</tr>
|
| 1079 |
+
</thead>
|
| 1080 |
+
<tbody>
|
| 1081 |
+
<tr>
|
| 1082 |
+
<td><strong>Economy/Jobs</strong></td>
|
| 1083 |
+
<td><span class="badge bg-warning">Medium</span></td>
|
| 1084 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1085 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1086 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1087 |
+
</tr>
|
| 1088 |
+
<tr>
|
| 1089 |
+
<td><strong>Healthcare</strong></td>
|
| 1090 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1091 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1092 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1093 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1094 |
+
</tr>
|
| 1095 |
+
<tr>
|
| 1096 |
+
<td><strong>Abortion/Reproductive Rights</strong></td>
|
| 1097 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1098 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1099 |
+
<td><span class="badge bg-secondary">Low</span></td>
|
| 1100 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1101 |
+
</tr>
|
| 1102 |
+
<tr>
|
| 1103 |
+
<td><strong>Immigration</strong></td>
|
| 1104 |
+
<td><span class="badge bg-secondary">Low</span></td>
|
| 1105 |
+
<td><span class="badge bg-warning">Medium</span></td>
|
| 1106 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1107 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1108 |
+
</tr>
|
| 1109 |
+
<tr>
|
| 1110 |
+
<td><strong>Climate/Environment</strong></td>
|
| 1111 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1112 |
+
<td><span class="badge bg-warning">Medium</span></td>
|
| 1113 |
+
<td><span class="badge bg-secondary">Low</span></td>
|
| 1114 |
+
<td><span class="badge bg-warning">Medium</span></td>
|
| 1115 |
+
</tr>
|
| 1116 |
+
<tr>
|
| 1117 |
+
<td><strong>Education</strong></td>
|
| 1118 |
+
<td><span class="badge bg-warning">Medium</span></td>
|
| 1119 |
+
<td><span class="badge bg-danger">Very High</span></td>
|
| 1120 |
+
<td><span class="badge bg-warning">Medium</span></td>
|
| 1121 |
+
<td><span class="badge bg-success">High</span></td>
|
| 1122 |
+
</tr>
|
| 1123 |
+
</tbody>
|
| 1124 |
+
</table>
|
| 1125 |
+
</div>
|
| 1126 |
+
</div>
|
| 1127 |
+
</div>
|
| 1128 |
+
|
| 1129 |
+
<div class="alert alert-info">
|
| 1130 |
+
<i class="bi bi-lightbulb"></i>
|
| 1131 |
+
<strong>Strategy Recommendation:</strong> Lead with economy/jobs for suburban swing voters. Layer in healthcare for seniors. Use abortion rights to mobilize young urban base. Avoid immigration emphasis with young voters.
|
| 1132 |
+
</div>
|
| 1133 |
+
</div>
|
| 1134 |
+
|
| 1135 |
+
<!-- Forecasting Section -->
|
| 1136 |
+
<div id="forecasting" class="content-section">
|
| 1137 |
+
<h2>Election Forecasting</h2>
|
| 1138 |
+
<p class="text-muted mb-4">Bayesian model with polling integration and early vote tracking</p>
|
| 1139 |
+
|
| 1140 |
+
<div class="card">
|
| 1141 |
+
<div class="card-header">
|
| 1142 |
+
<i class="bi bi-graph-up"></i> Model Inputs
|
| 1143 |
+
</div>
|
| 1144 |
+
<div class="card-body">
|
| 1145 |
+
<ul class="nav nav-tabs" role="tablist">
|
| 1146 |
+
<li class="nav-item">
|
| 1147 |
+
<a class="nav-link active" data-bs-toggle="tab" href="#fundamentals-tab">Fundamentals</a>
|
| 1148 |
+
</li>
|
| 1149 |
+
<li class="nav-item">
|
| 1150 |
+
<a class="nav-link" data-bs-toggle="tab" href="#polling-tab">Polling</a>
|
| 1151 |
+
</li>
|
| 1152 |
+
<li class="nav-item">
|
| 1153 |
+
<a class="nav-link" data-bs-toggle="tab" href="#early-vote-tab">Early Vote</a>
|
| 1154 |
+
</li>
|
| 1155 |
+
</ul>
|
| 1156 |
+
|
| 1157 |
+
<div class="tab-content mt-3">
|
| 1158 |
+
<div id="fundamentals-tab" class="tab-pane fade show active">
|
| 1159 |
+
<div class="row">
|
| 1160 |
+
<div class="col-md-6">
|
| 1161 |
+
<label class="form-label">Incumbent Party</label>
|
| 1162 |
+
<select class="form-select">
|
| 1163 |
+
<option>Democrat</option>
|
| 1164 |
+
<option>Republican</option>
|
| 1165 |
+
<option>Open Seat</option>
|
| 1166 |
+
</select>
|
| 1167 |
+
</div>
|
| 1168 |
+
<div class="col-md-6">
|
| 1169 |
+
<label class="form-label">Generic Ballot (D+/-)</label>
|
| 1170 |
+
<input type="number" class="form-control" value="0" step="0.5">
|
| 1171 |
+
</div>
|
| 1172 |
+
</div>
|
| 1173 |
+
<div class="row mt-3">
|
| 1174 |
+
<div class="col-md-6">
|
| 1175 |
+
<label class="form-label">Presidential Approval</label>
|
| 1176 |
+
<input type="number" class="form-control" value="45" step="1">
|
| 1177 |
+
</div>
|
| 1178 |
+
<div class="col-md-6">
|
| 1179 |
+
<label class="form-label">Right Track/Wrong Track</label>
|
| 1180 |
+
<input type="number" class="form-control" value="35" step="1">
|
| 1181 |
+
</div>
|
| 1182 |
+
</div>
|
| 1183 |
+
</div>
|
| 1184 |
+
|
| 1185 |
+
<div id="polling-tab" class="tab-pane fade">
|
| 1186 |
+
<p class="text-muted">Add recent polls to update the forecast</p>
|
| 1187 |
+
<div class="row">
|
| 1188 |
+
<div class="col-md-4">
|
| 1189 |
+
<label class="form-label">D Margin</label>
|
| 1190 |
+
<input type="number" class="form-control" step="0.5">
|
| 1191 |
+
</div>
|
| 1192 |
+
<div class="col-md-4">
|
| 1193 |
+
<label class="form-label">Sample Size</label>
|
| 1194 |
+
<input type="number" class="form-control">
|
| 1195 |
+
</div>
|
| 1196 |
+
<div class="col-md-4">
|
| 1197 |
+
<label class="form-label">Pollster Rating</label>
|
| 1198 |
+
<select class="form-select">
|
| 1199 |
+
<option>A+</option>
|
| 1200 |
+
<option>A</option>
|
| 1201 |
+
<option>B</option>
|
| 1202 |
+
<option>C</option>
|
| 1203 |
+
</select>
|
| 1204 |
+
</div>
|
| 1205 |
+
</div>
|
| 1206 |
+
<button class="btn btn-sm btn-primary mt-2">
|
| 1207 |
+
<i class="bi bi-plus"></i> Add Poll
|
| 1208 |
+
</button>
|
| 1209 |
+
</div>
|
| 1210 |
+
|
| 1211 |
+
<div id="early-vote-tab" class="tab-pane fade">
|
| 1212 |
+
<p class="text-muted">Track early/mail ballots returned</p>
|
| 1213 |
+
<div class="row">
|
| 1214 |
+
<div class="col-md-6">
|
| 1215 |
+
<label class="form-label">D Early Vote Share (%)</label>
|
| 1216 |
+
<input type="number" class="form-control" step="0.5">
|
| 1217 |
+
</div>
|
| 1218 |
+
<div class="col-md-6">
|
| 1219 |
+
<label class="form-label">% of Expected Turnout</label>
|
| 1220 |
+
<input type="number" class="form-control" step="1">
|
| 1221 |
+
</div>
|
| 1222 |
+
</div>
|
| 1223 |
+
</div>
|
| 1224 |
+
</div>
|
| 1225 |
+
</div>
|
| 1226 |
+
</div>
|
| 1227 |
+
|
| 1228 |
+
<div class="card">
|
| 1229 |
+
<div class="card-header">
|
| 1230 |
+
<i class="bi bi-speedometer"></i> Current Forecast
|
| 1231 |
+
</div>
|
| 1232 |
+
<div class="card-body">
|
| 1233 |
+
<div class="row text-center">
|
| 1234 |
+
<div class="col-md-4">
|
| 1235 |
+
<h1 class="text-primary">52.3%</h1>
|
| 1236 |
+
<p class="text-muted">Win Probability</p>
|
| 1237 |
+
</div>
|
| 1238 |
+
<div class="col-md-4">
|
| 1239 |
+
<h1>D+1.8</h1>
|
| 1240 |
+
<p class="text-muted">Expected Margin</p>
|
| 1241 |
+
</div>
|
| 1242 |
+
<div class="col-md-4">
|
| 1243 |
+
<h1 class="text-success">85%</h1>
|
| 1244 |
+
<p class="text-muted">Model Confidence</p>
|
| 1245 |
+
</div>
|
| 1246 |
+
</div>
|
| 1247 |
+
<div class="progress mt-3" style="height: 30px;">
|
| 1248 |
+
<div class="progress-bar bg-primary" style="width: 52.3%">Democrat 52.3%</div>
|
| 1249 |
+
<div class="progress-bar bg-danger" style="width: 47.7%">Republican 47.7%</div>
|
| 1250 |
+
</div>
|
| 1251 |
+
</div>
|
| 1252 |
+
</div>
|
| 1253 |
+
</div>
|
| 1254 |
+
|
| 1255 |
+
<!-- Data Upload Section -->
|
| 1256 |
+
<div id="upload" class="content-section">
|
| 1257 |
+
<h2>Data Upload & Management</h2>
|
| 1258 |
+
<p class="text-muted mb-4">Import voter files, election results, demographics, and precinct shapefiles</p>
|
| 1259 |
+
|
| 1260 |
+
<div class="row">
|
| 1261 |
+
<div class="col-lg-6">
|
| 1262 |
+
<div class="card">
|
| 1263 |
+
<div class="card-header">
|
| 1264 |
+
<i class="bi bi-file-earmark-arrow-up"></i> Upload Election Results
|
| 1265 |
+
</div>
|
| 1266 |
+
<div class="card-body">
|
| 1267 |
+
<div class="file-upload">
|
| 1268 |
+
<i class="bi bi-cloud-upload"></i>
|
| 1269 |
+
<h5>Drag & Drop or Click to Upload</h5>
|
| 1270 |
+
<p class="text-muted">CSV, Excel, TXT (precinct-level results)</p>
|
| 1271 |
+
<input type="file" class="d-none" id="results-file" accept=".csv,.xlsx,.txt">
|
| 1272 |
+
<button class="btn btn-primary mt-2" onclick="document.getElementById('results-file').click()">
|
| 1273 |
+
Select File
|
| 1274 |
+
</button>
|
| 1275 |
+
</div>
|
| 1276 |
+
</div>
|
| 1277 |
+
</div>
|
| 1278 |
+
</div>
|
| 1279 |
+
|
| 1280 |
+
<div class="col-lg-6">
|
| 1281 |
+
<div class="card">
|
| 1282 |
+
<div class="card-header">
|
| 1283 |
+
<i class="bi bi-people"></i> Upload Voter File
|
| 1284 |
+
</div>
|
| 1285 |
+
<div class="card-body">
|
| 1286 |
+
<div class="file-upload">
|
| 1287 |
+
<i class="bi bi-cloud-upload"></i>
|
| 1288 |
+
<h5>Drag & Drop or Click to Upload</h5>
|
| 1289 |
+
<p class="text-muted">CSV, Excel (L2, TargetSmart, VAN export)</p>
|
| 1290 |
+
<input type="file" class="d-none" id="voter-file" accept=".csv,.xlsx">
|
| 1291 |
+
<button class="btn btn-primary mt-2" onclick="document.getElementById('voter-file').click()">
|
| 1292 |
+
Select File
|
| 1293 |
+
</button>
|
| 1294 |
+
</div>
|
| 1295 |
+
</div>
|
| 1296 |
+
</div>
|
| 1297 |
+
</div>
|
| 1298 |
+
</div>
|
| 1299 |
+
|
| 1300 |
+
<div class="row">
|
| 1301 |
+
<div class="col-lg-6">
|
| 1302 |
+
<div class="card">
|
| 1303 |
+
<div class="card-header">
|
| 1304 |
+
<i class="bi bi-bar-chart"></i> Upload Demographics
|
| 1305 |
+
</div>
|
| 1306 |
+
<div class="card-body">
|
| 1307 |
+
<div class="file-upload">
|
| 1308 |
+
<i class="bi bi-cloud-upload"></i>
|
| 1309 |
+
<h5>Drag & Drop or Click to Upload</h5>
|
| 1310 |
+
<p class="text-muted">CSV, Excel (Census ACS tables)</p>
|
| 1311 |
+
<input type="file" class="d-none" id="demo-file" accept=".csv,.xlsx">
|
| 1312 |
+
<button class="btn btn-primary mt-2" onclick="document.getElementById('demo-file').click()">
|
| 1313 |
+
Select File
|
| 1314 |
+
</button>
|
| 1315 |
+
</div>
|
| 1316 |
+
</div>
|
| 1317 |
+
</div>
|
| 1318 |
+
</div>
|
| 1319 |
+
|
| 1320 |
+
<div class="col-lg-6">
|
| 1321 |
+
<div class="card">
|
| 1322 |
+
<div class="card-header">
|
| 1323 |
+
<i class="bi bi-map"></i> Upload Precinct Boundaries
|
| 1324 |
+
</div>
|
| 1325 |
+
<div class="card-body">
|
| 1326 |
+
<div class="file-upload">
|
| 1327 |
+
<i class="bi bi-cloud-upload"></i>
|
| 1328 |
+
<h5>Drag & Drop or Click to Upload</h5>
|
| 1329 |
+
<p class="text-muted">GeoJSON, Shapefile (VEST, TIGER/Line)</p>
|
| 1330 |
+
<input type="file" class="d-none" id="geo-file" accept=".geojson,.json,.zip">
|
| 1331 |
+
<button class="btn btn-primary mt-2" onclick="document.getElementById('geo-file').click()">
|
| 1332 |
+
Select File
|
| 1333 |
+
</button>
|
| 1334 |
+
</div>
|
| 1335 |
+
</div>
|
| 1336 |
+
</div>
|
| 1337 |
+
</div>
|
| 1338 |
+
</div>
|
| 1339 |
+
|
| 1340 |
+
<div class="card">
|
| 1341 |
+
<div class="card-header">
|
| 1342 |
+
<i class="bi bi-database"></i> Loaded Datasets
|
| 1343 |
+
</div>
|
| 1344 |
+
<div class="card-body">
|
| 1345 |
+
<div class="alert alert-success">
|
| 1346 |
+
<i class="bi bi-check-circle"></i> <strong>Sample District Data</strong> - 10 precincts, 2020-2024 results
|
| 1347 |
+
</div>
|
| 1348 |
+
<div class="alert alert-secondary">
|
| 1349 |
+
<i class="bi bi-x-circle"></i> No voter file loaded
|
| 1350 |
+
</div>
|
| 1351 |
+
<div class="alert alert-secondary">
|
| 1352 |
+
<i class="bi bi-x-circle"></i> No demographic data loaded
|
| 1353 |
+
</div>
|
| 1354 |
+
</div>
|
| 1355 |
+
</div>
|
| 1356 |
+
</div>
|
| 1357 |
+
|
| 1358 |
+
<!-- Export Section -->
|
| 1359 |
+
<div id="export" class="content-section">
|
| 1360 |
+
<h2>Export & Reports</h2>
|
| 1361 |
+
<p class="text-muted mb-4">Download analysis results, visualizations, and campaign reports</p>
|
| 1362 |
+
|
| 1363 |
+
<div class="card">
|
| 1364 |
+
<div class="card-header">
|
| 1365 |
+
<i class="bi bi-file-earmark-text"></i> Available Exports
|
| 1366 |
+
</div>
|
| 1367 |
+
<div class="card-body">
|
| 1368 |
+
<div class="list-group">
|
| 1369 |
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
| 1370 |
+
<div>
|
| 1371 |
+
<h6 class="mb-1">Precinct-Level Analysis (CSV)</h6>
|
| 1372 |
+
<small class="text-muted">All computed metrics, PVI, swing, turnout, strategic values</small>
|
| 1373 |
+
</div>
|
| 1374 |
+
<button class="btn btn-sm btn-primary">
|
| 1375 |
+
<i class="bi bi-download"></i> Download
|
| 1376 |
+
</button>
|
| 1377 |
+
</div>
|
| 1378 |
+
|
| 1379 |
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
| 1380 |
+
<div>
|
| 1381 |
+
<h6 class="mb-1">Voter Targeting Matrix (Excel)</h6>
|
| 1382 |
+
<small class="text-muted">Segment-by-segment breakdown with strategies</small>
|
| 1383 |
+
</div>
|
| 1384 |
+
<button class="btn btn-sm btn-primary">
|
| 1385 |
+
<i class="bi bi-download"></i> Download
|
| 1386 |
+
</button>
|
| 1387 |
+
</div>
|
| 1388 |
+
|
| 1389 |
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
| 1390 |
+
<div>
|
| 1391 |
+
<h6 class="mb-1">Monte Carlo Results (JSON)</h6>
|
| 1392 |
+
<small class="text-muted">Full simulation output with margin distribution</small>
|
| 1393 |
+
</div>
|
| 1394 |
+
<button class="btn btn-sm btn-primary">
|
| 1395 |
+
<i class="bi bi-download"></i> Download
|
| 1396 |
+
</button>
|
| 1397 |
+
</div>
|
| 1398 |
+
|
| 1399 |
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
| 1400 |
+
<div>
|
| 1401 |
+
<h6 class="mb-1">Interactive Maps (HTML)</h6>
|
| 1402 |
+
<small class="text-muted">Standalone HTML files with all visualizations</small>
|
| 1403 |
+
</div>
|
| 1404 |
+
<button class="btn btn-sm btn-primary">
|
| 1405 |
+
<i class="bi bi-download"></i> Download
|
| 1406 |
+
</button>
|
| 1407 |
+
</div>
|
| 1408 |
+
|
| 1409 |
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
| 1410 |
+
<div>
|
| 1411 |
+
<h6 class="mb-1">Campaign Strategy Report (PDF)</h6>
|
| 1412 |
+
<small class="text-muted">Executive summary with recommendations</small>
|
| 1413 |
+
</div>
|
| 1414 |
+
<button class="btn btn-sm btn-success">
|
| 1415 |
+
<i class="bi bi-file-pdf"></i> Generate PDF
|
| 1416 |
+
</button>
|
| 1417 |
+
</div>
|
| 1418 |
+
|
| 1419 |
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
| 1420 |
+
<div>
|
| 1421 |
+
<h6 class="mb-1">GOTV Turf Packets (Printable)</h6>
|
| 1422 |
+
<small class="text-muted">Walk lists by precinct with maps</small>
|
| 1423 |
+
</div>
|
| 1424 |
+
<button class="btn btn-sm btn-success">
|
| 1425 |
+
<i class="bi bi-printer"></i> Generate
|
| 1426 |
+
</button>
|
| 1427 |
+
</div>
|
| 1428 |
+
</div>
|
| 1429 |
+
</div>
|
| 1430 |
+
</div>
|
| 1431 |
+
</div>
|
| 1432 |
+
|
| 1433 |
+
</div>
|
| 1434 |
+
|
| 1435 |
+
<!-- Bootstrap JS -->
|
| 1436 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
| 1437 |
+
|
| 1438 |
+
<script>
|
| 1439 |
+
// Navigation
|
| 1440 |
+
document.querySelectorAll('.nav-item').forEach(item => {
|
| 1441 |
+
item.addEventListener('click', function() {
|
| 1442 |
+
// Update nav active state
|
| 1443 |
+
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
| 1444 |
+
this.classList.add('active');
|
| 1445 |
+
|
| 1446 |
+
// Show corresponding section
|
| 1447 |
+
const sectionId = this.dataset.section;
|
| 1448 |
+
document.querySelectorAll('.content-section').forEach(section => {
|
| 1449 |
+
section.classList.remove('active');
|
| 1450 |
+
});
|
| 1451 |
+
document.getElementById(sectionId).classList.add('active');
|
| 1452 |
+
});
|
| 1453 |
+
});
|
| 1454 |
+
|
| 1455 |
+
// Initialize Dashboard with Sample Data
|
| 1456 |
+
function initializeDashboard() {
|
| 1457 |
+
// Update metrics
|
| 1458 |
+
document.getElementById('win-prob').textContent = '58.3%';
|
| 1459 |
+
document.getElementById('expected-margin').textContent = 'D+2.4';
|
| 1460 |
+
document.getElementById('high-value-precincts').textContent = '8';
|
| 1461 |
+
document.getElementById('contact-target').textContent = '24.5K';
|
| 1462 |
+
|
| 1463 |
+
// Sample scatter plot
|
| 1464 |
+
const sampleData = [
|
| 1465 |
+
{x: -5, y: 3, name: 'P01', size: 200, color: 'Red Lean'},
|
| 1466 |
+
{x: 2, y: 7, name: 'P02', size: 150, color: 'Swing'},
|
| 1467 |
+
{x: 8, y: 2, name: 'P03', size: 180, color: 'Blue Base'},
|
| 1468 |
+
{x: -2, y: 5, name: 'P04', size: 220, color: 'Swing'},
|
| 1469 |
+
{x: 12, y: 4, name: 'P05', size: 160, color: 'Blue Base'},
|
| 1470 |
+
];
|
| 1471 |
+
|
| 1472 |
+
const trace = {
|
| 1473 |
+
x: sampleData.map(d => d.x),
|
| 1474 |
+
y: sampleData.map(d => d.y),
|
| 1475 |
+
text: sampleData.map(d => d.name),
|
| 1476 |
+
mode: 'markers',
|
| 1477 |
+
marker: {
|
| 1478 |
+
size: sampleData.map(d => d.size / 10),
|
| 1479 |
+
color: sampleData.map(d => d.color === 'Blue Base' ? '#1e40af' : d.color === 'Red Lean' ? '#dc2626' : '#9333ea')
|
| 1480 |
+
},
|
| 1481 |
+
type: 'scatter'
|
| 1482 |
+
};
|
| 1483 |
+
|
| 1484 |
+
Plotly.newPlot('district-map', [trace], {
|
| 1485 |
+
title: 'Precinct Positioning',
|
| 1486 |
+
xaxis: {title: 'PVI (Partisan Lean)'},
|
| 1487 |
+
yaxis: {title: 'Swing Score'},
|
| 1488 |
+
hovermode: 'closest'
|
| 1489 |
+
}, {responsive: true});
|
| 1490 |
+
|
| 1491 |
+
// Demographic pie
|
| 1492 |
+
Plotly.newPlot('demo-pie', [{
|
| 1493 |
+
values: [35, 28, 22, 15],
|
| 1494 |
+
labels: ['White Non-Hispanic', 'Hispanic/Latino', 'Black/AA', 'Asian/Other'],
|
| 1495 |
+
type: 'pie',
|
| 1496 |
+
marker: {
|
| 1497 |
+
colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6']
|
| 1498 |
+
}
|
| 1499 |
+
}], {
|
| 1500 |
+
title: 'Racial/Ethnic Composition'
|
| 1501 |
+
}, {responsive: true});
|
| 1502 |
+
|
| 1503 |
+
// Win probability timeline
|
| 1504 |
+
const dates = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'];
|
| 1505 |
+
const probs = [45, 48, 50, 52, 54, 56, 57, 58, 58, 58.3];
|
| 1506 |
+
|
| 1507 |
+
Plotly.newPlot('prob-timeline', [{
|
| 1508 |
+
x: dates,
|
| 1509 |
+
y: probs,
|
| 1510 |
+
type: 'scatter',
|
| 1511 |
+
mode: 'lines+markers',
|
| 1512 |
+
line: {color: '#1e40af', width: 3},
|
| 1513 |
+
fill: 'tozeroy'
|
| 1514 |
+
}], {
|
| 1515 |
+
yaxis: {title: 'Win Probability (%)', range: [0, 100]},
|
| 1516 |
+
xaxis: {title: '2024'}
|
| 1517 |
+
}, {responsive: true});
|
| 1518 |
+
}
|
| 1519 |
+
|
| 1520 |
+
// Placeholder functions (would connect to backend/data processing)
|
| 1521 |
+
function generateDemoSegmentation() {
|
| 1522 |
+
alert('Generating demographic segmentation...
|
| 1523 |
+
|
| 1524 |
+
This would analyze uploaded data and compute segment-level metrics.');
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
function analyzePrecincts() {
|
| 1528 |
+
alert('Running precinct-level analysis...
|
| 1529 |
+
|
| 1530 |
+
This would compute PVI, swing, elasticity, and clustering for all precincts.');
|
| 1531 |
+
}
|
| 1532 |
+
|
| 1533 |
+
function runMonteCarloSimulation() {
|
| 1534 |
+
document.getElementById('mc-spinner').classList.add('active');
|
| 1535 |
+
setTimeout(() => {
|
| 1536 |
+
document.getElementById('mc-spinner').classList.remove('active');
|
| 1537 |
+
document.getElementById('mc-win-prob').textContent = '58.3%';
|
| 1538 |
+
document.getElementById('mc-margin').textContent = 'D+2.4';
|
| 1539 |
+
document.getElementById('mc-recount').textContent = '3.2%';
|
| 1540 |
+
|
| 1541 |
+
// Generate histogram
|
| 1542 |
+
const margins = Array.from({length: 1000}, () =>
|
| 1543 |
+
Math.random() * 10 - 3
|
| 1544 |
+
);
|
| 1545 |
+
|
| 1546 |
+
Plotly.newPlot('mc-histogram', [{
|
| 1547 |
+
x: margins,
|
| 1548 |
+
type: 'histogram',
|
| 1549 |
+
nbinsx: 50,
|
| 1550 |
+
marker: {color: '#1e40af'}
|
| 1551 |
+
}], {
|
| 1552 |
+
title: '10,000 Simulated Election Outcomes',
|
| 1553 |
+
xaxis: {title: 'D Margin (%)'},
|
| 1554 |
+
yaxis: {title: 'Frequency'}
|
| 1555 |
+
}, {responsive: true});
|
| 1556 |
+
}, 2000);
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
function resetSimulation() {
|
| 1560 |
+
document.getElementById('mc-win-prob').textContent = '--';
|
| 1561 |
+
document.getElementById('mc-margin').textContent = '--';
|
| 1562 |
+
document.getElementById('mc-recount').textContent = '--';
|
| 1563 |
+
Plotly.purge('mc-histogram');
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
function runScenario() {
|
| 1567 |
+
alert('Calculating scenario...
|
| 1568 |
+
|
| 1569 |
+
This would re-run the model with adjusted demographic/turnout parameters.');
|
| 1570 |
+
}
|
| 1571 |
+
|
| 1572 |
+
function buildUniverse() {
|
| 1573 |
+
document.getElementById('universe-size').textContent = '24,518';
|
| 1574 |
+
document.getElementById('contact-rate').textContent = '65%';
|
| 1575 |
+
document.getElementById('vote-yield').textContent = '+1,842';
|
| 1576 |
+
alert('Universe generated!
|
| 1577 |
+
|
| 1578 |
+
24,518 voters identified across persuasion and mobilization targets.');
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
function optimizeTurf() {
|
| 1582 |
+
alert('Optimizing turf allocation...
|
| 1583 |
+
|
| 1584 |
+
This would rank precincts and allocate volunteer resources to maximize vote yield.');
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
// Initialize on load
|
| 1588 |
+
window.addEventListener('load', initializeDashboard);
|
| 1589 |
+
</script>
|
| 1590 |
+
</body>
|
| 1591 |
+
</html>
|
static/politicoai_gui.html
ADDED
|
File without changes
|