Suryasticsai commited on
Commit
ea2f815
Β·
verified Β·
1 Parent(s): 9b0cacb

Update app.py

Browse files

Added bg color Switch & footer

Files changed (1) hide show
  1. app.py +154 -64
app.py CHANGED
@@ -101,14 +101,12 @@ def safe_json_loads(text):
101
  return json.loads(text)
102
  except json.JSONDecodeError:
103
  pass
104
- # Try extracting from markdown code blocks
105
  match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL)
106
  if match:
107
  try:
108
  return json.loads(match.group(1).strip())
109
  except:
110
  pass
111
- # Try finding the outermost JSON object
112
  start = text.find("{")
113
  end = text.rfind("}")
114
  if start != -1 and end != -1 and end > start:
@@ -122,7 +120,6 @@ def safe_json_loads(text):
122
  def parse_transcript_lines(text):
123
  """Parse raw transcript into structured lines."""
124
  lines = []
125
- # Pattern: [Time] Name: Message
126
  pattern = r"\[?(\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?)\]?\s*([A-Za-z\s]+?):\s*(.+)"
127
  for raw_line in text.strip().split("\n"):
128
  raw_line = raw_line.strip()
@@ -133,7 +130,6 @@ def parse_transcript_lines(text):
133
  time, user, msg = m.groups()
134
  lines.append({"time": time.strip(), "user": user.strip(), "text": msg.strip()})
135
  else:
136
- # Fallback: anything with a colon
137
  m2 = re.match(r"([A-Za-z\s]+?):\s*(.+)", raw_line)
138
  if m2:
139
  user, msg = m2.groups()
@@ -143,19 +139,16 @@ def parse_transcript_lines(text):
143
  return lines
144
 
145
  def chunk_lines(lines, max_per_chunk=35):
146
- """Yield chunks of lines."""
147
  for i in range(0, len(lines), max_per_chunk):
148
  yield lines[i:i+max_per_chunk]
149
 
150
  def lines_to_text(lines):
151
- """Convert structured lines back to transcript text."""
152
  return "\n".join([
153
  f"[{l.get('time','')}] {l['user']}: {l['text']}" for l in lines
154
  ])
155
 
156
  # --- Analysis Core ---
157
  def analyze_chunk(chunk_text, context, progress=None):
158
- """Analyze a single chunk via Groq."""
159
  try:
160
  response = groq_client.chat.completions.create(
161
  model="llama-3.1-8b-instant",
@@ -165,7 +158,7 @@ def analyze_chunk(chunk_text, context, progress=None):
165
  ],
166
  temperature=0.1,
167
  response_format={"type": "json_object"},
168
- max_tokens=4096 # CRITICAL: prevents truncation on long outputs
169
  )
170
  raw = response.choices[0].message.content
171
  return safe_json_loads(raw)
@@ -174,7 +167,6 @@ def analyze_chunk(chunk_text, context, progress=None):
174
  return None
175
 
176
  def merge_analyses(results):
177
- """Merge multiple chunk analyses into one coherent result."""
178
  if not results:
179
  return None
180
  if len(results) == 1:
@@ -195,7 +187,6 @@ def merge_analyses(results):
195
  for r in results:
196
  if not isinstance(r, dict):
197
  continue
198
-
199
  if isinstance(r.get("chain"), list):
200
  merged["chain"].extend(r["chain"])
201
  if isinstance(r.get("actions"), list):
@@ -206,19 +197,15 @@ def merge_analyses(results):
206
  merged["risks"].extend(r["risks"])
207
  if isinstance(r.get("topics"), list):
208
  merged["topics"].extend(r["topics"])
209
-
210
- # Keep first non-empty role assignment
211
  for rk in ["decision_maker", "facilitator", "dev_lead", "qa_lead"]:
212
  val = r.get("roles", {}).get(rk, "")
213
  if val and not merged["roles"].get(rk):
214
  merged["roles"][rk] = val
215
-
216
  h = r.get("stats", {}).get("health", 75)
217
  health_scores.append(h)
218
  if r.get("health_status"):
219
  merged["health_status"] = r["health_status"]
220
 
221
- # Deduplicate chain by user+text
222
  seen = set()
223
  unique_chain = []
224
  for msg in merged["chain"]:
@@ -227,21 +214,15 @@ def merge_analyses(results):
227
  seen.add(key)
228
  unique_chain.append(msg)
229
  merged["chain"] = unique_chain
