| |
| """ |
| Comments view β Live Chat Feed. |
| Imports shared infrastructure from app.py via sys.path manipulation. |
| All session state values are set by app.py before this page runs. |
| """ |
| import streamlit as st |
| import pandas as pd |
| import re |
| import time |
|
|
| import sys |
| import os |
| sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) |
| from shared import ( |
| store_llen, load_stream_data, |
| clean_sentiment, clean_topic, csv_download, |
| TOPIC_LABELS, TOPIC_COLOR, SENT_COLORS, STREAM_NAMES, |
| ) |
|
|
| |
| auto_refresh = st.session_state.get("auto_refresh", True) |
| refresh_rate = st.session_state.get("refresh_rate", 10) |
| msg_limit = st.session_state.get("msg_limit", 50) |
| _primary_key = st.session_state.get("_primary_key", "chat_messages") |
|
|
| |
| all_data = load_stream_data(_primary_key) |
| data = all_data[-msg_limit:] if len(all_data) > msg_limit else all_data |
|
|
| if not all_data: |
| st.markdown( |
| '<div class="empty-state">' |
| '<div class="empty-icon">π</div>' |
| '<div class="empty-title">No messages yet</div>' |
| '<div class="empty-sub">Set a video ID in the sidebar, then click βΆ Start</div>' |
| '</div>', unsafe_allow_html=True |
| ) |
| if auto_refresh: |
| time.sleep(refresh_rate) |
| st.rerun() |
| st.stop() |
|
|
| df = pd.DataFrame(data) |
| df["sentiment"] = df["sentiment"].apply(clean_sentiment) |
| df["topic"] = df["topic"].apply(clean_topic) if "topic" in df.columns else "General" |
|
|
| |
| st.markdown('<div class="sec-hdr"><span class="sec-ttl">Live Chat Feed</span></div>', unsafe_allow_html=True) |
|
|
| |
| if st.session_state.pinned_messages: |
| st.markdown( |
| '<div class="sec-hdr"><span class="sec-ttl">π Pinned Messages</span>' |
| f'<span class="sec-pill">{len(st.session_state.pinned_messages)} pinned</span></div>', |
| unsafe_allow_html=True |
| ) |
| for _pidx, _pmsg in enumerate(st.session_state.pinned_messages): |
| _ps = _pmsg.get("sentiment", "Neutral") |
| _ps_color = SENT_COLORS.get(_ps, "#6b7280") |
| _pt_color = TOPIC_COLOR.get(_pmsg.get("topic", "General"), "#6b7280") |
| _pcol1, _pcol2 = st.columns([10, 1]) |
| with _pcol1: |
| st.markdown( |
| f'<div class="chat-card chat-pinned">' |
| f'<div class="chat-author">π {_pmsg.get("author", "Unknown")}</div>' |
| f'<div class="chat-text">{_pmsg.get("text", "")}</div>' |
| f'<div class="chat-badges">' |
| f'<span class="badge pin-badge">Pinned</span>' |
| f'<span class="badge" style="color:{_ps_color};">{_ps}</span>' |
| f'<span class="badge" style="color:{_pt_color};">{_pmsg.get("topic","General")}</span>' |
| f'<span class="badge">{_pmsg.get("time","")[:19]}</span>' |
| f'</div></div>', |
| unsafe_allow_html=True |
| ) |
| with _pcol2: |
| if st.button("\u2715", key=f"unpin_top_{_pidx}"): |
| st.session_state.pinned_messages.pop(_pidx) |
| st.rerun() |
| st.divider() |
|
|
| |
| _feed_stream_options = {} |
| for _fs in st.session_state.streams: |
| _fkey = _fs.get("redis_key", "") |
| _flen = store_llen(_fkey) |
| if _flen > 0: |
| _fidx = st.session_state.streams.index(_fs) |
| _flabel = f"Stream {STREAM_NAMES[_fidx]} β {_fs.get('video_id', _fkey)[:20]}" |
| _feed_stream_options[_flabel] = _fkey |
|
|
| _cf0, _cf1, _cf2, _cf3, _cf4 = st.columns([1, 1, 1, 1, 2]) |
| with _cf0: |
| if len(_feed_stream_options) > 1: |
| _selected_stream_label = st.selectbox( |
| "Stream", list(_feed_stream_options.keys()), key="feed_stream_select" |
| ) |
| _feed_key = _feed_stream_options[_selected_stream_label] |
| else: |
| _feed_key = _primary_key |
| if _feed_stream_options: |
| st.markdown( |
| f'<div style="font-size:0.75rem;color:var(--text-2);padding-top:28px;">' |
| f'{list(_feed_stream_options.keys())[0]}</div>', |
| unsafe_allow_html=True |
| ) |
|
|
| if _feed_key == _primary_key: |
| _feed_df = df.copy() |
| else: |
| _feed_raw = load_stream_data(_feed_key, limit=msg_limit) |
| _feed_df = pd.DataFrame(_feed_raw) if _feed_raw else pd.DataFrame() |
| if not _feed_df.empty: |
| _feed_df["sentiment"] = _feed_df["sentiment"].apply(clean_sentiment) |
| _feed_df["topic"] = _feed_df["topic"].apply(clean_topic) if "topic" in _feed_df.columns else "General" |
|
|
| with _cf1: |
| _sentiment_filter = st.selectbox("Sentiment", ["All", "Positive", "Neutral", "Negative"]) |
| with _cf2: |
| _topic_filter = st.selectbox("Topic", ["All"] + TOPIC_LABELS) |
| with _cf3: |
| _all_action_types = [ |
| "General Appreciation", "Testimonials", "Faculty Request", "Faculty Feedback", |
| "Content requests", "Content Feedback", "Academic / Lecture / Concept Doubts", |
| "Academic requests", "Study Materials, Deliverables & Learning Resources", |
| "Access & Support", "Batch details / structure / offerings (incl faculty)", |
| "Schedule & logistics (Batch)", "Information- Exam", "Information- Post Exam", |
| "Eligibility & audience fit - Can I take this?", "Suitability & Sufficiency (Is this enough?)", |
| "Guidance- What should I take/do?", "Language Request", "Language medium", |
| "Pricing, discounts, scholarships, offer validity", "Fees + Financial Queries", |
| "Product/feature requests (non-content)", "Offline expansion & event-city requests", |
| "Offers + Events", "General Feedback", "Others", "N/A", |
| ] |
| _action_type_filter = st.selectbox("Action Type", ["All"] + _all_action_types) |
| with _cf4: |
| _search_term = st.text_input("Search messages", placeholder="Filter by keyword...") |
|
|
| |
| |
| |
| _any_filter = ( |
| _sentiment_filter != "All" |
| or _topic_filter != "All" |
| or _action_type_filter != "All" |
| or bool(_search_term) |
| ) |
|
|
| if _any_filter: |
| |
| _full_raw = load_stream_data(_feed_key) |
| if _full_raw: |
| _full_df = pd.DataFrame(_full_raw) |
| _full_df["sentiment"] = _full_df["sentiment"].apply(clean_sentiment) |
| _full_df["topic"] = _full_df["topic"].apply(clean_topic) if "topic" in _full_df.columns else "General" |
| |
| _filtered = _full_df.copy() |
| if _sentiment_filter != "All": |
| _filtered = _filtered[_filtered["sentiment"] == _sentiment_filter] |
| if _topic_filter != "All": |
| _filtered = _filtered[_filtered["topic"] == _topic_filter] |
| if _action_type_filter != "All": |
| if "action_type" in _filtered.columns: |
| _filtered = _filtered[_filtered["action_type"] == _action_type_filter] |
| if _search_term: |
| _filtered = _filtered[_filtered["text"].str.contains(_search_term, case=False, na=False)] |
| |
| if len(_filtered) > msg_limit: |
| _filtered = _filtered.iloc[-msg_limit:] |
| else: |
| _filtered = pd.DataFrame() |
| _total_matching = len(_filtered) |
| _total_scanned = len(_full_raw) if _full_raw else 0 |
| else: |
| |
| _filtered = _feed_df.copy() if not _feed_df.empty else pd.DataFrame() |
| _total_matching = len(_filtered) |
| _total_scanned = len(_feed_df) |
|
|
| _feed_hdr, _feed_dl = st.columns([3, 1]) |
| with _feed_hdr: |
| if _any_filter: |
| st.markdown( |
| f'<div style="font-size:0.78rem;color:var(--text-3);margin-bottom:12px;">' |
| f'Showing {len(_filtered)} matching messages (scanned all {_total_scanned}, capped at {msg_limit})</div>', |
| unsafe_allow_html=True |
| ) |
| else: |
| st.markdown( |
| f'<div style="font-size:0.78rem;color:var(--text-3);margin-bottom:12px;">' |
| f'Showing {len(_filtered)} of {len(_feed_df)} messages</div>', |
| unsafe_allow_html=True |
| ) |
| with _feed_dl: |
| if not _filtered.empty: |
| _export_cols = [c for c in ["author", "text", "sentiment", "confidence", "topic", "time"] if c in _filtered.columns] |
| csv_download(_filtered[_export_cols], "Download Feed CSV", "chat_feed.csv") |
|
|
| _SENT_ICON = {"Positive": "π’", "Negative": "π΄", "Neutral": "π‘"} |
| _pinned_texts = {m.get("text", "") for m in st.session_state.pinned_messages} |
|
|
| for _i, (_, _row) in enumerate(_filtered.iloc[::-1].iterrows()): |
| _s = _row.get("sentiment", "Neutral") |
| _conf_pct = int(_row.get("confidence", 0) * 100) |
| _topic = clean_topic(_row.get("topic", "General")) |
| _t_color = TOPIC_COLOR.get(_topic, "#6b7280") |
| _s_color = SENT_COLORS.get(_s, "#6b7280") |
| _s_icon = _SENT_ICON.get(_s, "βͺ") |
| _conf_color = "#22c55e" if _conf_pct >= 70 else "#eab308" if _conf_pct >= 40 else "#ef4444" |
| _msg_text = _row.get("text", "") |
| _display_text = re.sub(r":[a-zA-Z0-9_\-]+:", "", _msg_text).strip() or _msg_text |
| _is_pinned = _msg_text in _pinned_texts |
| _action_type = _row.get("action_type", "N/A") or "N/A" |
| _card_class = f"chat-card chat-{_s.lower()}" + (" chat-pinned" if _is_pinned else "") |
|
|
| _msg_col, _pin_col = st.columns([11, 1]) |
| with _msg_col: |
| _ab = ( |
| f'<span class="badge" style="color:#a78bfa;border-color:#a78bfa33;">π· {_action_type}</span>' |
| if _action_type not in ("N/A", "", None) else "" |
| ) |
| st.markdown( |
| f'<div class="{_card_class}">' |
| f'<div class="chat-author">{_s_icon} {_row.get("author", "Unknown")}' |
| + (' <span style="font-size:0.7rem;color:#eab308;">π</span>' if _is_pinned else '') + |
| f'</div>' |
| f'<div class="chat-text">{_display_text}</div>' |
| f'<div class="chat-badges">' |
| f'<span class="badge" style="color:{_s_color};border-color:{_s_color}33;">{_s}</span>' |
| f'<span class="badge" style="color:{_conf_color};">Confidence: {_conf_pct}%</span>' |
| f'<span class="badge" style="color:{_t_color};border-color:{_t_color}33;">{_topic}</span>' |
| f'{_ab}' |
| f'</div></div>', |
| unsafe_allow_html=True |
| ) |
| with _pin_col: |
| if _is_pinned: |
| if st.button("π", key=f"unpin_feed_{_i}", help="Unpin this message"): |
| st.session_state.pinned_messages = [ |
| m for m in st.session_state.pinned_messages if m.get("text") != _msg_text |
| ] |
| st.rerun() |
| else: |
| if st.button("π", key=f"pin_{_i}", help="Pin this message"): |
| _msg_dict = _row.to_dict() |
| if _msg_dict not in st.session_state.pinned_messages: |
| st.session_state.pinned_messages.append(_msg_dict) |
| st.rerun() |
|
|
| |
| if auto_refresh: |
| time.sleep(refresh_rate) |
| st.rerun() |
|
|