nothingworry commited on
Commit
adf80ee
·
1 Parent(s): 9513bb7

feat: Add LLM rule explanations, real-time visualizations, and fix analytics permissions

Browse files
app.py CHANGED
@@ -510,6 +510,18 @@ def add_admin_rules(tenant_id: str, role: str, rules_text: str) -> str:
510
  if data.get("enhanced"):
511
  edge_cases = data.get("edge_cases", [])
512
  improvements = data.get("improvements", [])
 
 
 
 
 
 
 
 
 
 
 
 
513
  if edge_cases or improvements:
514
  enhanced.append(f"**{data.get('added_rule', rules[0])}**:")
515
  if improvements:
@@ -546,6 +558,18 @@ def add_admin_rules(tenant_id: str, role: str, rules_text: str) -> str:
546
  if data.get("enhanced"):
547
  chunk_enhanced = data.get("enhancement_summary", [])
548
  enhanced.extend([f"[Chunk {chunk_num}/{total_chunks}] {e}" for e in chunk_enhanced])
 
 
 
 
 
 
 
 
 
 
 
 
549
  else:
550
  errors.append(f"Chunk {chunk_num}/{total_chunks} failed: {resp.status_code}: {resp.text}")
551
  except requests.exceptions.Timeout:
@@ -562,6 +586,7 @@ def add_admin_rules(tenant_id: str, role: str, rules_text: str) -> str:
562
  summary.append(f"\n🤖 LLM Enhancement Applied:\n" + "\n".join(enhanced[:5]))
563
  if len(enhanced) > 5:
564
  summary.append(f"... and {len(enhanced) - 5} more enhancements")
 
565
  if errors:
566
  summary.append("\n⚠️ Errors:\n" + "\n".join(errors))
567
 
 
510
  if data.get("enhanced"):
511
  edge_cases = data.get("edge_cases", [])
512
  improvements = data.get("improvements", [])
513
+ explanation = data.get("explanation", "")
514
+ examples = data.get("examples", [])
515
+ missing_patterns = data.get("missing_patterns", [])
516
+
517
+ if explanation:
518
+ enhanced.append(f"**💡 Explanation:** {explanation}")
519
+ if examples:
520
+ examples_list = "\n".join([f" • {ex}" for ex in examples[:5]])
521
+ enhanced.append(f"**📋 Examples:**\n{examples_list}")
522
+ if missing_patterns:
523
+ patterns_list = "\n".join([f" • {p}" for p in missing_patterns[:5]])
524
+ enhanced.append(f"**🔍 Suggested Patterns:**\n{patterns_list}")
525
  if edge_cases or improvements:
526
  enhanced.append(f"**{data.get('added_rule', rules[0])}**:")
527
  if improvements:
 
558
  if data.get("enhanced"):
559
  chunk_enhanced = data.get("enhancement_summary", [])
560
  enhanced.extend([f"[Chunk {chunk_num}/{total_chunks}] {e}" for e in chunk_enhanced])
561
+
562
+ # Add explanations for bulk rules if available
563
+ if data.get("explanations"):
564
+ for exp in data["explanations"][:3]: # Show first 3 explanations
565
+ if exp.get("explanation"):
566
+ enhanced.append(f"\n💡 **{exp.get('rule', 'Rule')} Explanation:** {exp['explanation']}")
567
+ if exp.get("examples"):
568
+ examples_list = "\n".join([f" • {ex}" for ex in exp['examples'][:3]])
569
+ enhanced.append(f"📋 **Examples:**\n{examples_list}")
570
+ if exp.get("missing_patterns"):
571
+ patterns_list = "\n".join([f" • {p}" for p in exp['missing_patterns'][:3]])
572
+ enhanced.append(f"🔍 **Suggested Patterns:**\n{patterns_list}")
573
  else:
574
  errors.append(f"Chunk {chunk_num}/{total_chunks} failed: {resp.status_code}: {resp.text}")
575
  except requests.exceptions.Timeout:
 
586
  summary.append(f"\n🤖 LLM Enhancement Applied:\n" + "\n".join(enhanced[:5]))
587
  if len(enhanced) > 5:
588
  summary.append(f"... and {len(enhanced) - 5} more enhancements")
589
+
590
  if errors:
591
  summary.append("\n⚠️ Errors:\n" + "\n".join(errors))
592
 