230
-
231
- # Deduplicate others
232
  merged["topics"] = list(dict.fromkeys(merged["topics"]))
233
  merged["risks"] = list(dict.fromkeys(merged["risks"]))
234
-
235
- # Recalculate stats
236
  merged["stats"]["messages"] = len(unique_chain)
237
  merged["stats"]["actions"] = len(merged["actions"])
238
  merged["stats"]["decisions"] = len(merged["decisions"])
239
  merged["stats"]["health"] = int(sum(health_scores)/len(health_scores)) if health_scores else 75
240
-
241
  return merged
242
 
243
  def apply_fallbacks(data):
244
- """Ensure all expected keys exist with safe defaults."""
245
  data.setdefault("stats", {"messages": 0, "actions": 0, "decisions": 0, "health": 75})
246
  data.setdefault("roles", {})
247
  data.setdefault("chain", [])
@@ -250,50 +231,48 @@ def apply_fallbacks(data):
250
  data.setdefault("actions", [])
251
  data.setdefault("decisions", [])
252
  data.setdefault("risks", [])
253
-
254
  data["roles"].setdefault("decision_maker", "")
255
  data["roles"].setdefault("facilitator", "")
256
  data["roles"].setdefault("dev_lead", "")
257
  data["roles"].setdefault("qa_lead", "")
258
-
259
- # Sync stats to actual array lengths
260
  data["stats"]["messages"] = len(data["chain"])
261
  data["stats"]["actions"] = len(data["actions"])
262
  data["stats"]["decisions"] = len(data["decisions"])
263
  return data
264
 
265
- # --- Dashboard Rendering ---
266
  def render_dashboard(data):
267
  css = """
268
  <style>
269
- .sl-container { font-family: 'Inter', sans-serif; background: #0b0f1a; color: #f1f5f9; padding: 20px; border-radius: 12px; }
270
- .sl-stats { display: flex; gap: 15px; margin-bottom: 25px; }
271
- .sl-stat-card { flex: 1; background: #111827; border: 1px solid #1e293b; padding: 15px; border-radius: 10px; text-align: center; }
272
- .sl-stat-val { font-size: 1.5rem; font-weight: 700; color: #00d4aa; }
273
- .sl-stat-label { font-size: 0.7rem; text-transform: uppercase; color: #64748b; margin-top: 5px; }
274
- .sl-section-title { font-size: 0.8rem; font-weight: 700; color: #8b5cf6; text-transform: uppercase; margin-bottom: 15px; display: flex; align-items: center; gap: 8px; }
275
  .sl-chain { display: flex; flex-direction: column; gap: 15px; position: relative; padding-left: 20px; }
276
- .sl-chain::before { content: ''; position: absolute; left: 35px; top: 0; bottom: 0; width: 2px; background: #1e293b; z-index: 0; }
277
  .sl-msg { display: flex; gap: 15px; position: relative; z-index: 1; }
278
- .sl-avatar { width: 36px; height: 36px; border-radius: 50%; background: #1e293b; display: flex; align-items: center; justify-content: center; font-weight: 700; border: 2px solid #00d4aa; font-size: 0.8rem; flex-shrink: 0; }
279
- .sl-msg-content { background: #111827; border: 1px solid #1e293b; padding: 12px 16px; border-radius: 10px; flex-grow: 1; }
280
  .sl-msg-header { display: flex; justify-content: space-between; margin-bottom: 5px; }
281
- .sl-msg-user { font-weight: 700; font-size: 0.9rem; }
282
- .sl-msg-role { font-size: 0.7rem; color: #64748b; font-weight: 400; margin-left: 5px; }
283
- .sl-msg-time { font-size: 0.7rem; color: #475569; }
284
- .sl-msg-text { font-size: 0.85rem; line-height: 1.5; color: #cbd5e1; }
285
  .sl-tag { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; font-weight: 700; margin-top: 8px; text-transform: uppercase; }
286
- .tag-decision { background: rgba(0, 212, 170, 0.1); color: #00d4aa; border: 1px solid rgba(0, 212, 170, 0.2); }
287
- .tag-action { background: rgba(59, 130, 246, 0.1); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.2); }
288
- .tag-risk { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
289
- .tag-blocker { background: rgba(245, 158, 11, 0.1); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.2); }
290
- .tag-idea { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; border: 1px solid rgba(139, 92, 246, 0.2); }
291
  .sl-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-top: 25px; }
292
- .sl-grid-item { background: #111827; border: 1px solid #1e293b; padding: 15px; border-radius: 10px; }
293
- .sl-list-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; font-size: 0.8rem; }
 
294
  .sl-list-bullet { width: 16px; height: 16px; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; }
295
- .health-bar { height: 8px; background: #1e293b; border-radius: 4px; overflow: hidden; margin: 10px 0; }
296
- .health-fill { height: 100%; background: linear-gradient(90deg, #ef4444, #f59e0b, #00d4aa); }
297
  </style>
298
  """
