DivYonko commited on
Commit ·
4e7e689
1
Parent(s): 2a149f0
Fix all broken emoji/unicode characters in sidebar and UI
Browse files
app.py
CHANGED
|
@@ -362,7 +362,7 @@ def is_scraper_running(slot_idx: int) -> bool:
|
|
| 362 |
st.set_page_config(
|
| 363 |
page_title="LivePulse",
|
| 364 |
layout="wide",
|
| 365 |
-
page_icon="
|
| 366 |
initial_sidebar_state="expanded"
|
| 367 |
)
|
| 368 |
|
|
@@ -629,7 +629,7 @@ def plotly_layout(height=280):
|
|
| 629 |
|
| 630 |
def csv_download(df_export, label, filename):
|
| 631 |
csv = df_export.to_csv(index=False).encode("utf-8")
|
| 632 |
-
st.download_button(label=f"
|
| 633 |
file_name=filename, mime="text/csv", key=filename)
|
| 634 |
|
| 635 |
|
|
@@ -655,17 +655,17 @@ def compute_velocity(df_all_json: str, window: int = 20) -> dict:
|
|
| 655 |
sentiments = [m.get("sentiment", "Neutral") for m in _json.loads(df_all_json)]
|
| 656 |
n = len(sentiments)
|
| 657 |
if n < window * 2:
|
| 658 |
-
return {"direction": "
|
| 659 |
recent = sentiments[-window:]
|
| 660 |
prev = sentiments[-window*2:-window]
|
| 661 |
r_pos = sum(1 for s in recent if s == "Positive") / window
|
| 662 |
p_pos = sum(1 for s in prev if s == "Positive") / window
|
| 663 |
delta = r_pos - p_pos
|
| 664 |
if delta > 0.08:
|
| 665 |
-
return {"direction": "
|
| 666 |
elif delta < -0.08:
|
| 667 |
-
return {"direction": "
|
| 668 |
-
return {"direction": "
|
| 669 |
|
| 670 |
|
| 671 |
@st.cache_data(ttl=10, show_spinner=False)
|
|
@@ -733,9 +733,9 @@ def compute_engagement(all_data_json: str, window: int = 50) -> dict:
|
|
| 733 |
rate_norm = min(rate / 60, 1.0)
|
| 734 |
score = round((rate_norm * 0.4 + pos_ratio * 0.4 + q_density * 0.2) * 100)
|
| 735 |
|
| 736 |
-
if score >= 70: grade = "
|
| 737 |
-
elif score >= 40: grade = "
|
| 738 |
-
else: grade = "
|
| 739 |
|
| 740 |
return {"score": score, "rate": rate, "pos_ratio": pos_ratio, "q_density": q_density, "grade": grade}
|
| 741 |
|
|
@@ -937,7 +937,7 @@ if "streams" not in st.session_state:
|
|
| 937 |
with st.sidebar:
|
| 938 |
st.markdown(
|
| 939 |
'<div style="padding:12px 0 20px;">'
|
| 940 |
-
'<div style="font-size:1.35rem;font-weight:800;color:var(--text-1);letter-spacing:-0.02em;">
|
| 941 |
'<div style="font-size:0.75rem;color:var(--text-3);margin-top:2px;">YouTube Chat Analytics</div>'
|
| 942 |
'</div>', unsafe_allow_html=True
|
| 943 |
)
|
|
@@ -989,7 +989,7 @@ with st.sidebar:
|
|
| 989 |
|
| 990 |
sc1, sc2 = st.columns(2)
|
| 991 |
with sc1:
|
| 992 |
-
if st.button("
|
| 993 |
vid = extract_video_id(st.session_state[vid_skey])
|
| 994 |
rkey = st.session_state[rkey_skey].strip() or f"chat_messages_{label.lower()}"
|
| 995 |
if vid:
|
|
@@ -1006,11 +1006,11 @@ with st.sidebar:
|
|
| 1006 |
else:
|
| 1007 |
_META.pop("video_title", None)
|
| 1008 |
st.session_state.alert_dismissed = False
|
| 1009 |
-
st.success(f"Stream {label} started
|
| 1010 |
else:
|
| 1011 |
st.error("Invalid video ID or URL")
|
| 1012 |
with sc2:
|
| 1013 |
-
if st.button("
|
| 1014 |
if is_scraper_running(idx):
|
| 1015 |
stop_scraper(idx)
|
| 1016 |
st.session_state.streams[idx]["proc"] = None
|
|
@@ -1021,7 +1021,7 @@ with st.sidebar:
|
|
| 1021 |
running = is_scraper_running(idx)
|
| 1022 |
dot_color = "#22c55e" if running else "#ef4444"
|
| 1023 |
status = "running" if running else "stopped"
|
| 1024 |
-
st.markdown(f'<div style="font-size:0.72rem;color:{dot_color};margin-bottom:4px;">
|
| 1025 |
|
| 1026 |
# Show scraper error if any (only for stream A)
|
| 1027 |
if idx == 0 and _META.get("scraper_error"):
|
|
@@ -1056,7 +1056,7 @@ with st.sidebar:
|
|
| 1056 |
st.markdown('<p style="font-size:0.68rem;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:8px;">Pinned Messages</p>', unsafe_allow_html=True)
|
| 1057 |
pin_count = len(st.session_state.pinned_messages)
|
| 1058 |
st.markdown(f'<div style="font-size:0.78rem;color:var(--text-3);">{pin_count} message{"s" if pin_count != 1 else ""} pinned</div>', unsafe_allow_html=True)
|
| 1059 |
-
if pin_count > 0 and st.button("
|
| 1060 |
st.session_state.pinned_messages = []
|
| 1061 |
st.rerun()
|
| 1062 |
st.divider()
|
|
@@ -1081,7 +1081,7 @@ with st.sidebar:
|
|
| 1081 |
_fname = f"livepulse_{_rkey}_{_ts}.csv"
|
| 1082 |
_csv_bytes = _dl_df.to_csv(index=False).encode("utf-8")
|
| 1083 |
st.download_button(
|
| 1084 |
-
label=f"? {_slabel} ({len(_dl_rows)} msgs)",
|
| 1085 |
data=_csv_bytes,
|
| 1086 |
file_name=_fname,
|
| 1087 |
mime="text/csv",
|
|
@@ -1095,7 +1095,7 @@ with st.sidebar:
|
|
| 1095 |
|
| 1096 |
# -- Danger Zone --
|
| 1097 |
st.markdown('<p style="font-size:0.68rem;font-weight:700;color:#ef4444;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:8px;">Danger Zone</p>', unsafe_allow_html=True)
|
| 1098 |
-
if st.button("
|
| 1099 |
for s in st.session_state.streams:
|
| 1100 |
store_delete(s["redis_key"])
|
| 1101 |
st.session_state.pinned_messages = []
|
|
@@ -1105,7 +1105,7 @@ with st.sidebar:
|
|
| 1105 |
st.markdown(
|
| 1106 |
'<div style="font-size:0.72rem;color:var(--text-3);text-align:center;line-height:1.6;">'
|
| 1107 |
'Theme follows Streamlit settings<br>'
|
| 1108 |
-
'<span style="font-size:0.65rem;">
|
| 1109 |
'</div>', unsafe_allow_html=True
|
| 1110 |
)
|
| 1111 |
|
|
|
|
| 362 |
st.set_page_config(
|
| 363 |
page_title="LivePulse",
|
| 364 |
layout="wide",
|
| 365 |
+
page_icon="\U0001F4E1",
|
| 366 |
initial_sidebar_state="expanded"
|
| 367 |
)
|
| 368 |
|
|
|
|
| 629 |
|
| 630 |
def csv_download(df_export, label, filename):
|
| 631 |
csv = df_export.to_csv(index=False).encode("utf-8")
|
| 632 |
+
st.download_button(label=f"\u2b07 {label}", data=csv,
|
| 633 |
file_name=filename, mime="text/csv", key=filename)
|
| 634 |
|
| 635 |
|
|
|
|
| 655 |
sentiments = [m.get("sentiment", "Neutral") for m in _json.loads(df_all_json)]
|
| 656 |
n = len(sentiments)
|
| 657 |
if n < window * 2:
|
| 658 |
+
return {"direction": "\u2192", "delta": 0.0, "label": "Stable", "color": "#eab308"}
|
| 659 |
recent = sentiments[-window:]
|
| 660 |
prev = sentiments[-window*2:-window]
|
| 661 |
r_pos = sum(1 for s in recent if s == "Positive") / window
|
| 662 |
p_pos = sum(1 for s in prev if s == "Positive") / window
|
| 663 |
delta = r_pos - p_pos
|
| 664 |
if delta > 0.08:
|
| 665 |
+
return {"direction": "\u2191", "delta": delta, "label": "Rising", "color": "#22c55e"}
|
| 666 |
elif delta < -0.08:
|
| 667 |
+
return {"direction": "\u2193", "delta": delta, "label": "Falling", "color": "#ef4444"}
|
| 668 |
+
return {"direction": "\u2192", "delta": delta, "label": "Stable", "color": "#eab308"}
|
| 669 |
|
| 670 |
|
| 671 |
@st.cache_data(ttl=10, show_spinner=False)
|
|
|
|
| 733 |
rate_norm = min(rate / 60, 1.0)
|
| 734 |
score = round((rate_norm * 0.4 + pos_ratio * 0.4 + q_density * 0.2) * 100)
|
| 735 |
|
| 736 |
+
if score >= 70: grade = "\U0001f525 High"
|
| 737 |
+
elif score >= 40: grade = "\u26a1 Medium"
|
| 738 |
+
else: grade = "\U0001f4a4 Low"
|
| 739 |
|
| 740 |
return {"score": score, "rate": rate, "pos_ratio": pos_ratio, "q_density": q_density, "grade": grade}
|
| 741 |
|
|
|
|
| 937 |
with st.sidebar:
|
| 938 |
st.markdown(
|
| 939 |
'<div style="padding:12px 0 20px;">'
|
| 940 |
+
'<div style="font-size:1.35rem;font-weight:800;color:var(--text-1);letter-spacing:-0.02em;">\U0001F4E1 LivePulse</div>'
|
| 941 |
'<div style="font-size:0.75rem;color:var(--text-3);margin-top:2px;">YouTube Chat Analytics</div>'
|
| 942 |
'</div>', unsafe_allow_html=True
|
| 943 |
)
|
|
|
|
| 989 |
|
| 990 |
sc1, sc2 = st.columns(2)
|
| 991 |
with sc1:
|
| 992 |
+
if st.button("\u25b6 Start", key=f"start_{idx}"):
|
| 993 |
vid = extract_video_id(st.session_state[vid_skey])
|
| 994 |
rkey = st.session_state[rkey_skey].strip() or f"chat_messages_{label.lower()}"
|
| 995 |
if vid:
|
|
|
|
| 1006 |
else:
|
| 1007 |
_META.pop("video_title", None)
|
| 1008 |
st.session_state.alert_dismissed = False
|
| 1009 |
+
st.success(f"Stream {label} started -> `{rkey}`")
|
| 1010 |
else:
|
| 1011 |
st.error("Invalid video ID or URL")
|
| 1012 |
with sc2:
|
| 1013 |
+
if st.button("\u23f9 Stop", key=f"stop_{idx}"):
|
| 1014 |
if is_scraper_running(idx):
|
| 1015 |
stop_scraper(idx)
|
| 1016 |
st.session_state.streams[idx]["proc"] = None
|
|
|
|
| 1021 |
running = is_scraper_running(idx)
|
| 1022 |
dot_color = "#22c55e" if running else "#ef4444"
|
| 1023 |
status = "running" if running else "stopped"
|
| 1024 |
+
st.markdown(f'<div style="font-size:0.72rem;color:{dot_color};margin-bottom:4px;">\u25cf {status}</div>', unsafe_allow_html=True)
|
| 1025 |
|
| 1026 |
# Show scraper error if any (only for stream A)
|
| 1027 |
if idx == 0 and _META.get("scraper_error"):
|
|
|
|
| 1056 |
st.markdown('<p style="font-size:0.68rem;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:8px;">Pinned Messages</p>', unsafe_allow_html=True)
|
| 1057 |
pin_count = len(st.session_state.pinned_messages)
|
| 1058 |
st.markdown(f'<div style="font-size:0.78rem;color:var(--text-3);">{pin_count} message{"s" if pin_count != 1 else ""} pinned</div>', unsafe_allow_html=True)
|
| 1059 |
+
if pin_count > 0 and st.button("\U0001f5d1 Clear pins"):
|
| 1060 |
st.session_state.pinned_messages = []
|
| 1061 |
st.rerun()
|
| 1062 |
st.divider()
|
|
|
|
| 1081 |
_fname = f"livepulse_{_rkey}_{_ts}.csv"
|
| 1082 |
_csv_bytes = _dl_df.to_csv(index=False).encode("utf-8")
|
| 1083 |
st.download_button(
|
| 1084 |
+
label=f"?"\u2b07 {_slabel} ({len(_dl_rows)} msgs)",
|
| 1085 |
data=_csv_bytes,
|
| 1086 |
file_name=_fname,
|
| 1087 |
mime="text/csv",
|
|
|
|
| 1095 |
|
| 1096 |
# -- Danger Zone --
|
| 1097 |
st.markdown('<p style="font-size:0.68rem;font-weight:700;color:#ef4444;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:8px;">Danger Zone</p>', unsafe_allow_html=True)
|
| 1098 |
+
if st.button("\U0001f5d1 Clear all data"):
|
| 1099 |
for s in st.session_state.streams:
|
| 1100 |
store_delete(s["redis_key"])
|
| 1101 |
st.session_state.pinned_messages = []
|
|
|
|
| 1105 |
st.markdown(
|
| 1106 |
'<div style="font-size:0.72rem;color:var(--text-3);text-align:center;line-height:1.6;">'
|
| 1107 |
'Theme follows Streamlit settings<br>'
|
| 1108 |
+
'<span style="font-size:0.65rem;">\u2630 \u2192 Settings \u2192 Theme</span>'
|
| 1109 |
'</div>', unsafe_allow_html=True
|
| 1110 |
)
|
| 1111 |
|