Spaces:
Sleeping
Sleeping
app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
-
from flask import Flask, render_template_string, jsonify,
|
| 2 |
import pandas as pd
|
| 3 |
-
from io import BytesIO
|
| 4 |
import os
|
|
|
|
|
|
|
| 5 |
|
| 6 |
app = Flask(__name__)
|
| 7 |
|
|
@@ -91,12 +92,40 @@ def quarter_trends_df():
|
|
| 91 |
qdf = qdf.sort_values(["YEAR", "QNUM"]).drop(columns=["YEAR", "QNUM"])
|
| 92 |
return qdf
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
# -----------------------------------------------------
|
| 95 |
# Chatbot Endpoint
|
| 96 |
# -----------------------------------------------------
|
| 97 |
@app.route("/chat_ollama/<query>")
|
| 98 |
def chat_ollama(query):
|
| 99 |
q = query.lower().strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
reply = "No relevant insight found. Try asking about customers, block types, or quarterly trends."
|
| 101 |
|
| 102 |
try:
|
|
@@ -120,7 +149,7 @@ def chat_ollama(query):
|
|
| 120 |
main_block = block_counts.index[0] if not block_counts.empty else "general"
|
| 121 |
reply = (
|
| 122 |
f"{cust_name} currently holds the highest backlog value (${value:,.0f}) "
|
| 123 |
-
f"with {main_block} as the main issue. Coordinate cross-functionally to resolve {main_block.lower()} constraints
|
| 124 |
)
|
| 125 |
|
| 126 |
# --- MULTIPLE BLOCK ORDERS ---
|
|
@@ -142,7 +171,7 @@ def chat_ollama(query):
|
|
| 142 |
top = bs.iloc[0]
|
| 143 |
reply = (
|
| 144 |
f"{top['BLOCK_TYPE']} block represents the largest backlog exposure "
|
| 145 |
-
f"(${top[WORTH_COL]:,.0f}). Engage respective teams to address {top['BLOCK_TYPE'].lower()} issues
|
| 146 |
)
|
| 147 |
|
| 148 |
# --- QUARTERLY TREND LOGIC ---
|
|
@@ -153,22 +182,17 @@ def chat_ollama(query):
|
|
| 153 |
qdf["QUARTER"].unique(),
|
| 154 |
key=lambda q: (int(q[:4]), int(q[-1]))
|
| 155 |
)
|
| 156 |
-
if
|
| 157 |
-
reply = "No quarterly trend data available."
|
| 158 |
-
else:
|
| 159 |
-
latest_quarter = quarters[-2] if len(quarters) >= 2 else quarters[-1]
|
| 160 |
next_quarter = quarters[-1]
|
| 161 |
-
|
| 162 |
next_q_df = qdf[qdf["QUARTER"] == next_quarter]
|
| 163 |
if not next_q_df.empty:
|
| 164 |
top_block = next_q_df.sort_values(WORTH_COL, ascending=False).iloc[0]
|
| 165 |
reply = (
|
| 166 |
-
f"
|
| 167 |
-
f"(${top_block[WORTH_COL]:,.0f}). Prepare mitigation plans with
|
| 168 |
)
|
| 169 |
else:
|
| 170 |
-
reply = f"No data found for
|
| 171 |
-
|
| 172 |
except Exception as e:
|
| 173 |
reply = f"(Error: {e})"
|
| 174 |
|
|
@@ -223,12 +247,7 @@ def data_quarter_trends():
|
|
| 223 |
qdf = quarter_trends_df()
|
| 224 |
if qdf.empty:
|
| 225 |
return jsonify({"columns": [], "rows": [], "insight": "No quarterly data found."})
|
| 226 |
-
|
| 227 |
-
qdf["QUARTER"].unique(),
|
| 228 |
-
key=lambda q: (int(q[:4]), int(q[-1]))
|
| 229 |
-
)
|
| 230 |
-
latest_quarter = quarters[-2] if len(quarters) >= 2 else quarters[-1]
|
| 231 |
-
next_quarter = quarters[-1]
|
| 232 |
next_q_df = qdf[qdf["QUARTER"] == next_quarter]
|
| 233 |
if not next_q_df.empty:
|
| 234 |
top_block = next_q_df.sort_values(WORTH_COL, ascending=False).iloc[0]
|
|
@@ -246,9 +265,8 @@ def data_quarter_trends():
|
|
| 246 |
})
|
| 247 |
|
| 248 |
# -----------------------------------------------------
|
| 249 |
-
# HTML Template
|
| 250 |
# -----------------------------------------------------
|
| 251 |
-
|
| 252 |
HTML = """
|
| 253 |
<!doctype html>
|
| 254 |
<html>
|
|
@@ -264,15 +282,8 @@ body{margin:0;font-family:'Segoe UI',sans-serif;overflow:hidden;}
|
|
| 264 |
.tiles{padding:10px;display:grid;grid-template-columns:1fr 1fr;gap:10px;}
|
| 265 |
.tile{background:#f4f4f4;color:#0A2048;border-radius:10px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,.2);padding:10px;font-weight:600;cursor:pointer;transition:0.2s;}
|
| 266 |
.tile:hover{background:#e2e2e2;}
|
| 267 |
-
#faqTile{
|
| 268 |
-
|
| 269 |
-
color:white;
|
| 270 |
-
font-style:italic;
|
| 271 |
-
grid-column:span 2;
|
| 272 |
-
}
|
| 273 |
-
#faqTile:hover{
|
| 274 |
-
background:linear-gradient(135deg,#1a2b60,#334b9a);
|
| 275 |
-
}
|
| 276 |
.chat{flex:1;overflow-y:auto;padding:10px;background:#fafafa;}
|
| 277 |
.msg-bot{background:white;border:1px solid #ddd;border-radius:10px;padding:10px;margin-bottom:8px;line-height:1.4;}
|
| 278 |
.table-wrap{overflow-x:auto;overflow-y:auto;max-height:60vh;margin-top:6px;border:1px solid #ddd;border-radius:6px;background:#fff;}
|
|
@@ -289,155 +300,102 @@ button:hover{background:#122b6b;}
|
|
| 289 |
</head>
|
| 290 |
<body>
|
| 291 |
<div class="container">
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
<div class="
|
| 298 |
-
|
| 299 |
-
<div class="
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
<
|
| 307 |
-
|
| 308 |
-
<div id="chat" class="chat">
|
| 309 |
-
<div class="msg-bot"><b>Hello!</b><br>I'm your Blocks Assistant. How can I help you today?</div>
|
| 310 |
-
</div>
|
| 311 |
-
|
| 312 |
-
<div class="searchBox">
|
| 313 |
-
<input id="searchInput" placeholder="Ask about blocks or customers..." onkeypress="if(event.key==='Enter')sendChat()">
|
| 314 |
-
<button onclick="sendChat()">Search</button>
|
| 315 |
-
</div>
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
-
|
| 319 |
<script>
|
| 320 |
function markdownToHTML(t){return t.replace(/\\*\\*(.*?)\\*\\*/g,'<strong>$1</strong>').replace(/\\n/g,'<br>');}
|
| 321 |
-
|
| 322 |
async function loadTile(url){
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
html+=j.rows.map(r=>`<tr>${j.columns.map(c=>`<td>${r[c]??''}</td>`).join('')}</tr>`).join('')+'</table></div>';
|
| 334 |
-
if(j.download){html+=`<a class='download' href='${window.location.origin + j.download}' target='_blank'>📥 Download CSV</a>`;}
|
| 335 |
-
}
|
| 336 |
-
section.innerHTML=html;
|
| 337 |
-
chat.appendChild(section);
|
| 338 |
-
chat.scrollTop=chat.scrollHeight;
|
| 339 |
}
|
| 340 |
-
|
| 341 |
async function sendChat(){
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
u.textContent=q;
|
| 350 |
-
chat.appendChild(u);
|
| 351 |
-
document.getElementById('searchInput').value='';
|
| 352 |
-
const thinking=document.createElement('div');
|
| 353 |
-
thinking.className='msg-bot';
|
| 354 |
-
thinking.textContent='Thinking...';
|
| 355 |
-
chat.appendChild(thinking);
|
| 356 |
-
chat.scrollTop=chat.scrollHeight;
|
| 357 |
-
const res=await fetch('/chat_ollama/'+encodeURIComponent(q));
|
| 358 |
-
const j=await res.json();
|
| 359 |
-
thinking.remove();
|
| 360 |
-
const b=document.createElement('div');
|
| 361 |
-
b.className='msg-bot';
|
| 362 |
-
b.innerHTML=markdownToHTML(j.response);
|
| 363 |
-
chat.appendChild(b);
|
| 364 |
-
chat.scrollTop=chat.scrollHeight;
|
| 365 |
}
|
| 366 |
-
|
| 367 |
-
// -------------------------------------------
|
| 368 |
-
// Rotating FAQ Tile Logic
|
| 369 |
-
// -------------------------------------------
|
| 370 |
-
const faqs=[
|
| 371 |
-
"Which block type had the highest backlog last quarter?",
|
| 372 |
-
"Which customers have the most open blocked orders?",
|
| 373 |
-
"How many orders have multiple active blocks?",
|
| 374 |
-
"What is the trend of delivery blocks across quarters?",
|
| 375 |
-
"Which blocks contribute the most to total backlog value?"
|
| 376 |
-
];
|
| 377 |
let faqIndex=0;
|
| 378 |
-
function rotateFAQ(){
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
faqText.textContent=faqs[faqIndex];
|
| 382 |
-
faqIndex=(faqIndex+1)%faqs.length;
|
| 383 |
-
}
|
| 384 |
-
setInterval(rotateFAQ,5000); // Change every 5 seconds
|
| 385 |
-
rotateFAQ();
|
| 386 |
-
function autoAskFAQ(){
|
| 387 |
-
const q=document.getElementById('faqText').textContent;
|
| 388 |
-
document.getElementById('searchInput').value=q;
|
| 389 |
-
sendChat();
|
| 390 |
-
}
|
| 391 |
</script>
|
| 392 |
</body>
|
| 393 |
</html>
|
| 394 |
"""
|
| 395 |
|
| 396 |
-
|
| 397 |
-
@app.route("/")
|
| 398 |
-
def home(): return render_template_string(HTML)
|
| 399 |
# -----------------------------------------------------
|
| 400 |
-
#
|
| 401 |
# -----------------------------------------------------
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
|
|
|
|
|
|
|
|
|
| 404 |
def df_to_csv_response(dataframe, filename):
|
| 405 |
-
"""Convert a DataFrame to downloadable CSV response."""
|
| 406 |
csv_data = dataframe.to_csv(index=False)
|
| 407 |
-
return Response(
|
| 408 |
-
csv_data,
|
| 409 |
-
mimetype="text/csv",
|
| 410 |
-
headers={"Content-Disposition": f"attachment;filename={filename}.csv"}
|
| 411 |
-
)
|
| 412 |
|
| 413 |
@app.route("/download/summary")
|
| 414 |
def download_summary():
|
| 415 |
bs = block_summary_df()
|
| 416 |
if bs.empty:
|
| 417 |
-
return jsonify({"error": "No data
|
| 418 |
return df_to_csv_response(bs, "block_summary")
|
| 419 |
|
| 420 |
@app.route("/download/focus")
|
| 421 |
def download_focus():
|
| 422 |
top2, _ = actionable_focus_df()
|
| 423 |
if top2.empty:
|
| 424 |
-
return jsonify({"error": "No data
|
| 425 |
return df_to_csv_response(top2, "actionable_focus")
|
| 426 |
|
| 427 |
@app.route("/download/multiple")
|
| 428 |
def download_multiple():
|
| 429 |
mb = multiple_block_df()
|
| 430 |
if mb.empty:
|
| 431 |
-
return jsonify({"error": "No data
|
| 432 |
return df_to_csv_response(mb, "multiple_blocks")
|
| 433 |
|
| 434 |
@app.route("/download/quarter")
|
| 435 |
def download_quarter():
|
| 436 |
qdf = quarter_trends_df()
|
| 437 |
if qdf.empty:
|
| 438 |
-
return jsonify({"error": "No data
|
| 439 |
return df_to_csv_response(qdf, "quarter_trends")
|
| 440 |
|
| 441 |
if __name__ == "__main__":
|
| 442 |
app.run(host="0.0.0.0", port=7860)
|
| 443 |
-
|
|
|
|
| 1 |
+
from flask import Flask, render_template_string, jsonify, Response
|
| 2 |
import pandas as pd
|
|
|
|
| 3 |
import os
|
| 4 |
+
from collections import Counter
|
| 5 |
+
from io import BytesIO
|
| 6 |
|
| 7 |
app = Flask(__name__)
|
| 8 |
|
|
|
|
| 92 |
qdf = qdf.sort_values(["YEAR", "QNUM"]).drop(columns=["YEAR", "QNUM"])
|
| 93 |
return qdf
|
| 94 |
|
| 95 |
+
|
| 96 |
+
def get_top_faqs(limit=6):
|
| 97 |
+
"""Read logged queries and return top N most common questions."""
|
| 98 |
+
if not os.path.exists("faq_log.csv"):
|
| 99 |
+
return [
|
| 100 |
+
"Which block type had the highest backlog last quarter?",
|
| 101 |
+
"Which customers have the most open blocked orders?",
|
| 102 |
+
"How many orders have multiple active blocks?",
|
| 103 |
+
"What is the trend of delivery blocks across quarters?",
|
| 104 |
+
"Which blocks contribute the most to total backlog value?"
|
| 105 |
+
]
|
| 106 |
+
with open("faq_log.csv", "r", encoding="utf-8") as f:
|
| 107 |
+
lines = [l.strip() for l in f if l.strip()]
|
| 108 |
+
# keep last 500
|
| 109 |
+
if len(lines) > 500:
|
| 110 |
+
with open("faq_log.csv", "w", encoding="utf-8") as f:
|
| 111 |
+
f.write("\n".join(lines[-500:]))
|
| 112 |
+
common = [q for q, _ in Counter(lines).most_common(limit)]
|
| 113 |
+
return common if common else ["Ask me about blocked orders or quarterly trends!"]
|
| 114 |
+
|
| 115 |
# -----------------------------------------------------
|
| 116 |
# Chatbot Endpoint
|
| 117 |
# -----------------------------------------------------
|
| 118 |
@app.route("/chat_ollama/<query>")
|
| 119 |
def chat_ollama(query):
|
| 120 |
q = query.lower().strip()
|
| 121 |
+
|
| 122 |
+
# Log the user query to file
|
| 123 |
+
try:
|
| 124 |
+
with open("faq_log.csv", "a", encoding="utf-8") as f:
|
| 125 |
+
f.write(q + "\n")
|
| 126 |
+
except Exception as e:
|
| 127 |
+
print("FAQ log write error:", e)
|
| 128 |
+
|
| 129 |
reply = "No relevant insight found. Try asking about customers, block types, or quarterly trends."
|
| 130 |
|
| 131 |
try:
|
|
|
|
| 149 |
main_block = block_counts.index[0] if not block_counts.empty else "general"
|
| 150 |
reply = (
|
| 151 |
f"{cust_name} currently holds the highest backlog value (${value:,.0f}) "
|
| 152 |
+
f"with {main_block} as the main issue. Coordinate cross-functionally to resolve {main_block.lower()} constraints."
|
| 153 |
)
|
| 154 |
|
| 155 |
# --- MULTIPLE BLOCK ORDERS ---
|
|
|
|
| 171 |
top = bs.iloc[0]
|
| 172 |
reply = (
|
| 173 |
f"{top['BLOCK_TYPE']} block represents the largest backlog exposure "
|
| 174 |
+
f"(${top[WORTH_COL]:,.0f}). Engage respective teams to address {top['BLOCK_TYPE'].lower()} issues."
|
| 175 |
)
|
| 176 |
|
| 177 |
# --- QUARTERLY TREND LOGIC ---
|
|
|
|
| 182 |
qdf["QUARTER"].unique(),
|
| 183 |
key=lambda q: (int(q[:4]), int(q[-1]))
|
| 184 |
)
|
| 185 |
+
if quarters:
|
|
|
|
|
|
|
|
|
|
| 186 |
next_quarter = quarters[-1]
|
|
|
|
| 187 |
next_q_df = qdf[qdf["QUARTER"] == next_quarter]
|
| 188 |
if not next_q_df.empty:
|
| 189 |
top_block = next_q_df.sort_values(WORTH_COL, ascending=False).iloc[0]
|
| 190 |
reply = (
|
| 191 |
+
f"In {next_quarter}, {top_block['BLOCK_TYPE']} block shows the highest exposure "
|
| 192 |
+
f"(${top_block[WORTH_COL]:,.0f}). Prepare mitigation plans with owners."
|
| 193 |
)
|
| 194 |
else:
|
| 195 |
+
reply = f"No data found for {next_quarter}."
|
|
|
|
| 196 |
except Exception as e:
|
| 197 |
reply = f"(Error: {e})"
|
| 198 |
|
|
|
|
| 247 |
qdf = quarter_trends_df()
|
| 248 |
if qdf.empty:
|
| 249 |
return jsonify({"columns": [], "rows": [], "insight": "No quarterly data found."})
|
| 250 |
+
next_quarter = qdf["QUARTER"].max()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
next_q_df = qdf[qdf["QUARTER"] == next_quarter]
|
| 252 |
if not next_q_df.empty:
|
| 253 |
top_block = next_q_df.sort_values(WORTH_COL, ascending=False).iloc[0]
|
|
|
|
| 265 |
})
|
| 266 |
|
| 267 |
# -----------------------------------------------------
|
| 268 |
+
# HTML Template with adaptive FAQ placeholder
|
| 269 |
# -----------------------------------------------------
|
|
|
|
| 270 |
HTML = """
|
| 271 |
<!doctype html>
|
| 272 |
<html>
|
|
|
|
| 282 |
.tiles{padding:10px;display:grid;grid-template-columns:1fr 1fr;gap:10px;}
|
| 283 |
.tile{background:#f4f4f4;color:#0A2048;border-radius:10px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,.2);padding:10px;font-weight:600;cursor:pointer;transition:0.2s;}
|
| 284 |
.tile:hover{background:#e2e2e2;}
|
| 285 |
+
#faqTile{background:linear-gradient(135deg,#0A2048,#1c398b);color:white;font-style:italic;grid-column:span 2;}
|
| 286 |
+
#faqTile:hover{background:linear-gradient(135deg,#1a2b60,#334b9a);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
.chat{flex:1;overflow-y:auto;padding:10px;background:#fafafa;}
|
| 288 |
.msg-bot{background:white;border:1px solid #ddd;border-radius:10px;padding:10px;margin-bottom:8px;line-height:1.4;}
|
| 289 |
.table-wrap{overflow-x:auto;overflow-y:auto;max-height:60vh;margin-top:6px;border:1px solid #ddd;border-radius:6px;background:#fff;}
|
|
|
|
| 300 |
</head>
|
| 301 |
<body>
|
| 302 |
<div class="container">
|
| 303 |
+
<iframe title="Midterm Control Tower" src="https://app.powerbi.com/reportEmbed?reportId=c74ac0ec-b0f8-48b9-b7a7-4baab36d9ac5&autoAuth=true&ctid=41f88ecb-ca63-404d-97dd-ab0a169fd138" frameborder="0" allowFullScreen="true" class="dashboard"></iframe>
|
| 304 |
+
<div class="assistant">
|
| 305 |
+
<div class="header">Blocks Assistant</div>
|
| 306 |
+
<div class="tiles">
|
| 307 |
+
<div class="tile" onclick="loadTile('/data/summary')">Overall Block Summary</div>
|
| 308 |
+
<div class="tile" onclick="loadTile('/data/focus_area')">Actionable Focus Area</div>
|
| 309 |
+
<div class="tile" onclick="loadTile('/data/multiple_blocks')">Multiple Block Orders</div>
|
| 310 |
+
<div class="tile" onclick="loadTile('/data/quarter_trends')">Quarter-wise Trends</div>
|
| 311 |
+
<div class="tile" id="faqTile" onclick="autoAskFAQ()">💡 FAQ: <span id="faqText"></span></div>
|
| 312 |
+
</div>
|
| 313 |
+
<div id="chat" class="chat">
|
| 314 |
+
<div class="msg-bot"><b>Hello!</b><br>I'm your Blocks Assistant. How can I help you today?</div>
|
| 315 |
+
</div>
|
| 316 |
+
<div class="searchBox">
|
| 317 |
+
<input id="searchInput" placeholder="Ask about blocks or customers..." onkeypress="if(event.key==='Enter')sendChat()">
|
| 318 |
+
<button onclick="sendChat()">Search</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
+
</div>
|
| 322 |
<script>
|
| 323 |
function markdownToHTML(t){return t.replace(/\\*\\*(.*?)\\*\\*/g,'<strong>$1</strong>').replace(/\\n/g,'<br>');}
|
|
|
|
| 324 |
async function loadTile(url){
|
| 325 |
+
const res=await fetch(url);const j=await res.json();
|
| 326 |
+
const chat=document.getElementById('chat');
|
| 327 |
+
const section=document.createElement('div');section.className='msg-bot';section.style.borderTop='2px solid #eee';section.style.marginTop='10px';
|
| 328 |
+
let html=`<p>${j.insight||''}</p>`;
|
| 329 |
+
if(j.columns?.length){
|
| 330 |
+
html+=`<div class='table-wrap'><table><tr>${j.columns.map(c=>`<th>${c}</th>`).join('')}</tr>`;
|
| 331 |
+
html+=j.rows.map(r=>`<tr>${j.columns.map(c=>`<td>${r[c]??''}</td>`).join('')}</tr>`).join('')+'</table></div>';
|
| 332 |
+
if(j.download){html+=`<a class='download' href='${window.location.origin + j.download}' target='_blank'>📥 Download CSV</a>`;}
|
| 333 |
+
}
|
| 334 |
+
section.innerHTML=html;chat.appendChild(section);chat.scrollTop=chat.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
}
|
|
|
|
| 336 |
async function sendChat(){
|
| 337 |
+
const q=document.getElementById('searchInput').value.trim();if(!q)return;
|
| 338 |
+
const chat=document.getElementById('chat');
|
| 339 |
+
const u=document.createElement('div');u.className='msg-bot';u.style.background='#0A2048';u.style.color='white';u.textContent=q;chat.appendChild(u);
|
| 340 |
+
document.getElementById('searchInput').value='';
|
| 341 |
+
const thinking=document.createElement('div');thinking.className='msg-bot';thinking.textContent='Thinking...';chat.appendChild(thinking);chat.scrollTop=chat.scrollHeight;
|
| 342 |
+
const res=await fetch('/chat_ollama/'+encodeURIComponent(q));const j=await res.json();thinking.remove();
|
| 343 |
+
const b=document.createElement('div');b.className='msg-bot';b.innerHTML=markdownToHTML(j.response);chat.appendChild(b);chat.scrollTop=chat.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
+
const faqs=[PLACEHOLDER_FAQS];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
let faqIndex=0;
|
| 347 |
+
function rotateFAQ(){const faqText=document.getElementById('faqText');if(!faqText)return;faqText.textContent=faqs[faqIndex];faqIndex=(faqIndex+1)%faqs.length;}
|
| 348 |
+
setInterval(rotateFAQ,5000);rotateFAQ();
|
| 349 |
+
function autoAskFAQ(){const q=document.getElementById('faqText').textContent;document.getElementById('searchInput').value=q;sendChat();}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
</script>
|
| 351 |
</body>
|
| 352 |
</html>
|
| 353 |
"""
|
| 354 |
|
|
|
|
|
|
|
|
|
|
| 355 |
# -----------------------------------------------------
|
| 356 |
+
# Dynamic injection of top FAQs
|
| 357 |
# -----------------------------------------------------
|
| 358 |
+
@app.route("/")
|
| 359 |
+
def home():
|
| 360 |
+
faqs = get_top_faqs()
|
| 361 |
+
faq_js_array = "[" + ",".join([f'"{f}"' for f in faqs]) + "]"
|
| 362 |
+
html_with_faqs = HTML.replace("const faqs=[PLACEHOLDER_FAQS];", f"const faqs={faq_js_array};")
|
| 363 |
+
return render_template_string(html_with_faqs)
|
| 364 |
|
| 365 |
+
# -----------------------------------------------------
|
| 366 |
+
# CSV Download Endpoints
|
| 367 |
+
# -----------------------------------------------------
|
| 368 |
def df_to_csv_response(dataframe, filename):
|
|
|
|
| 369 |
csv_data = dataframe.to_csv(index=False)
|
| 370 |
+
return Response(csv_data, mimetype="text/csv", headers={"Content-Disposition": f"attachment;filename={filename}.csv"})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
@app.route("/download/summary")
|
| 373 |
def download_summary():
|
| 374 |
bs = block_summary_df()
|
| 375 |
if bs.empty:
|
| 376 |
+
return jsonify({"error": "No data"}), 404
|
| 377 |
return df_to_csv_response(bs, "block_summary")
|
| 378 |
|
| 379 |
@app.route("/download/focus")
|
| 380 |
def download_focus():
|
| 381 |
top2, _ = actionable_focus_df()
|
| 382 |
if top2.empty:
|
| 383 |
+
return jsonify({"error": "No data"}), 404
|
| 384 |
return df_to_csv_response(top2, "actionable_focus")
|
| 385 |
|
| 386 |
@app.route("/download/multiple")
|
| 387 |
def download_multiple():
|
| 388 |
mb = multiple_block_df()
|
| 389 |
if mb.empty:
|
| 390 |
+
return jsonify({"error": "No data"}), 404
|
| 391 |
return df_to_csv_response(mb, "multiple_blocks")
|
| 392 |
|
| 393 |
@app.route("/download/quarter")
|
| 394 |
def download_quarter():
|
| 395 |
qdf = quarter_trends_df()
|
| 396 |
if qdf.empty:
|
| 397 |
+
return jsonify({"error": "No data"}), 404
|
| 398 |
return df_to_csv_response(qdf, "quarter_trends")
|
| 399 |
|
| 400 |
if __name__ == "__main__":
|
| 401 |
app.run(host="0.0.0.0", port=7860)
|
|
|