299
  stats_html = f'<div class="sl-stats"><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["messages"]}</div><div class="sl-stat-label">Messages</div></div><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["actions"]}</div><div class="sl-stat-label">Actions</div></div><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["decisions"]}</div><div class="sl-stat-label">Decisions</div></div><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["health"]}%</div><div class="sl-stat-label">Health</div></div></div>'
@@ -308,32 +287,32 @@ def render_dashboard(data):
308
  chain_html += "</div>"
309
 
310
  topics_html = '<div style="margin-top:20px"><div class="sl-section-title">Topics</div><div style="display:flex;gap:10px;flex-wrap:wrap">'
311
- for t in data['topics']: topics_html += f'<span style="background:#1e293b;padding:4px 12px;border-radius:20px;font-size:0.75rem"># {t}</span>'
312
  topics_html += "</div></div>"
313
 
314
- health_html = f'<div style="margin-top:20px"><div class="sl-section-title">Sprint Health</div><div class="health-bar"><div class="health-fill" style="width:{data["stats"]["health"]}%"></div></div><div style="font-size:0.8rem;color:#00d4aa">βœ“ {data["health_status"]}</div></div>'
315
 
316
  actions_html = '<div class="sl-grid-item"><div class="sl-section-title">Actions</div>'
317
  for a in data['actions']:
318
  owner = a.get("owner", "Unassigned")
319
- actions_html += f'<div class="sl-list-item"><div class="sl-list-bullet" style="background:rgba(59,130,246,0.2);color:#3b82f6">⚑</div><div>{a["text"]} <span style="color:#64748b;font-size:0.7rem">@{owner}</span></div></div>'
320
  if not data['actions']:
321
- actions_html += '<div style="color:#64748b;font-size:0.8rem">No actions detected</div>'
322
  actions_html += '</div>'
323
 
324
  decisions_html = '<div class="sl-grid-item"><div class="sl-section-title">Decisions</div>'
325
  for d in data['decisions']:
326
  owner = d.get("owner", "Unassigned")
327
- decisions_html += f'<div class="sl-list-item"><div class="sl-list-bullet" style="background:rgba(0,212,170,0.2);color:#00d4aa">πŸ“Œ</div><div>{d["text"]} <span style="color:#64748b;font-size:0.7rem">@{owner}</span></div></div>'
328
  if not data['decisions']:
329
- decisions_html += '<div style="color:#64748b;font-size:0.8rem">No decisions detected</div>'
330
  decisions_html += '</div>'
331
 
332
  risks_html = '<div class="sl-grid-item"><div class="sl-section-title">Risks & Blockers</div>'
333
  for r in data['risks']:
334
- risks_html += f'<div class="sl-list-item"><div class="sl-list-bullet" style="background:rgba(239,68,68,0.2);color:#ef4444">🚩</div><div>{r}</div></div>'
335
  if not data['risks']:
336
- risks_html += '<div style="color:#64748b;font-size:0.8rem">No risks detected</div>'
337
  risks_html += '</div>'
338
 
339
  return f'<div class="sl-container">{css}{stats_html}{chain_html}{topics_html}{health_html}<div class="sl-grid">{actions_html}{decisions_html}{risks_html}</div></div>'
@@ -347,11 +326,10 @@ def analyze_conversation(text, progress=gr.Progress()):
347
  if not lines:
348
  return (
349
  None,
350
- "<div style='padding:20px;background:#111827;color:#ef4444;border-radius:10px'><h3>Parse Error</h3><p>Could not parse transcript lines.</p></div>",
351
  "", "", "", ""
352
  )
353
 
354
- # Query Chroma with a sample (not the full huge text)
355
  query_sample = text[:800]
356
  try:
357
  results = collection.query(query_texts=[query_sample], n_results=3)
@@ -360,7 +338,6 @@ def analyze_conversation(text, progress=gr.Progress()):
360
  context = ""
361
  print(f"Chroma query error: {e}")
