Spaces:
Running
Running
File size: 31,225 Bytes
0cd4232 9b0cacb 0cd4232 8e10a6c 9b0cacb 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb ea2f815 0cd4232 ea2f815 0cd4232 ea2f815 0cd4232 ea2f815 0cd4232 ea2f815 0cd4232 ea2f815 0cd4232 ea2f815 0cd4232 ea2f815 0cd4232 9b0cacb 930b1f9 0cd4232 9b0cacb 0cd4232 9b0cacb 0cd4232 ea2f815 0cd4232 9b0cacb ea2f815 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb ea2f815 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb ea2f815 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb ea2f815 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb 0cd4232 9b0cacb 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb ea2f815 9b0cacb 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 0cd4232 ea2f815 f3227ef ea2f815 f3227ef ea2f815 f3227ef ea2f815 f3227ef 9b0cacb ea2f815 f3227ef ea2f815 6c0cd27 f3227ef ea2f815 f3227ef ea2f815 f3227ef ea2f815 f3227ef ea2f815 f3227ef ea2f815 0cd4232 ea2f815 0cd4232 9b0cacb 0cd4232 ea2f815 0cd4232 9b0cacb 0cd4232 9b0cacb ea2f815 9b0cacb 0cd4232 ea2f815 0cd4232 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 | import gradio as gr
import chromadb
from chromadb.utils import embedding_functions
from groq import Groq
import os
import json
import datetime
import re
# --- Knowledge Base Setup ---
documents = [
"Sprint Planning is a Scrum ceremony where the Product Owner presents prioritized backlog items, the Scrum Master facilitates the session, and the Development Team estimates and commits to sprint work. Output includes Sprint Goal, Sprint Backlog, and committed user stories.",
"Daily Standup is a 15-minute Agile sync facilitated by the Scrum Master where Developers discuss completed work, upcoming tasks, and blockers. Output includes blocker visibility, progress tracking, and daily alignment.",
"Sprint Review is conducted by the Scrum Team with stakeholders to demonstrate completed increments. Product Owner validates delivered work and gathers feedback. Output includes stakeholder feedback, accepted stories, and backlog refinement inputs.",
"Sprint Retrospective is facilitated by the Scrum Master to identify improvements, celebrate wins, and resolve team pain points. Output includes actionable improvements, retrospective notes, and process enhancement ideas.",
"Bornfire Retrospective Talks were introduced as interactive retrospective discussions focused on honesty, emotional transparency, team bonding, and psychological safety. Scrum Masters facilitated conversations while team members shared learnings and concerns. Output included trust building, actionable feedback, and stronger collaboration.",
"Velocity measures the amount of work completed by a Scrum Team during a sprint using story points. Scrum Masters track velocity while Product Owners use it for forecasting future sprint capacity. Output includes sprint predictability and planning insights.",
"Definition of Done is a shared quality agreement created collaboratively by Developers, Product Owner, and Scrum Master. Output ensures stories meet quality standards before completion.",
"PI Planning is a SAFe event where Release Train Engineers, Product Management, Scrum Masters, Product Owners, Business Owners, and Agile Teams align on Program Increment objectives. Output includes PI Objectives, dependency mapping, and team commitments.",
"WSJF stands for Weighted Shortest Job First, a SAFe prioritization method usually driven by Product Management and Business Owners. Output includes prioritized backlog sequencing based on business value and duration.",
"A Scrum Master facilitates Agile ceremonies, removes impediments, tracks team health, coaches Agile practices, manages delivery coordination, and enables continuous improvement. Output includes smoother delivery flow, improved collaboration, and Agile maturity.",
"A Product Owner owns backlog prioritization, defines acceptance criteria, aligns business goals, and clarifies requirements for teams. Output includes refined backlog, prioritized user stories, and business alignment.",
"Developers are responsible for designing, coding, testing, reviewing, and delivering working software increments during a sprint. Output includes completed features, technical improvements, and production-ready deliverables.",
"Release Train Engineers coordinate Agile Release Trains in SAFe environments, manage cross-team dependencies, support PI Planning, and track ART progress. Output includes ART alignment, dependency management, and program-level delivery tracking.",
"Sai Varakala expressed gratitude towards Synergeons for collaboration, dedication, learning culture, and shared memories across Agile journeys.",
"Pokemon Sprint Card Game transformed Agile execution into a gamified adventure where team members earned Pokemon cards based on sprint performance, collaboration, story closures, and engagement. Scrum Masters managed gameplay while teams participated in sprint activities. Output included team bonding, engagement, motivation, and improved sprint participation.",
"Pokemon cards symbolized resilience, teamwork, and camaraderie. Trainers collected cards as memorable artifacts representing sprint contributions and achievements.",
"Sprint Agile Economy introduced concepts like PikaCoins, StoryStocks, SprintMarket, and investment-based gamification mechanics to encourage Agile engagement. Scrum Masters experimented with economy balancing while participants interacted with the framework. Output included experimentation in Agile gamification and learning around behavioral systems.",
"Sprint Agile Economy was later discontinued due to complexity, unfair taxation, lack of impact, and micro-management concerns. Output from retrospectives suggested focusing on simpler and more meaningful engagement frameworks.",
"EnvyLevelUp was introduced as a pilot self-development and productivity initiative focused on growth, learning, and personal improvement within Agile teams.",
"Synergeon Learners channel was created on November 25th, 2024 for collaborative learning, resource sharing, Agile discussions, and planning activities.",
"The channel supported resources sharing, Q&A forums, retrospective discussions, planning sessions, JavaScript learning, and Agile knowledge exchange.",
"Festival Of Lights was the Agile iteration theme for Sprint 4.1.",
"Gracious Sunshine was the Agile iteration theme for Sprint 4.2.",
"Battle of Kurukshetra was the Agile iteration theme for Sprint 4.3.",
"Spirit of Game was the Agile iteration theme for Sprint 4.4.",
"Christmas Parade was the Agile iteration theme for Sprint 4.5.",
"New Year Vibe was the Agile iteration theme for Sprint 4.IP.",
"JavaScript learning sessions introduced concepts such as variables, let, const, and var declarations to learners within the Synergeon community.",
"Utility Toolkit initiatives were introduced to automate repetitive Scrum Master and Agile coordination activities using modern tooling and lightweight automation approaches.",
"Jira Sprint Summary Generator automated sprint reporting and progress summarization responsibilities usually handled by Scrum Masters. Output included faster reporting and reduced manual effort.",
"Slack Summary Poster automated Agile communication updates across channels and teams. Output included improved visibility and streamlined coordination.",
"WSR Mail Generator automated Weekly Status Report creation using HTML templates and Agile metrics. Output included professional reporting and reduced reporting overhead.",
"Jira Token Hyperlink Utility simplified Jira ticket referencing and navigation workflows for Agile teams. Output included improved accessibility and faster navigation.",
"Sprint Burndown Calculator automated sprint progress calculations and visualization support for Scrum Masters. Output included sprint forecasting and delivery transparency.",
"Retro Action Tracker automated retrospective action item tracking and follow-up reminders. Output included accountability and continuous improvement tracking.",
"Dynamic Jira dashboards were maintained for INC, FF, TT, and AOS teams to track delivery metrics, sprint progress, impediments, and team health.",
"IT2.5 A Moment for Unity focused on compassion, teamwork, and resilience after the Air India AI-171 tragedy while encouraging emotional solidarity among team members.",
"IT2.IP Bonalu Festive Vibe celebrated Hyderabad culture and marked successful completion of PI 2025.2 with gratitude and celebration.",
"IT3.1 Dancing Peacock emphasized agility, resilience, and vibrant collaboration during technical and operational challenges.",
"IT3.2 Shooting Stars celebrated milestones, weddings, birthdays, and team achievements while maintaining delivery momentum.",
"IT3.3 Monsoon Rainbow celebrated Ganesh Chaturthi, zero impediments, and recognition through Best Team Award achievements.",
"IT3.4 Aquatic Symphony introduced ReCall Play retrospective activities and innovative team engagement concepts.",
"IT3.5 Envy Master focused on mentorship, innovation, self-growth, and SM-ART Factory initiatives.",
"IT3.IP Oktober Grand Festival celebrated Dussehra while preparing teams for future Program Increment goals and planning.",
"IT4.1 Festival of Lights promoted positivity, collaboration, AI-assisted productivity, and adoption of tools like Co-pilot within development workflows.",
"AWS Cloud Practitioner Certification resources and AWS learning materials were shared within the Synergeon Learners ecosystem to encourage cloud learning.",
"Retrospective agendas included Daily Standup planning, Synergeon Learning planning, IdeaTalks, Sprint Card Game discussions, planned leave tracking, and collaboration improvement discussions.",
"Thunder Sprint statistics included Pokemon such as Regidrago, Arcanine, Umbreon, Snorlax, Dragonite, Pikachu, Palkia, Flareon, and Garchomp representing sprint performance and achievements.",
"The Synergeon community focused on combining Agile practices, gamification, learning culture, automation, emotional intelligence, and collaboration into a memorable and innovative Agile ecosystem."
]
db = chromadb.Client()
emb = embedding_functions.DefaultEmbeddingFunction()
collection = db.get_or_create_collection(name="agile_knowledge", embedding_function=emb)
if collection.count() == 0:
collection.add(documents=documents, ids=[f"doc_{i}" for i in range(len(documents))])
# Fetch API key securely from HuggingFace Secrets
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
if not GROQ_API_KEY:
raise ValueError("Missing GROQ_API_KEY")
groq_client = Groq(api_key=GROQ_API_KEY)
# --- System Prompt for Structured Extraction ---
SYSTEM_PROMPT = """
You are ScrumLens Analyzer. Your job is to analyze a conversation transcript and extract key Agile insights.
For long transcripts, analyze ALL messages but keep each chain item concise.
Return ONLY a JSON object with the following structure:
{
"stats": { "messages": 0, "actions": 0, "decisions": 0, "health": 100 },
"roles": { "decision_maker": "", "facilitator": "", "dev_lead": "", "qa_lead": "" },
"chain": [
{ "user": "Name", "role": "Role", "time": "HH:MM AM", "text": "...", "tag": "Decision/Action/Risk/Blocker/Idea/None" }
],
"topics": ["Topic 1", "Topic 2"],
"health_status": "Sprint looks healthy...",
"actions": [ { "text": "Action...", "owner": "Name" } ],
"decisions": [ { "text": "Decision...", "owner": "Name" } ],
"risks": [ "Risk..." ]
}
"""
# --- Robust JSON Repair ---
def safe_json_loads(text):
"""Parse JSON with multiple fallback strategies."""
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL)
if match:
try:
return json.loads(match.group(1).strip())
except:
pass
start = text.find("{")
end = text.rfind("}")
if start != -1 and end != -1 and end > start:
try:
return json.loads(text[start:end+1])
except:
pass
return None
# --- Transcript Chunking ---
def parse_transcript_lines(text):
"""Parse raw transcript into structured lines."""
lines = []
pattern = r"\[?(\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?)\]?\s*([A-Za-z\s]+?):\s*(.+)"
for raw_line in text.strip().split("\n"):
raw_line = raw_line.strip()
if not raw_line:
continue
m = re.match(pattern, raw_line)
if m:
time, user, msg = m.groups()
lines.append({"time": time.strip(), "user": user.strip(), "text": msg.strip()})
else:
m2 = re.match(r"([A-Za-z\s]+?):\s*(.+)", raw_line)
if m2:
user, msg = m2.groups()
lines.append({"time": "", "user": user.strip(), "text": msg.strip()})
else:
lines.append({"time": "", "user": "Unknown", "text": raw_line})
return lines
def chunk_lines(lines, max_per_chunk=35):
for i in range(0, len(lines), max_per_chunk):
yield lines[i:i+max_per_chunk]
def lines_to_text(lines):
return "\n".join([
f"[{l.get('time','')}] {l['user']}: {l['text']}" for l in lines
])
# --- Analysis Core ---
def analyze_chunk(chunk_text, context, progress=None):
try:
response = groq_client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Context: {context}\n\nAnalyze this conversation segment:\n{chunk_text}"}
],
temperature=0.1,
response_format={"type": "json_object"},
max_tokens=4096
)
raw = response.choices[0].message.content
return safe_json_loads(raw)
except Exception as e:
print(f"Chunk analysis error: {e}")
return None
def merge_analyses(results):
if not results:
return None
if len(results) == 1:
return results[0]
merged = {
"stats": {"messages": 0, "actions": 0, "decisions": 0, "health": 75},
"roles": {"decision_maker": "", "facilitator": "", "dev_lead": "", "qa_lead": ""},
"chain": [],
"topics": [],
"health_status": "",
"actions": [],
"decisions": [],
"risks": []
}
health_scores = []
for r in results:
if not isinstance(r, dict):
continue
if isinstance(r.get("chain"), list):
merged["chain"].extend(r["chain"])
if isinstance(r.get("actions"), list):
merged["actions"].extend(r["actions"])
if isinstance(r.get("decisions"), list):
merged["decisions"].extend(r["decisions"])
if isinstance(r.get("risks"), list):
merged["risks"].extend(r["risks"])
if isinstance(r.get("topics"), list):
merged["topics"].extend(r["topics"])
for rk in ["decision_maker", "facilitator", "dev_lead", "qa_lead"]:
val = r.get("roles", {}).get(rk, "")
if val and not merged["roles"].get(rk):
merged["roles"][rk] = val
h = r.get("stats", {}).get("health", 75)
health_scores.append(h)
if r.get("health_status"):
merged["health_status"] = r["health_status"]
seen = set()
unique_chain = []
for msg in merged["chain"]:
key = f"{msg.get('user','')}:{msg.get('text','')}:{msg.get('time','')}"
if key not in seen:
seen.add(key)
unique_chain.append(msg)
merged["chain"] = unique_chain
merged["topics"] = list(dict.fromkeys(merged["topics"]))
merged["risks"] = list(dict.fromkeys(merged["risks"]))
merged["stats"]["messages"] = len(unique_chain)
merged["stats"]["actions"] = len(merged["actions"])
merged["stats"]["decisions"] = len(merged["decisions"])
merged["stats"]["health"] = int(sum(health_scores)/len(health_scores)) if health_scores else 75
return merged
def apply_fallbacks(data):
data.setdefault("stats", {"messages": 0, "actions": 0, "decisions": 0, "health": 75})
data.setdefault("roles", {})
data.setdefault("chain", [])
data.setdefault("topics", [])
data.setdefault("health_status", "No health summary available.")
data.setdefault("actions", [])
data.setdefault("decisions", [])
data.setdefault("risks", [])
data["roles"].setdefault("decision_maker", "")
data["roles"].setdefault("facilitator", "")
data["roles"].setdefault("dev_lead", "")
data["roles"].setdefault("qa_lead", "")
data["stats"]["messages"] = len(data["chain"])
data["stats"]["actions"] = len(data["actions"])
data["stats"]["decisions"] = len(data["decisions"])
return data
# --- Dashboard Rendering (uses CSS variables for theme support) ---
def render_dashboard(data):
css = """
<style>
.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; }
.sl-stats { display: flex; gap: 15px; margin-bottom: 25px; flex-wrap: wrap; }
.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; }
.sl-stat-val { font-size: 1.5rem; font-weight: 700; color: var(--sl-accent); }
.sl-stat-label { font-size: 0.7rem; text-transform: uppercase; color: var(--sl-muted); margin-top: 5px; }
.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; }
.sl-chain { display: flex; flex-direction: column; gap: 15px; position: relative; padding-left: 20px; }
.sl-chain::before { content: ''; position: absolute; left: 35px; top: 0; bottom: 0; width: 2px; background: var(--sl-border); z-index: 0; }
.sl-msg { display: flex; gap: 15px; position: relative; z-index: 1; }
.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; }
.sl-msg-content { background: var(--sl-card); border: 1px solid var(--sl-border); padding: 12px 16px; border-radius: 10px; flex-grow: 1; }
.sl-msg-header { display: flex; justify-content: space-between; margin-bottom: 5px; }
.sl-msg-user { font-weight: 700; font-size: 0.9rem; color: var(--sl-text); }
.sl-msg-role { font-size: 0.7rem; color: var(--sl-muted); font-weight: 400; margin-left: 5px; }
.sl-msg-time { font-size: 0.7rem; color: var(--sl-muted); }
.sl-msg-text { font-size: 0.85rem; line-height: 1.5; color: var(--sl-text-sec); }
.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; }
.tag-decision { background: rgba(0, 212, 170, 0.1); color: var(--sl-accent); border: 1px solid rgba(0, 212, 170, 0.2); }
.tag-action { background: rgba(59, 130, 246, 0.1); color: var(--sl-blue); border: 1px solid rgba(59, 130, 246, 0.2); }
.tag-risk { background: rgba(239, 68, 68, 0.1); color: var(--sl-red); border: 1px solid rgba(239, 68, 68, 0.2); }
.tag-blocker { background: rgba(245, 158, 11, 0.1); color: var(--sl-orange); border: 1px solid rgba(245, 158, 11, 0.2); }
.tag-idea { background: rgba(139, 92, 246, 0.1); color: var(--sl-purple); border: 1px solid rgba(139, 92, 246, 0.2); }
.sl-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-top: 25px; }
@media (max-width: 768px) { .sl-grid { grid-template-columns: 1fr; } }
.sl-grid-item { background: var(--sl-card); border: 1px solid var(--sl-border); padding: 15px; border-radius: 10px; }
.sl-list-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; font-size: 0.8rem; color: var(--sl-text-sec); }
.sl-list-bullet { width: 16px; height: 16px; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; }
.health-bar { height: 8px; background: var(--sl-border); border-radius: 4px; overflow: hidden; margin: 10px 0; }
.health-fill { height: 100%; background: linear-gradient(90deg, var(--sl-red), var(--sl-orange), var(--sl-accent)); }
</style>
"""
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>'
chain_html = '<div class="sl-section-title"> Conversation Chain</div><div class="sl-chain">'
for msg in data['chain']:
initials = "".join([n[0] for n in msg['user'].split()])[:2].upper()
tag = msg.get("tag", "None")
tag_class = f"tag-{tag.lower()}" if tag.lower() not in ["none", ""] else ""
tag_html = f'<div class="sl-tag {tag_class}">^ {tag}</div>' if tag_class else ""
chain_html += f'<div class="sl-msg"><div class="sl-avatar">{initials}</div><div class="sl-msg-content"><div class="sl-msg-header"><span class="sl-msg-user">{msg["user"]} <span class="sl-msg-role">({msg.get("role","")})</span></span><span class="sl-msg-time">{msg.get("time","")}</span></div><div class="sl-msg-text">{msg["text"]}</div>{tag_html}</div></div>'
chain_html += "</div>"
topics_html = '<div style="margin-top:20px"><div class="sl-section-title">Topics</div><div style="display:flex;gap:10px;flex-wrap:wrap">'
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>'
topics_html += "</div></div>"
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>'
actions_html = '<div class="sl-grid-item"><div class="sl-section-title">Actions</div>'
for a in data['actions']:
owner = a.get("owner", "Unassigned")
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>'
if not data['actions']:
actions_html += '<div style="color:var(--sl-muted);font-size:0.8rem">No actions detected</div>'
actions_html += '</div>'
decisions_html = '<div class="sl-grid-item"><div class="sl-section-title">Decisions</div>'
for d in data['decisions']:
owner = d.get("owner", "Unassigned")
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>'
if not data['decisions']:
decisions_html += '<div style="color:var(--sl-muted);font-size:0.8rem">No decisions detected</div>'
decisions_html += '</div>'
risks_html = '<div class="sl-grid-item"><div class="sl-section-title">Risks & Blockers</div>'
for r in data['risks']:
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>'
if not data['risks']:
risks_html += '<div style="color:var(--sl-muted);font-size:0.8rem">No risks detected</div>'
risks_html += '</div>'
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>'
# --- Main Analysis Flow ---
def analyze_conversation(text, progress=gr.Progress()):
if not text.strip():
return None, "", "", "", "", ""
lines = parse_transcript_lines(text)
if not lines:
return (
None,
"<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>",
"", "", "", ""
)
query_sample = text[:800]
try:
results = collection.query(query_texts=[query_sample], n_results=3)
context = "\n".join(results["documents"][0]) if results and results.get("documents") and results["documents"] else ""
except Exception as e:
context = ""
print(f"Chroma query error: {e}")
all_results = []
chunks = list(chunk_lines(lines, max_per_chunk=35))
total_chunks = len(chunks)
for idx, chunk in enumerate(chunks):
progress((idx + 0.5) / total_chunks, desc=f"Analyzing chunk {idx+1}/{total_chunks}...")
chunk_text = lines_to_text(chunk)
result = analyze_chunk(chunk_text, context)
if result:
all_results.append(result)
progress((idx + 1) / total_chunks, desc=f"Chunk {idx+1}/{total_chunks} done")
if not all_results:
return (
None,
"""<div style='padding:20px;background:var(--sl-card);color:var(--sl-red);border-radius:10px'>
<h3>Analysis Failed</h3>
<p>Could not analyze the conversation. The input may be too long, malformed, or the API is unavailable.</p>
<p style="color:var(--sl-muted)">Tip: Try a shorter segment or verify your GROQ_API_KEY.</p>
</div>""",
"", "", "", ""
)
data = merge_analyses(all_results)
data = apply_fallbacks(data)
dashboard = render_dashboard(data)
return (
data,
dashboard,
data["roles"]["decision_maker"],
data["roles"]["facilitator"],
data["roles"]["dev_lead"],
data["roles"]["qa_lead"]
)
# --- Export ---
def export_report(data_state):
if not data_state:
return None
html_content = render_dashboard(data_state)
filename = f"scrumlens_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
with open(filename, "w", encoding="utf-8") as f:
f.write(f"""<html>
<head><meta charset='utf-8'><title>ScrumLens Report</title>
<style>
: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; }}
</style>
</head>
<body style='background:var(--sl-bg);padding:40px'>{html_content}</body></html>""")
return filename
# --- Theme Toggle JS ---
THEME_TOGGLE_JS = """
() => {
const body = document.body;
const isLight = body.classList.contains('light-mode');
if (isLight) {
body.classList.remove('light-mode');
return 'βοΈ Light Mode';
} else {
body.classList.add('light-mode');
return 'π Dark Mode';
}
}
"""
# --- Gradio UI ---
APP_CSS = """
: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;
}
body.light-mode {
--sl-bg: #ffffff;
--sl-card: #f8fafc;
--sl-border: #e2e8f0;
--sl-text: #0f172a; /* dark text */
--sl-text-sec: #334155; /* dark secondary text */
--sl-muted: #64748b;
--sl-accent: #059669;
--sl-purple: #7c3aed;
--sl-blue: #2563eb;
--sl-red: #dc2626;
--sl-orange: #d97706;
}
.gradio-container {
background-color: var(--sl-bg) !important;
color: var(--sl-text) !important; /* <-- make sure !important is here */
transition: background-color 0.3s ease, color 0.3s ease;
}
.gradio-container textarea, .gradio-container input {
background-color: var(--sl-card) !important;
color: var(--sl-text) !important;
border-color: var(--sl-border) !important;
}
.gradio-container label {
color: var(--sl-text-sec) !important;
}
.gradio-container button.primary {
background: linear-gradient(135deg, var(--sl-accent), var(--sl-blue)) !important;
color: white !important;
border: none !important;
}
.gradio-container button.secondary {
background: var(--sl-card) !important;
color: var(--sl-text) !important;
border: 1px solid var(--sl-border) !important;
}
/* Force ALL text inside Gradio to use the theme color */
.gradio-container, .gradio-container * {
color: var(--sl-text) !important;
}
.gradio-container button, .gradio-container a {
color: inherit !important;
}
.gradio-container .footer-wrap {
text-align: center;
padding: 24px 16px;
margin-top: 24px;
border-top: 1px solid var(--sl-border);
color: var(--sl-muted);
font-size: 0.75rem;
line-height: 1.6;
transition: all 0.3s ease;
}
.gradio-container .footer-wrap a {
color: var(--sl-accent);
text-decoration: none;
}
.gradio-container .footer-wrap a:hover {
text-decoration: underline;
}
.gradio-container .header-row {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
"""
with gr.Blocks(theme=gr.themes.Soft(), css=APP_CSS) as demo:
data_state = gr.State()
with gr.Row(elem_classes="header-row"):
gr.Markdown("# π ScrumLens v0.5\n### CHAOS 2 CLARITY | Long-input ready")
theme_btn = gr.Button("βοΈ Light Mode", size="sm", variant="secondary")
with gr.Row():
with gr.Column(scale=4):
input_text = gr.Textbox(
label="β PASTE CONVERSATION",
placeholder="[10:15 AM] Raj: Let's delay the release...",
lines=12
)
with gr.Row():
analyze_btn = gr.Button("π Analyze", variant="primary")
export_btn = gr.Button("π Export HTML Report", variant="secondary")
with gr.Column(scale=2):
gr.Markdown("β TEAM ROLES & FOCUS")
with gr.Row():
dm_box = gr.Textbox(label="DECISION MAKER", interactive=False)
fa_box = gr.Textbox(label="FACILITATOR", interactive=False)
with gr.Row():
dl_box = gr.Textbox(label="DEV LEAD", interactive=False)
ql_box = gr.Textbox(label="QA LEAD", interactive=False)
output_html = gr.HTML(label="Analysis Results")
export_file = gr.File(label="Download Report")
# Theme toggle (pure JS β no Python round-trip needed)
theme_btn.click(fn=None, inputs=None, outputs=[theme_btn], js=THEME_TOGGLE_JS)
analyze_btn.click(
fn=analyze_conversation,
inputs=[input_text],
outputs=[data_state, output_html, dm_box, fa_box, dl_box, ql_box]
)
export_btn.click(
fn=export_report,
inputs=[data_state],
outputs=[export_file]
)
# Footer
gr.Markdown("""
<div class="footer-wrap">
<strong>ScrumLens v0.5</strong> β Crafted with βοΈ by Sai Varakala<br>
<a href="mailto:suryasticsai@gmail.com">suryasticsai@gmail.com</a> Β·
<a href="https://scrumlens.netlify.app" target="_blank">scrumlens.netlify.app</a><br>
Built for Scrum Masters who hate manual reporting Β· MIT License
</div>
""")
if __name__ == "__main__":
demo.launch()
|