Spaces:
Sleeping
Sleeping
Claude Code Claude Opus 4.6 commited on
Commit ·
cc1784f
1
Parent(s): d43d04a
feat: Add chat analytics dashboard to Cain
Browse files- Create app_analytics.py with Gradio.Blocks interface
- Add load_analytics_data() function to read from session-archive.jsonl
- Implement 4 chart types: line (messages/hour), bar (agent responses), pie (sentiment), DataFrame (top sessions)
- Add Analytics tab to main app.py with embedded dashboard
- Use Plotly for interactive charts with minimal functional design
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app.py +61 -0
- app_analytics.py +281 -0
app.py
CHANGED
|
@@ -1845,6 +1845,67 @@ def create_agent_office():
|
|
| 1845 |
label="Session Statistics"
|
| 1846 |
)
|
| 1847 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1848 |
# ========== Footer ==========
|
| 1849 |
gr.Markdown("---")
|
| 1850 |
with gr.Row():
|
|
|
|
| 1845 |
label="Session Statistics"
|
| 1846 |
)
|
| 1847 |
|
| 1848 |
+
# ========== Tab: Analytics Dashboard ==========
|
| 1849 |
+
with gr.Tab("📊 Analytics"):
|
| 1850 |
+
gr.Markdown("## 📊 Chat Analytics Dashboard")
|
| 1851 |
+
gr.Markdown("Visualize conversation patterns and agent performance metrics.")
|
| 1852 |
+
|
| 1853 |
+
# Import analytics functions
|
| 1854 |
+
from app_analytics import load_analytics_data, create_line_chart, create_bar_chart, create_pie_chart
|
| 1855 |
+
import pandas as pd
|
| 1856 |
+
|
| 1857 |
+
# Load initial data
|
| 1858 |
+
initial_analytics = load_analytics_data()
|
| 1859 |
+
|
| 1860 |
+
with gr.Row():
|
| 1861 |
+
analytics_refresh_btn = gr.Button("🔄 Refresh Analytics", variant="primary")
|
| 1862 |
+
|
| 1863 |
+
with gr.Row():
|
| 1864 |
+
with gr.Column(scale=1):
|
| 1865 |
+
analytics_line_plot = gr.Plot(
|
| 1866 |
+
value=create_line_chart(initial_analytics["messages_per_hour"]),
|
| 1867 |
+
label="Messages per Hour (Last 24h)"
|
| 1868 |
+
)
|
| 1869 |
+
with gr.Column(scale=1):
|
| 1870 |
+
analytics_bar_plot = gr.Plot(
|
| 1871 |
+
value=create_bar_chart(initial_analytics["agent_distribution"]),
|
| 1872 |
+
label="Agent Response Distribution"
|
| 1873 |
+
)
|
| 1874 |
+
|
| 1875 |
+
with gr.Row():
|
| 1876 |
+
with gr.Column(scale=1):
|
| 1877 |
+
analytics_pie_plot = gr.Plot(
|
| 1878 |
+
value=create_pie_chart(initial_analytics["sentiment_counts"]),
|
| 1879 |
+
label="Sentiment Analysis"
|
| 1880 |
+
)
|
| 1881 |
+
with gr.Column(scale=1):
|
| 1882 |
+
analytics_df_data = initial_analytics["top_sessions"] if initial_analytics["top_sessions"] else [{
|
| 1883 |
+
"session": "No data", "activity_count": 0, "last_active": "N/A"
|
| 1884 |
+
}]
|
| 1885 |
+
analytics_sessions_df = gr.Dataframe(
|
| 1886 |
+
value=pd.DataFrame(analytics_df_data),
|
| 1887 |
+
label="Top 10 Active Sessions",
|
| 1888 |
+
headers=["Session", "Activity Count", "Last Active"],
|
| 1889 |
+
interactive=False
|
| 1890 |
+
)
|
| 1891 |
+
|
| 1892 |
+
# Analytics refresh handler
|
| 1893 |
+
def refresh_analytics_dashboard():
|
| 1894 |
+
data = load_analytics_data()
|
| 1895 |
+
line_fig = create_line_chart(data["messages_per_hour"])
|
| 1896 |
+
bar_fig = create_bar_chart(data["agent_distribution"])
|
| 1897 |
+
pie_fig = create_pie_chart(data["sentiment_counts"])
|
| 1898 |
+
df_data = data["top_sessions"] if data["top_sessions"] else [{
|
| 1899 |
+
"session": "No data", "activity_count": 0, "last_active": "N/A"
|
| 1900 |
+
}]
|
| 1901 |
+
df = pd.DataFrame(df_data)
|
| 1902 |
+
return line_fig, bar_fig, pie_fig, df
|
| 1903 |
+
|
| 1904 |
+
analytics_refresh_btn.click(
|
| 1905 |
+
fn=refresh_analytics_dashboard,
|
| 1906 |
+
outputs=[analytics_line_plot, analytics_bar_plot, analytics_pie_plot, analytics_sessions_df]
|
| 1907 |
+
)
|
| 1908 |
+
|
| 1909 |
# ========== Footer ==========
|
| 1910 |
gr.Markdown("---")
|
| 1911 |
with gr.Row():
|
app_analytics.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Chat Analytics Dashboard for HuggingClaw Cain
|
| 4 |
+
|
| 5 |
+
Visualizes conversation patterns and agent performance metrics.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from typing import Dict, List, Any, Optional
|
| 12 |
+
from collections import defaultdict, Counter
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import pandas as pd
|
| 16 |
+
import plotly.graph_objects as go
|
| 17 |
+
import plotly.express as px
|
| 18 |
+
|
| 19 |
+
# ========== Path Constants ==========
|
| 20 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 21 |
+
SESSION_ARCHIVE_PATH = BASE_DIR / ".openclaw" / "agents" / "logs" / "session-archive.jsonl"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_analytics_data() -> Dict[str, Any]:
|
| 25 |
+
"""
|
| 26 |
+
Load and parse analytics data from session-archive.jsonl.
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
Dictionary containing aggregated statistics:
|
| 30 |
+
- messages_per_hour: List of (hour, count) tuples for last 24h
|
| 31 |
+
- agent_distribution: Dict of agent names to response counts
|
| 32 |
+
- sentiment_counts: Dict of sentiment (positive/negative/neutral) to counts
|
| 33 |
+
- top_sessions: List of top 10 active session dicts
|
| 34 |
+
"""
|
| 35 |
+
default_result = {
|
| 36 |
+
"messages_per_hour": [],
|
| 37 |
+
"agent_distribution": {},
|
| 38 |
+
"sentiment_counts": {"positive": 0, "negative": 0, "neutral": 0},
|
| 39 |
+
"top_sessions": []
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
if not SESSION_ARCHIVE_PATH.exists():
|
| 43 |
+
return default_result
|
| 44 |
+
|
| 45 |
+
# Parse all records
|
| 46 |
+
records = []
|
| 47 |
+
try:
|
| 48 |
+
with open(SESSION_ARCHIVE_PATH, 'r') as f:
|
| 49 |
+
for line in f:
|
| 50 |
+
if line.strip():
|
| 51 |
+
try:
|
| 52 |
+
records.append(json.loads(line))
|
| 53 |
+
except json.JSONDecodeError:
|
| 54 |
+
continue
|
| 55 |
+
except Exception:
|
| 56 |
+
return default_result
|
| 57 |
+
|
| 58 |
+
if not records:
|
| 59 |
+
return default_result
|
| 60 |
+
|
| 61 |
+
# Parse timestamps and filter for last 24 hours
|
| 62 |
+
now = datetime.utcnow()
|
| 63 |
+
cutoff = now - timedelta(hours=24)
|
| 64 |
+
|
| 65 |
+
parsed_records = []
|
| 66 |
+
for r in records:
|
| 67 |
+
ts_str = r.get("timestamp")
|
| 68 |
+
if not ts_str:
|
| 69 |
+
continue
|
| 70 |
+
try:
|
| 71 |
+
# Handle ISO format with timezone
|
| 72 |
+
if ts_str.endswith("+00:00"):
|
| 73 |
+
ts_str = ts_str.replace("+00:00", "").replace("Z", "")
|
| 74 |
+
ts = datetime.fromisoformat(ts_str.replace("Z", ""))
|
| 75 |
+
parsed_records.append({
|
| 76 |
+
"timestamp": ts,
|
| 77 |
+
"agent": r.get("agent", "unknown"),
|
| 78 |
+
"state": r.get("state", "unknown"),
|
| 79 |
+
"action": r.get("action", "state_change"),
|
| 80 |
+
"type": r.get("type", "state_change")
|
| 81 |
+
})
|
| 82 |
+
except (ValueError, AttributeError):
|
| 83 |
+
continue
|
| 84 |
+
|
| 85 |
+
# Filter for last 24 hours for hourly chart
|
| 86 |
+
recent_records = [r for r in parsed_records if r["timestamp"] >= cutoff]
|
| 87 |
+
|
| 88 |
+
# 1. Messages per hour (last 24h)
|
| 89 |
+
hour_counts = defaultdict(int)
|
| 90 |
+
for r in recent_records:
|
| 91 |
+
hour_key = r["timestamp"].strftime("%Y-%m-%d %H:00")
|
| 92 |
+
hour_counts[hour_key] += 1
|
| 93 |
+
|
| 94 |
+
# Fill missing hours with 0
|
| 95 |
+
messages_per_hour = []
|
| 96 |
+
for i in range(24):
|
| 97 |
+
hour_time = now - timedelta(hours=23-i)
|
| 98 |
+
hour_key = hour_time.strftime("%Y-%m-%d %H:00")
|
| 99 |
+
messages_per_hour.append({
|
| 100 |
+
"hour": hour_time.strftime("%H:00"),
|
| 101 |
+
"count": hour_counts.get(hour_key, 0)
|
| 102 |
+
})
|
| 103 |
+
|
| 104 |
+
# 2. Agent response distribution (state changes by agent)
|
| 105 |
+
agent_counts = Counter(r["agent"] for r in parsed_records if r["agent"] != "unknown")
|
| 106 |
+
agent_distribution = dict(agent_counts.most_common())
|
| 107 |
+
|
| 108 |
+
# 3. Sentiment analysis (based on state: success/error/other)
|
| 109 |
+
sentiment_counts = {"positive": 0, "negative": 0, "neutral": 0}
|
| 110 |
+
for r in parsed_records:
|
| 111 |
+
state = r.get("state", "").lower()
|
| 112 |
+
if state == "success":
|
| 113 |
+
sentiment_counts["positive"] += 1
|
| 114 |
+
elif state in ("error", "failed"):
|
| 115 |
+
sentiment_counts["negative"] += 1
|
| 116 |
+
else:
|
| 117 |
+
sentiment_counts["neutral"] += 1
|
| 118 |
+
|
| 119 |
+
# 4. Top 10 active sessions (by agent activity)
|
| 120 |
+
session_activity = defaultdict(lambda: {"count": 0, "last_active": None})
|
| 121 |
+
for r in parsed_records:
|
| 122 |
+
agent = r["agent"]
|
| 123 |
+
session_activity[agent]["count"] += 1
|
| 124 |
+
if session_activity[agent]["last_active"] is None or r["timestamp"] > session_activity[agent]["last_active"]:
|
| 125 |
+
session_activity[agent]["last_active"] = r["timestamp"]
|
| 126 |
+
|
| 127 |
+
top_sessions = [
|
| 128 |
+
{
|
| 129 |
+
"session": agent,
|
| 130 |
+
"activity_count": data["count"],
|
| 131 |
+
"last_active": data["last_active"].strftime("%Y-%m-%d %H:%M:%S") if data["last_active"] else "N/A"
|
| 132 |
+
}
|
| 133 |
+
for agent, data in sorted(session_activity.items(), key=lambda x: x[1]["count"], reverse=True)[:10]
|
| 134 |
+
]
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"messages_per_hour": messages_per_hour,
|
| 138 |
+
"agent_distribution": agent_distribution,
|
| 139 |
+
"sentiment_counts": sentiment_counts,
|
| 140 |
+
"top_sessions": top_sessions
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def create_line_chart(data: List[Dict]) -> go.Figure:
|
| 145 |
+
"""Create line chart for messages per hour."""
|
| 146 |
+
hours = [d["hour"] for d in data]
|
| 147 |
+
counts = [d["count"] for d in data]
|
| 148 |
+
|
| 149 |
+
fig = go.Figure()
|
| 150 |
+
fig.add_trace(go.Scatter(
|
| 151 |
+
x=hours,
|
| 152 |
+
y=counts,
|
| 153 |
+
mode="lines+markers",
|
| 154 |
+
name="Messages",
|
| 155 |
+
line=dict(color="#3b82f6", width=2),
|
| 156 |
+
marker=dict(size=6)
|
| 157 |
+
))
|
| 158 |
+
|
| 159 |
+
fig.update_layout(
|
| 160 |
+
title="Messages per Hour (Last 24h)",
|
| 161 |
+
xaxis_title="Hour",
|
| 162 |
+
yaxis_title="Message Count",
|
| 163 |
+
hovermode="x unified",
|
| 164 |
+
height=300,
|
| 165 |
+
margin=dict(l=10, r=10, t=40, b=40)
|
| 166 |
+
)
|
| 167 |
+
return fig
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def create_bar_chart(data: Dict[str, int]) -> go.Figure:
|
| 171 |
+
"""Create bar chart for agent response distribution."""
|
| 172 |
+
agents = list(data.keys())
|
| 173 |
+
counts = list(data.values())
|
| 174 |
+
|
| 175 |
+
fig = go.Figure()
|
| 176 |
+
fig.add_trace(go.Bar(
|
| 177 |
+
x=agents,
|
| 178 |
+
y=counts,
|
| 179 |
+
marker_color="#10b981"
|
| 180 |
+
))
|
| 181 |
+
|
| 182 |
+
fig.update_layout(
|
| 183 |
+
title="Agent Response Distribution",
|
| 184 |
+
xaxis_title="Agent",
|
| 185 |
+
yaxis_title="Response Count",
|
| 186 |
+
height=300,
|
| 187 |
+
margin=dict(l=10, r=10, t=40, b=40)
|
| 188 |
+
)
|
| 189 |
+
return fig
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def create_pie_chart(data: Dict[str, int]) -> go.Figure:
|
| 193 |
+
"""Create pie chart for sentiment analysis."""
|
| 194 |
+
labels = list(data.keys())
|
| 195 |
+
values = list(data.values())
|
| 196 |
+
colors = ["#10b981", "#ef4444", "#6b7280"]
|
| 197 |
+
|
| 198 |
+
fig = go.Figure()
|
| 199 |
+
fig.add_trace(go.Pie(
|
| 200 |
+
labels=labels,
|
| 201 |
+
values=values,
|
| 202 |
+
marker=dict(colors=colors),
|
| 203 |
+
textinfo="label+percent"
|
| 204 |
+
))
|
| 205 |
+
|
| 206 |
+
fig.update_layout(
|
| 207 |
+
title="Sentiment Analysis",
|
| 208 |
+
height=300,
|
| 209 |
+
margin=dict(l=10, r=10, t=40, b=40)
|
| 210 |
+
)
|
| 211 |
+
return fig
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def refresh_analytics():
|
| 215 |
+
"""Refresh all analytics data and charts."""
|
| 216 |
+
data = load_analytics_data()
|
| 217 |
+
|
| 218 |
+
# Update charts
|
| 219 |
+
line_fig = create_line_chart(data["messages_per_hour"])
|
| 220 |
+
bar_fig = create_bar_chart(data["agent_distribution"])
|
| 221 |
+
pie_fig = create_pie_chart(data["sentiment_counts"])
|
| 222 |
+
|
| 223 |
+
# Update dataframe
|
| 224 |
+
df_data = data["top_sessions"] if data["top_sessions"] else [{
|
| 225 |
+
"session": "No data", "activity_count": 0, "last_active": "N/A"
|
| 226 |
+
}]
|
| 227 |
+
df = pd.DataFrame(df_data)
|
| 228 |
+
|
| 229 |
+
return line_fig, bar_fig, pie_fig, df
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def create_analytics_interface():
|
| 233 |
+
"""Create the Gradio analytics dashboard interface."""
|
| 234 |
+
# Load initial data
|
| 235 |
+
initial_data = load_analytics_data()
|
| 236 |
+
|
| 237 |
+
# Create initial charts
|
| 238 |
+
initial_line = create_line_chart(initial_data["messages_per_hour"])
|
| 239 |
+
initial_bar = create_bar_chart(initial_data["agent_distribution"])
|
| 240 |
+
initial_pie = create_pie_chart(initial_data["sentiment_counts"])
|
| 241 |
+
|
| 242 |
+
# Create initial dataframe
|
| 243 |
+
df_data = initial_data["top_sessions"] if initial_data["top_sessions"] else [{
|
| 244 |
+
"session": "No data", "activity_count": 0, "last_active": "N/A"
|
| 245 |
+
}]
|
| 246 |
+
initial_df = pd.DataFrame(df_data)
|
| 247 |
+
|
| 248 |
+
with gr.Blocks(title="Cain Analytics Dashboard", theme=gr.themes.Soft()) as app:
|
| 249 |
+
gr.Markdown("# 📊 Cain Chat Analytics Dashboard")
|
| 250 |
+
|
| 251 |
+
with gr.Row():
|
| 252 |
+
refresh_btn = gr.Button("🔄 Refresh", variant="primary")
|
| 253 |
+
|
| 254 |
+
with gr.Row():
|
| 255 |
+
with gr.Column(scale=1):
|
| 256 |
+
line_plot = gr.Plot(value=initial_line)
|
| 257 |
+
with gr.Column(scale=1):
|
| 258 |
+
bar_plot = gr.Plot(value=initial_bar)
|
| 259 |
+
|
| 260 |
+
with gr.Row():
|
| 261 |
+
with gr.Column(scale=1):
|
| 262 |
+
pie_plot = gr.Plot(value=initial_pie)
|
| 263 |
+
with gr.Column(scale=1):
|
| 264 |
+
sessions_df = gr.Dataframe(
|
| 265 |
+
value=initial_df,
|
| 266 |
+
label="Top 10 Active Sessions",
|
| 267 |
+
headers=["Session", "Activity Count", "Last Active"]
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# Bind refresh button
|
| 271 |
+
refresh_btn.click(
|
| 272 |
+
refresh_analytics,
|
| 273 |
+
outputs=[line_plot, bar_plot, pie_plot, sessions_df]
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
return app
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
if __name__ == "__main__":
|
| 280 |
+
app = create_analytics_interface()
|
| 281 |
+
app.launch(server_name="0.0.0.0", server_port=7861, share=False)
|