Shriya07 commited on
Commit
90193de
·
verified ·
1 Parent(s): c366030
Files changed (1) hide show
  1. app.py +94 -136
app.py CHANGED
@@ -1,7 +1,8 @@
1
- from flask import Flask, render_template_string, jsonify, send_file
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 and improve throughput."
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 and accelerate order release."
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 not quarters:
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"Upcoming {next_quarter}: {top_block['BLOCK_TYPE']} block shows highest exposure "
167
- f"(${top_block[WORTH_COL]:,.0f}). Prepare mitigation plans with {top_block['BLOCK_TYPE'].lower()} owners."
168
  )
169
  else:
170
- reply = f"No data found for next quarter ({next_quarter})."
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
- quarters = sorted(
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 (unchanged layout)
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
- background:linear-gradient(135deg,#0A2048,#1c398b);
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
- <iframe title="Midterm Control Tower" width="1140" height="541.25"
293
- src="https://app.powerbi.com/reportEmbed?reportId=c74ac0ec-b0f8-48b9-b7a7-4baab36d9ac5&autoAuth=true&ctid=41f88ecb-ca63-404d-97dd-ab0a169fd138"
294
- frameborder="0" allowFullScreen="true"></iframe>
295
-
296
- <div class="assistant">
297
- <div class="header">Blocks Assistant</div>
298
-
299
- <div class="tiles">
300
- <div class="tile" onclick="loadTile('/data/summary')">Overall Block Summary</div>
301
- <div class="tile" onclick="loadTile('/data/focus_area')">Actionable Focus Area</div>
302
- <div class="tile" onclick="loadTile('/data/multiple_blocks')">Multiple Block Orders</div>
303
- <div class="tile" onclick="loadTile('/data/quarter_trends')">Quarter-wise Trends</div>
304
- <!-- New dynamic FAQ tile -->
305
- <div class="tile" id="faqTile" onclick="autoAskFAQ()">💡 FAQ: <span id="faqText"></span></div>
306
- </div>
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
- const res=await fetch(url);
324
- const j=await res.json();
325
- const chat=document.getElementById('chat');
326
- const section=document.createElement('div');
327
- section.className='msg-bot';
328
- section.style.borderTop='2px solid #eee';
329
- section.style.marginTop='10px';
330
- let html=`<p>${j.insight||''}</p>`;
331
- if(j.columns?.length){
332
- html+=`<div class='table-wrap'><table><tr>${j.columns.map(c=>`<th>${c}</th>`).join('')}</tr>`;
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
- const q=document.getElementById('searchInput').value.trim();
343
- if(!q)return;
344
- const chat=document.getElementById('chat');
345
- const u=document.createElement('div');
346
- u.className='msg-bot';
347
- u.style.background='#0A2048';
348
- u.style.color='white';
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
- const faqText=document.getElementById('faqText');
380
- if(!faqText)return;
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
- # Download Endpoints (CSV export)
401
  # -----------------------------------------------------
402
- from flask import Response
 
 
 
 
 
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 available"}), 404
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 available"}), 404
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 available"}), 404
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 available"}), 404
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)