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()