362
 
363
- # Chunked analysis
364
  all_results = []
365
  chunks = list(chunk_lines(lines, max_per_chunk=35))
366
  total_chunks = len(chunks)
@@ -376,10 +353,10 @@ def analyze_conversation(text, progress=gr.Progress()):
376
  if not all_results:
377
  return (
378
  None,
379
- """<div style='padding:20px;background:#111827;color:#ef4444;border-radius:10px'>
380
  <h3>Analysis Failed</h3>
381
  <p>Could not analyze the conversation. The input may be too long, malformed, or the API is unavailable.</p>
382
- <p style="color:#94a3b8">Tip: Try a shorter segment or verify your GROQ_API_KEY.</p>
383
  </div>""",
384
  "", "", "", ""
385
  )
@@ -404,14 +381,114 @@ def export_report(data_state):
404
  html_content = render_dashboard(data_state)
405
  filename = f"scrumlens_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
406
  with open(filename, "w", encoding="utf-8") as f:
407
- f.write(f"<html><head><meta charset='utf-8'><title>ScrumLens Report</title></head><body style='background:#0b0f1a;padding:40px'>{html_content}</body></html>")
 
 
 
 
 
 
408
  return filename
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  # --- Gradio UI ---
411
- with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {background-color: #0b0f1a; color: white;}") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  data_state = gr.State()
413
- with gr.Row():
414
- gr.Markdown("# πŸ” ScrumLens v0.5\n### CHAOS 2 CLARITY | Now with long-input support")
 
 
 
415
  with gr.Row():
416
  with gr.Column(scale=4):
417
  input_text = gr.Textbox(
@@ -421,7 +498,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {background-color:
421
  )
422
  with gr.Row():
423
  analyze_btn = gr.Button("πŸ” Analyze", variant="primary")
424
- export_btn = gr.Button("πŸ“„ Export HTML Report")
425
  with gr.Column(scale=2):
426
  gr.Markdown("● TEAM ROLES & FOCUS")
427
  with gr.Row():
@@ -434,6 +511,9 @@ with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {background-color:
434
  output_html = gr.HTML(label="Analysis Results")
435
  export_file = gr.File(label="Download Report")
436
 
 
 
 
437
  analyze_btn.click(
438
  fn=analyze_conversation,
439
  inputs=[input_text],
@@ -445,5 +525,15 @@ with gr.Blocks(theme=gr.themes.Soft(), css=".gradio-container {background-color:
445
  outputs=[export_file]
446
  )
447
 
 
 
 
 
 
 
 
 
 
 
448
  if __name__ == "__main__":
449
  demo.launch()
 
101
  return json.loads(text)
102
  except json.JSONDecodeError:
103
  pass
 
104
  match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL)
105
  if match:
106
  try:
107
  return json.loads(match.group(1).strip())
108
  except:
109
  pass
 
110
  start = text.find("{")
111
  end = text.rfind("}")
112
  if start != -1 and end != -1 and end > start:
 
120
  def parse_transcript_lines(text):
121
  """Parse raw transcript into structured lines."""
122
  lines = []
 
123
  pattern = r"\[?(\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?)\]?\s*([A-Za-z\s]+?):\s*(.+)"
124
  for raw_line in text.strip().split("\n"):
125
  raw_line = raw_line.strip()
 
130
  time, user, msg = m.groups()
131
  lines.append({"time": time.strip(), "user": user.strip(), "text": msg.strip()})
132
  else:
 
133
  m2 = re.match(r"([A-Za-z\s]+?):\s*(.+)", raw_line)
134
  if m2:
135
  user, msg = m2.groups()
 
139
  return lines
140
 
141
  def chunk_lines(lines, max_per_chunk=35):
 
142
  for i in range(0, len(lines), max_per_chunk):
143
  yield lines[i:i+max_per_chunk]
144
 
145
  def lines_to_text(lines):
 
146
  return "\n".join([
147
  f"[{l.get('time','')}] {l['user']}: {l['text']}" for l in lines
148
  ])
149
 
150
  # --- Analysis Core ---
151
  def analyze_chunk(chunk_text, context, progress=None):
 
152
  try:
153
  response = groq_client.chat.completions.create(
154
  model="llama-3.1-8b-instant",
 
158
  ],
159
  temperature=0.1,
160
  response_format={"type": "json_object"},
161
+ max_tokens=4096
162
  )
163
  raw = response.choices[0].message.content
164
  return safe_json_loads(raw)
 
167
  return None
168
 
169
  def merge_analyses(results):
 
170
  if not results:
171
  return None
172
  if len(results) == 1:
 
187
  for r in results:
188
  if not isinstance(r, dict):
189
  continue
 
190
  if isinstance(r.get("chain"), list):
191
  merged["chain"].extend(r["chain"])
192
  if isinstance(r.get("actions"), list):
 
197
  merged["risks"].extend(r["risks"])
198
  if isinstance(r.get("topics"), list):
199
  merged["topics"].extend(r["topics"])
 
 
200
  for rk in ["decision_maker", "facilitator", "dev_lead", "qa_lead"]:
201
  val = r.get("roles", {}).get(rk, "")
202
  if val and not merged["roles"].get(rk):
203
  merged["roles"][rk] = val
 
204
  h = r.get("stats", {}).get("health", 75)
205
  health_scores.append(h)
206
  if r.get("health_status"):
207
  merged["health_status"] = r["health_status"]
208
 
 
209
  seen = set()
210
  unique_chain = []
211
  for msg in merged["chain"]:
 
214
  seen.add(key)
215
  unique_chain.append(msg)
216
  merged["chain"] = unique_chain
 
 
217
  merged["topics"] = list(dict.fromkeys(merged["topics"]))
218
  merged["risks"] = list(dict.fromkeys(merged["risks"]))
 
 
219
  merged["stats"]["messages"] = len(unique_chain)
220
  merged["stats"]["actions"] = len(merged["actions"])
221
  merged["stats"]["decisions"] = len(merged["decisions"])
222
  merged["stats"]["health"] = int(sum(health_scores)/len(health_scores)) if health_scores else 75
 
223
  return merged
224
 
225
  def apply_fallbacks(data):
 
226
  data.setdefault("stats", {"messages": 0, "actions": 0, "decisions": 0, "health": 75})
227
  data.setdefault("roles", {})
228
  data.setdefault("chain", [])
 
231
  data.setdefault("actions", [])
232
  data.setdefault("decisions", [])
233
  data.setdefault("risks", [])
 
234
  data["roles"].setdefault("decision_maker", "")
235
  data["roles"].setdefault("facilitator", "")
236
  data["roles"].setdefault("dev_lead", "")
237
  data["roles"].setdefault("qa_lead", "")
 
 
238
  data["stats"]["messages"] = len(data["chain"])
239
  data["stats"]["actions"] = len(data["actions"])
240
  data["stats"]["decisions"] = len(data["decisions"])
241
  return data
242
 
243
+ # --- Dashboard Rendering (uses CSS variables for theme support) ---
244
  def render_dashboard(data):
245
  css = """
246
  <style>
247
+ .sl-container { font-family: 'Inter', sans-serif; background: var(--sl-bg); color: var(--sl-text); padding: 20px; border-radius: 12px; transition: all 0.3s ease; }
248
+ .sl-stats { display: flex; gap: 15px; margin-bottom: 25px; flex-wrap: wrap; }
249
+ .sl-stat-card { flex: 1; min-width: 120px; background: var(--sl-card); border: 1px solid var(--sl-border); padding: 15px; border-radius: 10px; text-align: center; }
250
+ .sl-stat-val { font-size: 1.5rem; font-weight: 700; color: var(--sl-accent); }
251
+ .sl-stat-label { font-size: 0.7rem; text-transform: uppercase; color: var(--sl-muted); margin-top: 5px; }
252
+ .sl-section-title { font-size: 0.8rem; font-weight: 700; color: var(--sl-purple); text-transform: uppercase; margin-bottom: 15px; display: flex; align-items: center; gap: 8px; }
253
  .sl-chain { display: flex; flex-direction: column; gap: 15px; position: relative; padding-left: 20px; }
254
+ .sl-chain::before { content: ''; position: absolute; left: 35px; top: 0; bottom: 0; width: 2px; background: var(--sl-border); z-index: 0; }
255
  .sl-msg { display: flex; gap: 15px; position: relative; z-index: 1; }
256
+ .sl-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--sl-card); display: flex; align-items: center; justify-content: center; font-weight: 700; border: 2px solid var(--sl-accent); color: var(--sl-text); font-size: 0.8rem; flex-shrink: 0; }
257
+ .sl-msg-content { background: var(--sl-card); border: 1px solid var(--sl-border); padding: 12px 16px; border-radius: 10px; flex-grow: 1; }
258
  .sl-msg-header { display: flex; justify-content: space-between; margin-bottom: 5px; }
259
+ .sl-msg-user { font-weight: 700; font-size: 0.9rem; color: var(--sl-text); }
260
+ .sl-msg-role { font-size: 0.7rem; color: var(--sl-muted); font-weight: 400; margin-left: 5px; }
261
+ .sl-msg-time { font-size: 0.7rem; color: var(--sl-muted); }
262
+ .sl-msg-text { font-size: 0.85rem; line-height: 1.5; color: var(--sl-text-sec); }
263
  .sl-tag { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; font-weight: 700; margin-top: 8px; text-transform: uppercase; }
264
+ .tag-decision { background: rgba(0, 212, 170, 0.1); color: var(--sl-accent); border: 1px solid rgba(0, 212, 170, 0.2); }
265
+ .tag-action { background: rgba(59, 130, 246, 0.1); color: var(--sl-blue); border: 1px solid rgba(59, 130, 246, 0.2); }
266
+ .tag-risk { background: rgba(239, 68, 68, 0.1); color: var(--sl-red); border: 1px solid rgba(239, 68, 68, 0.2); }
267
+ .tag-blocker { background: rgba(245, 158, 11, 0.1); color: var(--sl-orange); border: 1px solid rgba(245, 158, 11, 0.2); }
268
+ .tag-idea { background: rgba(139, 92, 246, 0.1); color: var(--sl-purple); border: 1px solid rgba(139, 92, 246, 0.2); }
269
  .sl-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-top: 25px; }
270
+ @media (max-width: 768px) { .sl-grid { grid-template-columns: 1fr; } }
271
+ .sl-grid-item { background: var(--sl-card); border: 1px solid var(--sl-border); padding: 15px; border-radius: 10px; }
272
+ .sl-list-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; font-size: 0.8rem; color: var(--sl-text-sec); }
273
  .sl-list-bullet { width: 16px; height: 16px; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; }
274
+ .health-bar { height: 8px; background: var(--sl-border); border-radius: 4px; overflow: hidden; margin: 10px 0; }
275
+ .health-fill { height: 100%; background: linear-gradient(90deg, var(--sl-red), var(--sl-orange), var(--sl-accent)); }
276
  </style>
277
  """
278
  stats_html = f'<div class="sl-stats"><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["messages"]}</div><div class="sl-stat-label">Messages</div></div><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["actions"]}</div><div class="sl-stat-label">Actions</div></div><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["decisions"]}</div><div class="sl-stat-label">Decisions</div></div><div class="sl-stat-card"><div class="sl-stat-val">{data["stats"]["health"]}%</div><div class="sl-stat-label">Health</div></div></div>'
 
287
  chain_html += "</div>"
288
 
289
  topics_html = '<div style="margin-top:20px"><div class="sl-section-title">Topics</div><div style="display:flex;gap:10px;flex-wrap:wrap">'
290
+ for t in data['topics']: topics_html += f'<span style="background:var(--sl-card);border:1px solid var(--sl-border);padding:4px 12px;border-radius:20px;font-size:0.75rem;color:var(--sl-text-sec)"># {t}</span>'
291
  topics_html += "</div></div>"
292
 
293
+ health_html = f'<div style="margin-top:20px"><div class="sl-section-title">Sprint Health</div><div class="health-bar"><div class="health-fill" style="width:{data["stats"]["health"]}%"></div></div><div style="font-size:0.8rem;color:var(--sl-accent)">βœ“ {data["health_status"]}</div></div>'
294
 
295
  actions_html = '<div class="sl-grid-item"><div class="sl-section-title">Actions</div>'
296
  for a in data['actions']:
297
  owner = a.get("owner", "Unassigned")
298
+ actions_html += f'<div class="sl-list-item"><div class="sl-list-bullet" style="background:rgba(59,130,246,0.2);color:var(--sl-blue)">⚑</div><div>{a["text"]} <span style="color:var(--sl-muted);font-size:0.7rem">@{owner}</span></div></div>'
299
  if not data['actions']:
300
+ actions_html += '<div style="color:var(--sl-muted);font-size:0.8rem">No actions detected</div>'
301
  actions_html += '</div>'
302
 
303
  decisions_html = '<div class="sl-grid-item"><div class="sl-section-title">Decisions</div>'
304
  for d in data['decisions']:
305
  owner = d.get("owner", "Unassigned")
306
+ decisions_html += f'<div class="sl-list-item"><div class="sl-list-bullet" style="background:rgba(0,212,170,0.2);color:var(--sl-accent)">πŸ“Œ</div><div>{d["text"]} <span style="color:var(--sl-muted);font-size:0.7rem">@{owner}</span></div></div>'
307
  if not data['decisions']:
308
+ decisions_html += '<div style="color:var(--sl-muted);font-size:0.8rem">No decisions detected</div>'
309
  decisions_html += '</div>'
310
 
311
  risks_html = '<div class="sl-grid-item"><div class="sl-section-title">Risks & Blockers</div>'
312
  for r in data['risks']:
313
+ risks_html += f'<div class="sl-list-item"><div class="sl-list-bullet" style="background:rgba(239,68,68,0.2);color:var(--sl-red)">🚩</div><div>{r}</div></div>'
314
  if not data['risks']:
315
+ risks_html += '<div style="color:var(--sl-muted);font-size:0.8rem">No risks detected</div>'
316
  risks_html += '</div>'
317
 
318
  return f'<div class="sl-container">{css}{stats_html}{chain_html}{topics_html}{health_html}<div class="sl-grid">{actions_html}{decisions_html}{risks_html}</div></div>'
 
326
  if not lines:
327
  return (
328
  None,
329
+ "<div style='padding:20px;background:var(--sl-card);color:var(--sl-red);border-radius:10px'><h3>Parse Error</h3><p>Could not parse transcript lines.</p></div>",
330
  "", "", "", ""
331
  )
332
 
 
333
  query_sample = text[:800]
334
  try:
335
  results = collection.query(query_texts=[query_sample], n_results=3)
 
338
  context = ""
339
  print(f"Chroma query error: {e}")
340
 
 
341
  all_results = []
342
  chunks = list(chunk_lines(lines, max_per_chunk=35))
343
  total_chunks = len(chunks)
 
353
  if not all_results:
354
  return (
355
  None,
356
+ """<div style='padding:20px;background:var(--sl-card);color:var(--sl-red);border-radius:10px'>
357
  <h3>Analysis Failed</h3>
358
  <p>Could not analyze the conversation. The input may be too long, malformed, or the API is unavailable.</p>
359
+ <p style="color:var(--sl-muted)">Tip: Try a shorter segment or verify your GROQ_API_KEY.</p>
360
  </div>""",
361
  "", "", "", ""
362
  )
 
381
  html_content = render_dashboard(data_state)
382
  filename = f"scrumlens_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
383
  with open(filename, "w", encoding="utf-8") as f:
384
+ f.write(f"""<html>
385
+ <head><meta charset='utf-8'><title>ScrumLens Report</title>
386
+ <style>
387
+ :root {{ --sl-bg: #0b0f1a; --sl-card: #111827; --sl-border: #1e293b; --sl-text: #f1f5f9; --sl-text-sec: #cbd5e1; --sl-muted: #64748b; --sl-accent: #00d4aa; --sl-purple: #8b5cf6; --sl-blue: #3b82f6; --sl-red: #ef4444; --sl-orange: #f59e0b; }}
388
+ </style>
389
+ </head>
390
+ <body style='background:var(--sl-bg);padding:40px'>{html_content}</body></html>""")
391
  return filename
392
 
393
+ # --- Theme Toggle JS ---
394
+ THEME_TOGGLE_JS = """
395
+ () => {
396
+ const root = document.documentElement;
397
+ const isLight = root.classList.contains('light-mode');
398
+ if (isLight) {
399
+ root.classList.remove('light-mode');
400
+ return 'β˜€οΈ Light Mode';
401
+ } else {
402
+ root.classList.add('light-mode');
403
+ return 'πŸŒ™ Dark Mode';
404
+ }
405
+ }
406
+ """
407
+
408
  # --- Gradio UI ---
409
+ APP_CSS = """
410
+ :root {
411
+ --sl-bg: #0b0f1a;
412
+ --sl-card: #111827;
413
+ --sl-border: #1e293b;
414
+ --sl-text: #f1f5f9;
415
+ --sl-text-sec: #cbd5e1;
416
+ --sl-muted: #64748b;
417
+ --sl-accent: #00d4aa;
418
+ --sl-purple: #8b5cf6;
419
+ --sl-blue: #3b82f6;
420
+ --sl-red: #ef4444;
421
+ --sl-orange: #f59e0b;
422
+ }
423
+ :root.light-mode {
424
+ --sl-bg: #ffffff;
425
+ --sl-card: #f8fafc;
426
+ --sl-border: #e2e8f0;
427
+ --sl-text: #0f172a;
428
+ --sl-text-sec: #334155;
429
+ --sl-muted: #64748b;
430
+ --sl-accent: #059669;
431
+ --sl-purple: #7c3aed;
432
+ --sl-blue: #2563eb;
433
+ --sl-red: #dc2626;
434
+ --sl-orange: #d97706;
435
+ }
436
+ .gradio-container {
437
+ background-color: var(--sl-bg) !important;
438
+ color: var(--sl-text) !important;
439
+ transition: background-color 0.3s ease, color 0.3s ease;
440
+ }
441
+ .gradio-container textarea, .gradio-container input {
442
+ background-color: var(--sl-card) !important;
443
+ color: var(--sl-text) !important;
444
+ border-color: var(--sl-border) !important;
445
+ }
446
+ .gradio-container label {
447
+ color: var(--sl-text-sec) !important;
448
+ }
449
+ .gradio-container button.primary {
450
+ background: linear-gradient(135deg, var(--sl-accent), var(--sl-blue)) !important;
451
+ color: white !important;
452
+ border: none !important;
453
+ }
454
+ .gradio-container button.secondary {
455
+ background: var(--sl-card) !important;
456
+ color: var(--sl-text) !important;
457
+ border: 1px solid var(--sl-border) !important;
458
+ }
459
+ .gradio-container .footer-wrap {
460
+ text-align: center;
461
+ padding: 24px 16px;
462
+ margin-top: 24px;
463
+ border-top: 1px solid var(--sl-border);
464
+ color: var(--sl-muted);
465
+ font-size: 0.75rem;
466
+ line-height: 1.6;
467
+ transition: all 0.3s ease;
468
+ }
469
+ .gradio-container .footer-wrap a {
470
+ color: var(--sl-accent);
471
+ text-decoration: none;
472
+ }
473
+ .gradio-container .footer-wrap a:hover {
474
+ text-decoration: underline;
475
+ }
476
+ .gradio-container .header-row {
477
+ display: flex;
478
+ justify-content: space-between;
479
+ align-items: center;
480
+ flex-wrap: wrap;
481
+ gap: 12px;
482
+ }
483
+ """
484
+
485
+ with gr.Blocks(theme=gr.themes.Soft(), css=APP_CSS) as demo:
486
  data_state = gr.State()
487
+
488
+ with gr.Row(elem_classes="header-row"):
489
+ gr.Markdown("# πŸ” ScrumLens v0.5\n### CHAOS 2 CLARITY | Long-input ready")
490
+ theme_btn = gr.Button("β˜€οΈ Light Mode", size="sm", variant="secondary")
491
+
492
  with gr.Row():
493
  with gr.Column(scale=4):
494
  input_text = gr.Textbox(
 
498
  )
499
  with gr.Row():
500
  analyze_btn = gr.Button("πŸ” Analyze", variant="primary")
501
+ export_btn = gr.Button("πŸ“„ Export HTML Report", variant="secondary")
502
  with gr.Column(scale=2):
503
  gr.Markdown("● TEAM ROLES & FOCUS")
504
  with gr.Row():
 
511
  output_html = gr.HTML(label="Analysis Results")
512
  export_file = gr.File(label="Download Report")
513
 
514
+ # Theme toggle (pure JS β€” no Python round-trip needed)
515
+ theme_btn.click(fn=None, inputs=None, outputs=[theme_btn], js=THEME_TOGGLE_JS)
516
+
517
  analyze_btn.click(
518
  fn=analyze_conversation,
519
  inputs=[input_text],
 
525
  outputs=[export_file]
526
  )
527
 
528
+ # Footer
529
+ gr.Markdown("""
530
+ <div class="footer-wrap">
531
+ <strong>ScrumLens v0.5</strong> β€” Crafted with β˜€οΈ by Sai Varakala<br>
532
+ <a href="mailto:suryasticsai@gmail.com">suryasticsai@gmail.com</a> Β·
533
+ <a href="https://scrumlens.netlify.app" target="_blank">scrumlens.netlify.app</a><br>
534
+ Built for Scrum Masters who hate manual reporting Β· MIT License
535
+ </div>
536
+ """)
537
+
538
  if __name__ == "__main__":
539
  demo.launch()