backend/api/routes/admin.py CHANGED
@@ -183,6 +183,9 @@ async def add_redflag_rule(
183
  final_pattern = enhanced_data["pattern"]
184
  final_severity = enhanced_data["severity"]
185
  final_description = enhanced_data["description"]
 
 
 
186
  edge_cases = enhanced_data.get("edge_cases", [])
187
  improvements = enhanced_data.get("improvements", [])
188
  else:
@@ -191,6 +194,9 @@ async def add_redflag_rule(
191
  final_pattern = payload.pattern if payload else None
192
  final_severity = payload.severity if payload else "medium"
193
  final_description = payload.description if payload else None
 
 
 
194
  edge_cases = []
195
  improvements = []
196
 
@@ -228,6 +234,9 @@ async def add_redflag_rule(
228
  "severity": final_severity,
229
  "description": final_description or final_rule,
230
  "enhanced": enhanced_data is not None,
 
 
 
231
  "edge_cases": edge_cases,
232
  "improvements": improvements,
233
  "rules": rules
@@ -540,6 +549,17 @@ async def upload_rules_from_file(
540
 
541
  rules_list = get_rules_for_tenant(x_tenant_id)
542
 
 
 
 
 
 
 
 
 
 
 
 
543
  return {
544
  "tenant_id": x_tenant_id,
545
  "filename": file.filename,
@@ -547,6 +567,7 @@ async def upload_rules_from_file(
547
  "total_extracted": len(rules),
548
  "enhanced": len(enhanced_rules_data) > 0,
549
  "enhancement_summary": enhancement_summary,
 
550
  "rules": rules_list
551
  }
552
 
 
183
  final_pattern = enhanced_data["pattern"]
184
  final_severity = enhanced_data["severity"]
185
  final_description = enhanced_data["description"]
186
+ explanation = enhanced_data.get("explanation", "")
187
+ examples = enhanced_data.get("examples", [])
188
+ missing_patterns = enhanced_data.get("missing_patterns", [])
189
  edge_cases = enhanced_data.get("edge_cases", [])
190
  improvements = enhanced_data.get("improvements", [])
191
  else:
 
194
  final_pattern = payload.pattern if payload else None
195
  final_severity = payload.severity if payload else "medium"
196
  final_description = payload.description if payload else None
197
+ explanation = ""
198
+ examples = []
199
+ missing_patterns = []
200
  edge_cases = []
201
  improvements = []
202
 
 
234
  "severity": final_severity,
235
  "description": final_description or final_rule,
236
  "enhanced": enhanced_data is not None,
237
+ "explanation": explanation,
238
+ "examples": examples,
239
+ "missing_patterns": missing_patterns,
240
  "edge_cases": edge_cases,
241
  "improvements": improvements,
242
  "rules": rules
 
549
 
550
  rules_list = get_rules_for_tenant(x_tenant_id)
551
 
552
+ # Collect explanations, examples, and missing patterns from enhanced rules
553
+ explanations_data = []
554
+ for i, enhanced in enumerate(enhanced_rules_data):
555
+ if i < len(rules):
556
+ explanations_data.append({
557
+ "rule": enhanced.get("rule", rules[i]),
558
+ "explanation": enhanced.get("explanation", ""),
559
+ "examples": enhanced.get("examples", []),
560
+ "missing_patterns": enhanced.get("missing_patterns", [])
561
+ })
562
+
563
  return {
564
  "tenant_id": x_tenant_id,
565
  "filename": file.filename,
 
567
  "total_extracted": len(rules),
568
  "enhanced": len(enhanced_rules_data) > 0,
569
  "enhancement_summary": enhancement_summary,
570
+ "explanations": explanations_data,
571
  "rules": rules_list
572
  }
573
 
backend/api/routes/analytics.py CHANGED
@@ -109,7 +109,7 @@ async def analytics_activity(
109
  ):
110
  """
111
  Returns general tenant activity statistics.
112
- Includes total queries, active users, and last query timestamp.
113
  """
114
 
115
  if not x_tenant_id:
@@ -118,10 +118,14 @@ async def analytics_activity(
118
 
119
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
120
  activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp)
 
 
 
121
 
122
  return {
123
  "tenant_id": x_tenant_id,
124
  "activity": activity,
 
125
  "period_days": days
126
  }
127
 
 
109
  ):
110
  """
111
  Returns general tenant activity statistics.
112
+ Includes total queries, active users, last query timestamp, and individual activity records for heatmap visualization.
113
  """
114
 
115
  if not x_tenant_id:
 
118
 
119
  since_timestamp = int((datetime.now() - timedelta(days=days)).timestamp()) if days else None
120
  activity = analytics_store.get_activity_summary(x_tenant_id, since_timestamp)
121
+
122
+ # Also fetch individual activity records for heatmap visualization
123
+ activities = analytics_store.get_activity_records(x_tenant_id, since_timestamp)
124
 
125
  return {
126
  "tenant_id": x_tenant_id,
127
  "activity": activity,
128
+ "activities": activities, # Individual records with timestamps for heatmap
129
  "period_days": days
130
  }
131
 
backend/api/services/rule_enhancer.py CHANGED
@@ -51,7 +51,7 @@ class RuleEnhancer:
51
 
52
  context_text = f"\nAdditional context: {context}" if context else ""
53
 
54
- prompt = f"""You are an expert in policy rule analysis and pattern matching. Analyze the following rule and enhance it.
55
 
56
  Original Rule: "{rule_text}"
57
 
@@ -60,12 +60,15 @@ Existing Rules (for context):
60
  {context_text}
61
 
62
  Your task:
63
- 1. Analyze the rule for potential edge cases and improvements
64
- 2. Generate an improved regex pattern that catches more variations
65
- 3. Write a clear, comprehensive description
66
- 4. Suggest an appropriate severity level (low/medium/high/critical)
67
- 5. Identify edge cases that might be missed
68
- 6. Suggest improvements
 
 
 
69
 
70
  Respond in JSON format with the following structure:
71
  {{
@@ -73,6 +76,17 @@ Respond in JSON format with the following structure:
73
  "pattern": "Improved regex pattern (e.g., '.*password.*|.*pwd.*|.*passcode.*')",
74
  "description": "Clear description of what this rule detects",
75
  "severity": "low|medium|high|critical",
 
 
 
 
 
 
 
 
 
 
 
76
  "edge_cases": ["Edge case 1", "Edge case 2", ...],
77
  "improvements": ["Improvement 1", "Improvement 2", ...],
78
  "keywords": ["keyword1", "keyword2", ...]
@@ -107,6 +121,9 @@ Only return valid JSON, no additional text:"""
107
  "pattern": enhanced_data.get("pattern", rule_text),
108
  "description": enhanced_data.get("description", rule_text),
109
  "severity": enhanced_data.get("severity", "medium"),
 
 
 
110
  "edge_cases": enhanced_data.get("edge_cases", []),
111
  "improvements": enhanced_data.get("improvements", []),
112
  "keywords": enhanced_data.get("keywords", [])
@@ -119,27 +136,35 @@ Only return valid JSON, no additional text:"""
119
  return result
120
 
121
  except asyncio.TimeoutError:
122
- # Timeout - return original rule
123
  print(f"LLM enhancement timeout for rule: {rule_text[:50]}...")
 
124
  return {
125
  "rule": rule_text,
126
  "pattern": rule_text,
127
  "description": rule_text,
128
  "severity": "medium",
 
 
 
129
  "edge_cases": [],
130
- "improvements": ["Enhancement timed out - using original rule"],
131
  "keywords": []
132
  }
133
  except Exception as e:
134
- # Fallback to original rule if LLM fails
135
  print(f"LLM enhancement error: {e}")
 
136
  return {
137
  "rule": rule_text,
138
  "pattern": rule_text,
139
  "description": rule_text,
140
  "severity": "medium",
 
 
 
141
  "edge_cases": [],
142
- "improvements": [f"Enhancement failed: {str(e)[:50]}"],
143
  "keywords": []
144
  }
145
 
@@ -170,15 +195,111 @@ Only return valid JSON, no additional text:"""
170
  # If enhancement fails for one rule, use original rule
171
  # This ensures other rules can still be processed
172
  print(f"Warning: Rule {i+1}/{len(rules)} enhancement failed: {e}")
 
 
173
  enhanced_rules.append({
174
  "rule": rule,
175
  "pattern": rule,
176
  "description": rule,
177
  "severity": "medium",
 
 
 
178
  "edge_cases": [],
179
- "improvements": [f"Enhancement skipped due to error"],
180
  "keywords": []
181
  })
182
 
183
  return enhanced_rules
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
 
51
 
52
  context_text = f"\nAdditional context: {context}" if context else ""
53
 
54
+ prompt = f"""You are an expert in policy rule analysis and pattern matching. Analyze the following rule and provide comprehensive enhancements.
55
 
56
  Original Rule: "{rule_text}"
57
 
 
60
  {context_text}
61
 
62
  Your task:
63
+ 1. Generate a human-readable explanation of what this rule does (2-3 sentences, plain English)
64
+ 2. Provide 5-8 concrete examples of text/phrases that would match this rule's pattern
65
+ 3. Suggest 3-5 missing patterns or variations that should also be caught
66
+ 4. Analyze the rule for potential edge cases and improvements
67
+ 5. Generate an improved regex pattern that catches more variations
68
+ 6. Write a clear, comprehensive description
69
+ 7. Suggest an appropriate severity level (low/medium/high/critical)
70
+ 8. Identify edge cases that might be missed
71
+ 9. Suggest improvements
72
 
73
  Respond in JSON format with the following structure:
74
  {{
 
76
  "pattern": "Improved regex pattern (e.g., '.*password.*|.*pwd.*|.*passcode.*')",
77
  "description": "Clear description of what this rule detects",
78
  "severity": "low|medium|high|critical",
79
+ "explanation": "Human-readable explanation in 2-3 sentences explaining what this rule does and why it's important",
80
+ "examples": [
81
+ "Example text that would match: 'Please share your password'",
82
+ "Another example: 'My pwd is 12345'",
83
+ "More examples..."
84
+ ],
85
+ "missing_patterns": [
86
+ "Pattern variation 1 that should be considered",
87
+ "Pattern variation 2 that should be considered",
88
+ "More suggestions..."
89
+ ],
90
  "edge_cases": ["Edge case 1", "Edge case 2", ...],
91
  "improvements": ["Improvement 1", "Improvement 2", ...],
92
  "keywords": ["keyword1", "keyword2", ...]
 
121
  "pattern": enhanced_data.get("pattern", rule_text),
122
  "description": enhanced_data.get("description", rule_text),
123
  "severity": enhanced_data.get("severity", "medium"),
124
+ "explanation": enhanced_data.get("explanation", f"This rule detects: {rule_text}"),
125
+ "examples": enhanced_data.get("examples", []),
126
+ "missing_patterns": enhanced_data.get("missing_patterns", []),
127
  "edge_cases": enhanced_data.get("edge_cases", []),
128
  "improvements": enhanced_data.get("improvements", []),
129
  "keywords": enhanced_data.get("keywords", [])
 
136
  return result
137
 
138
  except asyncio.TimeoutError:
139
+ # Timeout - generate basic explanation without LLM
140
  print(f"LLM enhancement timeout for rule: {rule_text[:50]}...")
141
+ basic_explanation = self._generate_basic_explanation(rule_text)
142
  return {
143
  "rule": rule_text,
144
  "pattern": rule_text,
145
  "description": rule_text,
146
  "severity": "medium",
147
+ "explanation": basic_explanation["explanation"],
148
+ "examples": basic_explanation["examples"],
149
+ "missing_patterns": basic_explanation["missing_patterns"],
150
  "edge_cases": [],
151
+ "improvements": ["Enhancement timed out - using basic explanation"],
152
  "keywords": []
153
  }
154
  except Exception as e:
155
+ # Fallback to basic explanation if LLM fails
156
  print(f"LLM enhancement error: {e}")
157
+ basic_explanation = self._generate_basic_explanation(rule_text)
158
  return {
159
  "rule": rule_text,
160
  "pattern": rule_text,
161
  "description": rule_text,
162
  "severity": "medium",
163
+ "explanation": basic_explanation["explanation"],
164
+ "examples": basic_explanation["examples"],
165
+ "missing_patterns": basic_explanation["missing_patterns"],
166
  "edge_cases": [],
167
+ "improvements": [f"Enhancement failed - using basic explanation"],
168
  "keywords": []
169
  }
170
 
 
195
  # If enhancement fails for one rule, use original rule
196
  # This ensures other rules can still be processed
197
  print(f"Warning: Rule {i+1}/{len(rules)} enhancement failed: {e}")
198
+ # Generate basic explanation even on error
199
+ basic_explanation = self._generate_basic_explanation(rule)
200
  enhanced_rules.append({
201
  "rule": rule,
202
  "pattern": rule,
203
  "description": rule,
204
  "severity": "medium",
205
+ "explanation": basic_explanation["explanation"],
206
+ "examples": basic_explanation["examples"],
207
+ "missing_patterns": basic_explanation["missing_patterns"],
208
  "edge_cases": [],
209
+ "improvements": [f"Enhancement skipped - using basic explanation"],
210
  "keywords": []
211
  })
212
 
213
  return enhanced_rules
214
+
215
+ def _generate_basic_explanation(self, rule_text: str) -> Dict[str, Any]:
216
+ """
217
+ Generate a basic explanation without LLM when enhancement fails or times out.
218
+ Uses pattern matching and keyword extraction to provide useful information.
219
+ """
220
+ rule_lower = rule_text.lower()
221
+
222
+ # Extract key concepts
223
+ keywords = []
224
+ if any(word in rule_lower for word in ["password", "pwd", "passcode", "credential"]):
225
+ keywords.append("authentication credentials")
226
+ if any(word in rule_lower for word in ["api", "key", "token", "secret"]):
227
+ keywords.append("API keys and tokens")
228
+ if any(word in rule_lower for word in ["credit", "card", "payment", "bank"]):
229
+ keywords.append("financial information")
230
+ if any(word in rule_lower for word in ["share", "send", "disclose", "reveal"]):
231
+ keywords.append("information sharing")
232
+ if any(word in rule_lower for word in ["prevent", "block", "stop", "deny"]):
233
+ keywords.append("prevention")
234
+ if any(word in rule_lower for word in ["sensitive", "private", "confidential"]):
235
+ keywords.append("sensitive data")
236
+
237
+ # Generate explanation
238
+ if keywords:
239
+ explanation = f"This rule is designed to prevent sharing of {', '.join(keywords)}. It monitors conversations to detect attempts to disclose sensitive information that could compromise security or privacy."
240
+ else:
241
+ explanation = f"This rule monitors for: {rule_text}. It helps maintain security and compliance by detecting potentially sensitive information sharing."
242
+
243
+ # Generate basic examples based on keywords
244
+ examples = []
245
+ if "password" in rule_lower or "credential" in rule_lower:
246
+ examples.extend([
247
+ "Can you share your password?",
248
+ "My password is 12345",
249
+ "What's your login pwd?",
250
+ "Here's my passcode: 9876",
251
+ "The credentials are admin/password123"
252
+ ])
253
+ if "api" in rule_lower or "key" in rule_lower:
254
+ examples.extend([
255
+ "My API key is sk-1234567890",
256
+ "Here's the access token: xyz123",
257
+ "The secret key is abc-def-ghi",
258
+ "API token: bearer_abc123xyz"
259
+ ])
260
+ if "credit" in rule_lower or "card" in rule_lower:
261
+ examples.extend([
262
+ "My credit card number is 4532-1234-5678-9010",
263
+ "CVV is 123",
264
+ "Card expiry: 12/25"
265
+ ])
266
+ if "sensitive" in rule_lower or "authentication" in rule_lower:
267
+ examples.extend([
268
+ "Here's my login info",
269
+ "I'll send you the credentials",
270
+ "The password is...",
271
+ "Can I share my account details?"
272
+ ])
273
+ if not examples:
274
+ # Generic examples based on rule text
275
+ examples = [
276
+ f"Example: '{rule_text[:40]}...'",
277
+ "Similar variations of the rule text",
278
+ "Related phrases containing key terms from the rule"
279
+ ]
280
+
281
+ # Suggest missing patterns
282
+ missing_patterns = []
283
+ if "password" in rule_lower:
284
+ missing_patterns.extend([
285
+ "Consider variations: 'pwd', 'passcode', 'login credentials', 'auth info'"
286
+ ])
287
+ if "api" in rule_lower or "key" in rule_lower:
288
+ missing_patterns.extend([
289
+ "Consider: 'access token', 'secret key', 'auth token', 'bearer token'"
290
+ ])
291
+ if "share" in rule_lower:
292
+ missing_patterns.extend([
293
+ "Consider action verbs: 'send', 'disclose', 'reveal', 'provide', 'give'"
294
+ ])
295
+ if "sensitive" in rule_lower:
296
+ missing_patterns.extend([
297
+ "Consider synonyms: 'confidential', 'private', 'secret', 'classified'"
298
+ ])
299
+
300
+ return {
301
+ "explanation": explanation,
302
+ "examples": examples[:8], # Limit to 8 examples
303
+ "missing_patterns": missing_patterns[:5] # Limit to 5 suggestions
304
+ }
305
 
backend/api/storage/analytics_store.py CHANGED
@@ -454,6 +454,46 @@ class AnalyticsStore:
454
  if last_query_ts
455
  else None,
456
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
  def get_rag_quality_metrics(
459
  self,
 
454
  if last_query_ts
455
  else None,
456
  }
457
+
458
+ def get_activity_records(
459
+ self,
460
+ tenant_id: str,
461
+ since_timestamp: Optional[int] = None,
462
+ limit: Optional[int] = None
463
+ ) -> List[Dict[str, Any]]:
464
+ """Get individual activity records (agent queries) with timestamps for heatmap visualization."""
465
+ if not self.supabase_client:
466
+ raise RuntimeError("Supabase client not initialized. Cannot get activity records.")
467
+
468
+ # Fetch agent query events (these represent user queries/activity)
469
+ queries = self._supabase_simple_select(
470
+ self.table_names["agent_query"],
471
+ tenant_id,
472
+ since_timestamp=since_timestamp,
473
+ limit=limit,
474
+ order_desc=False, # Chronological order for heatmap
475
+ )
476
+
477
+ # Format records for frontend consumption
478
+ activities = []
479
+ for query in queries:
480
+ timestamp = query.get("timestamp")
481
+ if timestamp:
482
+ # Convert timestamp to ISO format if it's a Unix timestamp
483
+ if isinstance(timestamp, (int, float)):
484
+ timestamp_iso = datetime.fromtimestamp(timestamp).isoformat()
485
+ else:
486
+ timestamp_iso = str(timestamp)
487
+
488
+ activities.append({
489
+ "timestamp": timestamp_iso,
490
+ "created_at": timestamp_iso, # Alias for compatibility
491
+ "type": "query",
492
+ "user_id": query.get("user_id"),
493
+ "message_preview": query.get("message_preview", "")[:100],
494
+ })
495
+
496
+ return activities
497
 
498
  def get_rag_quality_metrics(
499
  self,
backend/mcp_server/common/access_control.py CHANGED
@@ -11,7 +11,7 @@ PERMISSIONS: dict[str, Set[str]] = {
11
  "manage_rules": {"owner", "admin"},
12
  "ingest_documents": {"owner", "admin", "editor"},
13
  "delete_documents": {"owner", "admin"},
14
- "view_analytics": {"owner", "admin"},
15
  }
16
 
17
  # Mapping of MCP tool names to enterprise actions
 
11
  "manage_rules": {"owner", "admin"},
12
  "ingest_documents": {"owner", "admin", "editor"},
13
  "delete_documents": {"owner", "admin"},
14
+ "view_analytics": {"viewer", "editor", "admin", "owner"}, # All roles can view analytics
15
  }
16
 
17
  # Mapping of MCP tool names to enterprise actions
frontend/app/admin-rules/page.tsx CHANGED
@@ -1,9 +1,10 @@
1
  "use client";
2
 
3
- import { useCallback, useMemo, useState, useRef, useEffect } from "react";
4
  import Link from "next/link";
5
 
6
  import { AdminRulesPanel } from "@/components/admin-rules-panel";
 
7
  import { Footer } from "@/components/footer";
8
  import { useTenant } from "@/contexts/TenantContext";
9
  import { TenantSelector } from "@/components/tenant-selector";
@@ -50,6 +51,9 @@ export default function AdminRulesPage() {
50
  const [status, setStatus] = useState<StatusState>(null);
51
  const [isDragging, setIsDragging] = useState(false);
52
  const [lastUpdated, setLastUpdated] = useState<string>("");
 
 
 
53
  const fileInputRef = useRef<HTMLInputElement>(null);
54
 
55
  // Set initial time only on client side to avoid hydration mismatch
@@ -123,6 +127,25 @@ export default function AdminRulesPage() {
123
  const data = await response.json();
124
  await handleRefresh();
125
  setRulesInput("");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : "";
127
  setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || lines.length} rule(s)${enhancedMsg}.` });
128
  } catch (error: any) {
@@ -173,6 +196,25 @@ export default function AdminRulesPage() {
173
 
174
  const data = await response.json();
175
  await handleRefresh();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : "";
177
  setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || lines.length} rule(s) from ${file.name}${enhancedMsg}.` });
178
  return;
@@ -198,6 +240,25 @@ export default function AdminRulesPage() {
198
 
199
  const data = await response.json();
200
  await handleRefresh();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : "";
202
  setStatus({
203
  tone: "success",
@@ -248,6 +309,71 @@ export default function AdminRulesPage() {
248
  await processFile(file);
249
  }, [processFile]);
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  const handleDelete = useCallback(async () => {
252
  if (!requireTenant()) return;
253
  if (!deleteInput.trim()) {
@@ -578,15 +704,66 @@ export default function AdminRulesPage() {
578
  </td>
579
  </tr>
580
  )}
581
- {rules.map((rule, idx) => (
582
- <tr
583
- key={`${rule}-${idx}`}
584
- className="border-t border-white/5 transition hover:bg-white/5"
585
- >
586
- <td className="px-6 py-4 text-slate-400 font-mono">{idx + 1}</td>
587
- <td className="px-6 py-4 text-slate-200">{rule}</td>
588
- </tr>
589
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  </tbody>
591
  </table>
592
  </div>
 
1
  "use client";
2
 
3
+ import React, { useCallback, useMemo, useState, useRef, useEffect } from "react";
4
  import Link from "next/link";
5
 
6
  import { AdminRulesPanel } from "@/components/admin-rules-panel";
7
+ import { RuleExplanation } from "@/components/rule-explanation";
8
  import { Footer } from "@/components/footer";
9
  import { useTenant } from "@/contexts/TenantContext";
10
  import { TenantSelector } from "@/components/tenant-selector";
 
51
  const [status, setStatus] = useState<StatusState>(null);
52
  const [isDragging, setIsDragging] = useState(false);
53
  const [lastUpdated, setLastUpdated] = useState<string>("");
54
+ const [ruleExplanations, setRuleExplanations] = useState<Record<string, any>>({});
55
+ const [expandedRules, setExpandedRules] = useState<Set<string>>(new Set());
56
+ const [loadingExplanations, setLoadingExplanations] = useState<Set<string>>(new Set());
57
  const fileInputRef = useRef<HTMLInputElement>(null);
58
 
59
  // Set initial time only on client side to avoid hydration mismatch
 
127
  const data = await response.json();
128
  await handleRefresh();
129
  setRulesInput("");
130
+
131
+ // Store explanations for display and auto-expand
132
+ if (data.explanations && Array.isArray(data.explanations)) {
133
+ const explanationsMap: Record<string, any> = {};
134
+ const newExpanded = new Set(expandedRules);
135
+
136
+ data.explanations.forEach((exp: any) => {
137
+ if (exp.rule) {
138
+ explanationsMap[exp.rule] = exp;
139
+ // Auto-expand rules that have explanations
140
+ if (exp.explanation || exp.examples || exp.missing_patterns) {
141
+ newExpanded.add(exp.rule);
142
+ }
143
+ }
144
+ });
145
+ setRuleExplanations((prev) => ({ ...prev, ...explanationsMap }));
146
+ setExpandedRules(newExpanded);
147
+ }
148
+
149
  const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : "";
150
  setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || lines.length} rule(s)${enhancedMsg}.` });
151
  } catch (error: any) {
 
196
 
197
  const data = await response.json();
198
  await handleRefresh();
199
+
200
+ // Store explanations for display and auto-expand
201
+ if (data.explanations && Array.isArray(data.explanations)) {
202
+ const explanationsMap: Record<string, any> = {};
203
+ const newExpanded = new Set(expandedRules);
204
+
205
+ data.explanations.forEach((exp: any) => {
206
+ if (exp.rule) {
207
+ explanationsMap[exp.rule] = exp;
208
+ // Auto-expand rules that have explanations
209
+ if (exp.explanation || exp.examples || exp.missing_patterns) {
210
+ newExpanded.add(exp.rule);
211
+ }
212
+ }
213
+ });
214
+ setRuleExplanations((prev) => ({ ...prev, ...explanationsMap }));
215
+ setExpandedRules(newExpanded);
216
+ }
217
+
218
  const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : "";
219
  setStatus({ tone: "success", message: `Uploaded ${data.added_rules?.length || lines.length} rule(s) from ${file.name}${enhancedMsg}.` });
220
  return;
 
240
 
241
  const data = await response.json();
242
  await handleRefresh();
243
+
244
+ // Store explanations for display and auto-expand
245
+ if (data.explanations && Array.isArray(data.explanations)) {
246
+ const explanationsMap: Record<string, any> = {};
247
+ const newExpanded = new Set(expandedRules);
248
+
249
+ data.explanations.forEach((exp: any) => {
250
+ if (exp.rule) {
251
+ explanationsMap[exp.rule] = exp;
252
+ // Auto-expand rules that have explanations
253
+ if (exp.explanation || exp.examples || exp.missing_patterns) {
254
+ newExpanded.add(exp.rule);
255
+ }
256
+ }
257
+ });
258
+ setRuleExplanations((prev) => ({ ...prev, ...explanationsMap }));
259
+ setExpandedRules(newExpanded);
260
+ }
261
+
262
  const enhancedMsg = data.enhanced ? " (enhanced by LLM)" : "";
263
  setStatus({
264
  tone: "success",
 
309
  await processFile(file);
310
  }, [processFile]);
311
 
312
+ const fetchRuleExplanation = useCallback(async (rule: string) => {
313
+ if (!requireTenant()) return;
314
+ if (ruleExplanations[rule]) return; // Already have explanation
315
+
316
+ try {
317
+ setLoadingExplanations((prev) => new Set(prev).add(rule));
318
+
319
+ // Fetch explanation by calling the enhance endpoint
320
+ // We'll use POST with the rule in the body to get explanation
321
+ const response = await fetch(
322
+ `${BACKEND_BASE_URL}/admin/rules?enhance=true`,
323
+ {
324
+ method: "POST",
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ "x-tenant-id": tenantId.trim(),
328
+ "x-user-role": role,
329
+ },
330
+ body: JSON.stringify({ rule }),
331
+ }
332
+ );
333
+
334
+ if (response.ok) {
335
+ const data = await response.json();
336
+ if (data.explanation || data.examples || data.missing_patterns) {
337
+ setRuleExplanations((prev) => ({
338
+ ...prev,
339
+ [rule]: {
340
+ explanation: data.explanation,
341
+ examples: data.examples || [],
342
+ missing_patterns: data.missing_patterns || [],
343
+ edge_cases: data.edge_cases || [],
344
+ improvements: data.improvements || [],
345
+ severity: data.severity,
346
+ },
347
+ }));
348
+ }
349
+ }
350
+ } catch (error) {
351
+ console.error("Failed to fetch rule explanation:", error);
352
+ } finally {
353
+ setLoadingExplanations((prev) => {
354
+ const next = new Set(prev);
355
+ next.delete(rule);
356
+ return next;
357
+ });
358
+ }
359
+ }, [tenantId, role, ruleExplanations, requireTenant]);
360
+
361
+ const toggleRuleExplanation = useCallback((rule: string) => {
362
+ setExpandedRules((prev) => {
363
+ const next = new Set(prev);
364
+ if (next.has(rule)) {
365
+ next.delete(rule);
366
+ } else {
367
+ next.add(rule);
368
+ // Fetch explanation if we don't have it
369
+ if (!ruleExplanations[rule]) {
370
+ fetchRuleExplanation(rule);
371
+ }
372
+ }
373
+ return next;
374
+ });
375
+ }, [ruleExplanations, fetchRuleExplanation]);
376
+
377
  const handleDelete = useCallback(async () => {
378
  if (!requireTenant()) return;
379
  if (!deleteInput.trim()) {
 
704
  </td>
705
  </tr>
706
  )}
707
+ {rules.map((rule, idx) => {
708
+ const explanation = ruleExplanations[rule];
709
+ const isExpanded = expandedRules.has(rule);
710
+ const isLoading = loadingExplanations.has(rule);
711
+ return (
712
+ <React.Fragment key={`${rule}-${idx}`}>
713
+ <tr className="border-t border-white/5 transition hover:bg-white/5">
714
+ <td className="px-6 py-4 text-slate-400 font-mono">{idx + 1}</td>
715
+ <td className="px-6 py-4">
716
+ <div className="flex items-center justify-between gap-3">
717
+ <span className="text-slate-200 flex-1">{rule}</span>
718
+ <button
719
+ onClick={() => toggleRuleExplanation(rule)}
720
+ className="flex items-center gap-2 rounded-lg border border-cyan-500/30 bg-cyan-500/10 px-3 py-1.5 text-xs font-semibold text-cyan-300 transition hover:bg-cyan-500/20 hover:border-cyan-400/50"
721
+ title={isExpanded ? "Hide explanation" : "Show explanation"}
722
+ >
723
+ {isLoading ? (
724
+ <>
725
+ <span className="animate-spin">⏳</span>
726
+ <span>Loading...</span>
727
+ </>
728
+ ) : isExpanded ? (
729
+ <>
730
+ <span>▼</span>
731
+ <span>Hide</span>
732
+ </>
733
+ ) : (
734
+ <>
735
+ <span>▶</span>
736
+ <span>Explain</span>
737
+ </>
738
+ )}
739
+ </button>
740
+ </div>
741
+ </td>
742
+ </tr>
743
+ {isExpanded && explanation && (
744
+ <tr>
745
+ <td colSpan={2} className="px-6 py-4">
746
+ <RuleExplanation
747
+ explanation={explanation.explanation}
748
+ examples={explanation.examples}
749
+ missingPatterns={explanation.missing_patterns}
750
+ edgeCases={explanation.edge_cases}
751
+ improvements={explanation.improvements}
752
+ severity={explanation.severity}
753
+ />
754
+ </td>
755
+ </tr>
756
+ )}
757
+ {isExpanded && !explanation && !isLoading && (
758
+ <tr>
759
+ <td colSpan={2} className="px-6 py-4 text-center text-slate-400 text-sm">
760
+ No explanation available for this rule.
761
+ </td>
762
+ </tr>
763
+ )}
764
+ </React.Fragment>
765
+ );
766
+ })}
767
  </tbody>
768
  </table>
769
  </div>
frontend/components/rule-explanation.tsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ type RuleExplanationProps = {
4
+ explanation?: string;
5
+ examples?: string[];
6
+ missingPatterns?: string[];
7
+ edgeCases?: string[];
8
+ improvements?: string[];
9
+ severity?: string;
10
+ };
11
+
12
+ export function RuleExplanation({
13
+ explanation,
14
+ examples = [],
15
+ missingPatterns = [],
16
+ edgeCases = [],
17
+ improvements = [],
18
+ severity,
19
+ }: RuleExplanationProps) {
20
+ if (!explanation && examples.length === 0 && missingPatterns.length === 0) {
21
+ return null;
22
+ }
23
+
24
+ const severityColors = {
25
+ low: "bg-blue-500/20 border-blue-500/50 text-blue-200",
26
+ medium: "bg-yellow-500/20 border-yellow-500/50 text-yellow-200",
27
+ high: "bg-orange-500/20 border-orange-500/50 text-orange-200",
28
+ critical: "bg-red-500/20 border-red-500/50 text-red-200",
29
+ };
30
+
31
+ const severityColor = severityColors[severity as keyof typeof severityColors] || severityColors.medium;
32
+
33
+ return (
34
+ <div className="mt-4 space-y-4 rounded-2xl border border-white/10 bg-slate-950/60 p-6">
35
+ {/* Explanation */}
36
+ {explanation && (
37
+ <div>
38
+ <h4 className="mb-2 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-cyan-300">
39
+ <span>💡</span> Explanation
40
+ </h4>
41
+ <p className="text-sm leading-relaxed text-slate-200">{explanation}</p>
42
+ </div>
43
+ )}
44
+
45
+ {/* Examples */}
46
+ {examples.length > 0 && (
47
+ <div>
48
+ <h4 className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-emerald-300">
49
+ <span>📋</span> Examples This Rule Would Catch
50
+ </h4>
51
+ <div className="space-y-2">
52
+ {examples.map((example, idx) => (
53
+ <div
54
+ key={idx}
55
+ className="rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-4 py-2.5 text-sm text-slate-100"
56
+ >
57
+ <span className="font-mono text-emerald-300">"{example}"</span>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ </div>
62
+ )}
63
+
64
+ {/* Missing Patterns */}
65
+ {missingPatterns.length > 0 && (
66
+ <div>
67
+ <h4 className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-amber-300">
68
+ <span>🔍</span> Suggested Missing Patterns
69
+ </h4>
70
+ <div className="space-y-2">
71
+ {missingPatterns.map((pattern, idx) => (
72
+ <div
73
+ key={idx}
74
+ className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-2.5 text-sm text-slate-100"
75
+ >
76
+ <span className="font-mono text-amber-300">{pattern}</span>
77
+ </div>
78
+ ))}
79
+ </div>
80
+ </div>
81
+ )}
82
+
83
+ {/* Edge Cases */}
84
+ {edgeCases.length > 0 && (
85
+ <div>
86
+ <h4 className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-purple-300">
87
+ <span>⚠️</span> Edge Cases Identified
88
+ </h4>
89
+ <ul className="space-y-1.5">
90
+ {edgeCases.map((edgeCase, idx) => (
91
+ <li key={idx} className="text-sm text-slate-300">
92
+ • {edgeCase}
93
+ </li>
94
+ ))}
95
+ </ul>
96
+ </div>
97
+ )}
98
+
99
+ {/* Improvements */}
100
+ {improvements.length > 0 && (
101
+ <div>
102
+ <h4 className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-cyan-300">
103
+ <span>✨</span> Improvements Applied
104
+ </h4>
105
+ <ul className="space-y-1.5">
106
+ {improvements.map((improvement, idx) => (
107
+ <li key={idx} className="text-sm text-slate-300">
108
+ • {improvement}
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ </div>
113
+ )}
114
+
115
+ {/* Severity Badge */}
116
+ {severity && (
117
+ <div className="pt-2">
118
+ <span
119
+ className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wider ${severityColor}`}
120
+ >
121
+ <span>🛡️</span>
122
+ Severity: {severity}
123
+ </span>
124
+ </div>
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
frontend/components/tenant-heatmap.tsx CHANGED
@@ -26,7 +26,7 @@ const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
26
  const HOURS = Array.from({ length: 24 }, (_, i) => i);
27
 
28
  export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
29
- const { tenantId, userRole } = useTenant();
30
  const [loading, setLoading] = useState(false);
31
  const [heatmapData, setHeatmapData] = useState<HeatmapData[]>([]);
32
  const [toolTrends, setToolTrends] = useState<ToolUsageTrend[]>([]);
@@ -44,7 +44,7 @@ export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
44
  {
45
  headers: {
46
  "x-tenant-id": tenantId,
47
- "x-user-role": userRole,
48
  },
49
  },
50
  );
@@ -61,7 +61,7 @@ export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
61
  {
62
  headers: {
63
  "x-tenant-id": tenantId,
64
- "x-user-role": userRole,
65
  },
66
  },
67
  );
@@ -77,14 +77,22 @@ export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
77
  const activityByHour: Record<string, number> = {};
78
 
79
  // Group activities by hour and day
80
- if (activityData.activities) {
81
  activityData.activities.forEach((activity: any) => {
82
- const timestamp = new Date(activity.timestamp || activity.created_at);
83
- const hour = timestamp.getHours();
84
- const day = timestamp.getDay();
85
- const key = `${day}-${hour}`;
86
-
87
- activityByHour[key] = (activityByHour[key] || 0) + 1;
 
 
 
 
 
 
 
 
88
  });
89
  }
90
 
@@ -117,13 +125,17 @@ export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
117
  setToolTrends(trends.slice(0, 10)); // Top 10 tools
118
  } catch (err) {
119
  console.error("Failed to fetch heatmap data:", err);
 
 
 
 
120
  } finally {
121
  setLoading(false);
122
  }
123
  }
124
 
125
  fetchHeatmapData();
126
- }, [tenantId, userRole, days]);
127
 
128
  const getIntensity = (count: number) => {
129
  if (maxCount === 0) return 0;
 
26
  const HOURS = Array.from({ length: 24 }, (_, i) => i);
27
 
28
  export function TenantHeatmap({ days = 7 }: TenantHeatmapProps) {
29
+ const { tenantId, role } = useTenant();
30
  const [loading, setLoading] = useState(false);
31
  const [heatmapData, setHeatmapData] = useState<HeatmapData[]>([]);
32
  const [toolTrends, setToolTrends] = useState<ToolUsageTrend[]>([]);
 
44
  {
45
  headers: {
46
  "x-tenant-id": tenantId,
47
+ "x-user-role": role,
48
  },
49
  },
50
  );
 
61
  {
62
  headers: {
63
  "x-tenant-id": tenantId,
64
+ "x-user-role": role,
65
  },
66
  },
67
  );
 
77
  const activityByHour: Record<string, number> = {};
78
 
79
  // Group activities by hour and day
80
+ if (activityData.activities && Array.isArray(activityData.activities)) {
81
  activityData.activities.forEach((activity: any) => {
82
+ try {
83
+ const timestamp = new Date(activity.timestamp || activity.created_at);
84
+ // Check if date is valid
85
+ if (!isNaN(timestamp.getTime())) {
86
+ const hour = timestamp.getHours();
87
+ const day = timestamp.getDay();
88
+ const key = `${day}-${hour}`;
89
+
90
+ activityByHour[key] = (activityByHour[key] || 0) + 1;
91
+ }
92
+ } catch (e) {
93
+ // Skip invalid timestamps
94
+ console.warn("Invalid timestamp in activity:", activity);
95
+ }
96
  });
97
  }
98
 
 
125
  setToolTrends(trends.slice(0, 10)); // Top 10 tools
126
  } catch (err) {
127
  console.error("Failed to fetch heatmap data:", err);
128
+ // Set empty data on error to show empty state
129
+ setHeatmapData([]);
130
+ setToolTrends([]);
131
+ setMaxCount(1);
132
  } finally {
133
  setLoading(false);
134
  }
135
  }
136
 
137
  fetchHeatmapData();
138
+ }, [tenantId, role, days]);
139
 
140
  const getIntensity = (count: number) => {
141
  if (maxCount === 0) return 0;