| import json | |
| from typing import Any, Dict, List | |
| import gradio as gr | |
| from analysis_core import extract_chats, get_chat_name, analyze_chat | |
| THEME = gr.themes.Soft( | |
| primary_hue="fuchsia", | |
| secondary_hue="pink", | |
| neutral_hue="slate", | |
| ) | |
| CSS = """ | |
| /* full width */ | |
| .gradio-container { | |
| max-width: 100% !important; | |
| padding-left: 24px !important; | |
| padding-right: 24px !important; | |
| } | |
| /* buttons */ | |
| .btn-load button { | |
| background: linear-gradient(90deg, #d946ef, #ec4899) !important; | |
| border-radius: 16px !important; | |
| font-weight: 700 !important; | |
| } | |
| .btn-analyze button { | |
| background: linear-gradient(90deg, #22c55e, #16a34a) !important; | |
| border-radius: 16px !important; | |
| font-weight: 700 !important; | |
| } | |
| /* status */ | |
| .status-box textarea { | |
| border-radius: 14px !important; | |
| font-weight: 500; | |
| } | |
| /* plot */ | |
| .plot-container { min-height: 520px; } | |
| /* -------- Dataframe styling (match magenta theme) -------- */ | |
| .lex-table .wrap { | |
| border-radius: 16px !important; | |
| border: 1px solid rgba(236,72,153,0.28) !important; | |
| overflow: hidden !important; | |
| } | |
| .lex-table table { | |
| border-collapse: separate !important; | |
| border-spacing: 0 !important; | |
| } | |
| /* header */ | |
| .lex-table thead th { | |
| background: linear-gradient(90deg, rgba(217,70,239,0.35), rgba(236,72,153,0.25)) !important; | |
| color: rgba(255,255,255,0.92) !important; | |
| font-weight: 800 !important; | |
| border-bottom: 1px solid rgba(236,72,153,0.28) !important; | |
| } | |
| /* body cells */ | |
| .lex-table tbody td { | |
| background: rgba(255,255,255,0.02) !important; | |
| border-bottom: 1px solid rgba(236,72,153,0.10) !important; | |
| } | |
| /* zebra rows */ | |
| .lex-table tbody tr:nth-child(even) td { | |
| background: rgba(217,70,239,0.06) !important; | |
| } | |
| /* hover */ | |
| .lex-table tbody tr:hover td { | |
| background: rgba(236,72,153,0.14) !important; | |
| } | |
| /* align */ | |
| .lex-table td, .lex-table th { padding: 10px 12px !important; } | |
| /* make numbers a bit clearer */ | |
| .lex-table td:last-child { | |
| font-variant-numeric: tabular-nums; | |
| } | |
| """ | |
| def _path(file_obj) -> str: | |
| if file_obj is None: | |
| raise gr.Error("upload Telegram result.json first.") | |
| if isinstance(file_obj, str): | |
| return file_obj | |
| if isinstance(file_obj, dict) and "path" in file_obj: | |
| return file_obj["path"] | |
| if hasattr(file_obj, "name") and isinstance(file_obj.name, str): | |
| return file_obj.name | |
| raise gr.Error("could not read uploaded file path.") | |
| def lex_list_to_rows(lst: List[Dict[str, Any]]): | |
| if not lst: | |
| return [] | |
| return [[d["word"], round(float(d["score"]), 6)] for d in lst] | |
| def load_chats(file_obj): | |
| p = _path(file_obj) | |
| with open(p, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| chats = extract_chats(data) | |
| labels = [f"{i} | {get_chat_name(c, f'Chat {i}')}" for i, c in enumerate(chats)] | |
| if not labels: | |
| raise gr.Error("no chats found. make sure you uploaded Telegram export result.json") | |
| state = {"data": data} | |
| return gr.update(choices=labels, value=labels[0]), state, f"loaded {len(labels)} chats." | |
| def run_analysis(choice: str, state: Dict[str, Any], max_bert_persian: int): | |
| if not state or "data" not in state: | |
| raise gr.Error("Upload result.json and load chats first.") | |
| if not choice: | |
| raise gr.Error("Choose a chat first.") | |
| chats = extract_chats(state["data"]) | |
| idx = int(choice.split("|", 1)[0].strip()) | |
| if idx < 0 or idx >= len(chats): | |
| raise gr.Error("Invalid chat selection.") | |
| result, fig, pos_top, neg_top = analyze_chat( | |
| chats[idx], | |
| max_bert_persian=int(max_bert_persian), | |
| ) | |
| pos_rows = lex_list_to_rows(pos_top) if pos_top else [] | |
| neg_rows = lex_list_to_rows(neg_top) if neg_top else [] | |
| return result, fig, pos_rows, neg_rows | |
| with gr.Blocks( | |
| title="Telegram Sentiment Analysis", | |
| theme=THEME, | |
| css=CSS, | |
| ) as demo: | |
| gr.Markdown( | |
| "upload Telegram result.json → load chats → choose chat → analyze. " | |
| "weekly plot includes peak/low word annotations. tables show top lex words." | |
| ) | |
| state = gr.State({}) | |
| status = gr.Textbox( | |
| label="Status", | |
| interactive=False, | |
| elem_classes=["status-box"], | |
| ) | |
| file_in = gr.File( | |
| label="Upload Telegram result.json", | |
| file_types=[".json"], | |
| ) | |
| chat_dd = gr.Dropdown( | |
| label="Choose chat", | |
| choices=[], | |
| value=None, | |
| ) | |
| max_bert = gr.Slider( | |
| minimum=0, | |
| maximum=2000, | |
| value=300, | |
| step=50, | |
| label="Max Persian messages to run BERT on (speed control)", | |
| ) | |
| load_btn = gr.Button("Load chats", elem_classes=["btn-load"]) | |
| analyze_btn = gr.Button("Analyze selected chat", elem_classes=["btn-analyze"]) | |
| out_json = gr.JSON(label="Results (JSON)") | |
| out_plot = gr.Plot( | |
| label="Weekly emotion trajectory (with peak/low word annotations)", | |
| elem_classes=["plot-container"], | |
| ) | |
| out_pos = gr.Dataframe( | |
| label="Top 5 positive lex words (word, score)", | |
| headers=["word", "score"], | |
| elem_classes=["lex-table"], | |
| ) | |
| out_neg = gr.Dataframe( | |
| label="Top 5 negative lex words (word, score)", | |
| headers=["word", "score"], | |
| elem_classes=["lex-table"], | |
| ) | |
| load_btn.click(load_chats, inputs=[file_in], outputs=[chat_dd, state, status]) | |
| analyze_btn.click( | |
| run_analysis, | |
| inputs=[chat_dd, state, max_bert], | |
| outputs=[out_json, out_plot, out_pos, out_neg], | |
| ) | |
| demo.launch() |