"""Mustalih Living — Gradio Space app (Gradio 6.13+)""" import os, json, random, tempfile from collections import defaultdict import gradio as gr import networkx as nx from huggingface_hub import hf_hub_download from pyvis.network import Network DATASET_REPO = "FatimahEmadEldin/icaire-ai-glossary-enriched" # ----------------------------------------------------------------------------- # Load data from the paired dataset (or local fallback for dev) # ----------------------------------------------------------------------------- def load_data(): try: path = hf_hub_download( repo_id=DATASET_REPO, filename="glossary_enriched.json", repo_type="dataset", ) except Exception: # Fallback to local file if the dataset repo isn't reachable path = "glossary_enriched.json" with open(path, "r", encoding="utf-8") as f: return json.load(f) DATASET = load_data() TERMS = DATASET["terms"] TERMS_BY_NAME = {t["english_term"]: t for t in TERMS if t.get("english_term")} ALL_TERMS = sorted(TERMS_BY_NAME.keys()) # ----------------------------------------------------------------------------- # Build the knowledge graph # ----------------------------------------------------------------------------- def build_graph(): G = nx.Graph() for t in TERMS: name = t.get("english_term") if not name: continue G.add_node( name, cluster=t.get("primary_cluster", "UNKNOWN"), arabic=t.get("arabic_term", ""), difficulty=t.get("difficulty", "intermediate"), ) for t in TERMS: source = t.get("english_term") graph_raw = t.get("graph_raw", {}) or {} for edge_type in ["prerequisites", "unlocks", "related_concepts", "used_with", "part_of"]: for target in graph_raw.get(edge_type, []): if target in TERMS_BY_NAME and target != source: G.add_edge(source, target, type=edge_type) return G G = build_graph() # Clusters index CLUSTERS = defaultdict(list) for t in TERMS: c = t.get("primary_cluster", "UNKNOWN") if t.get("english_term"): CLUSTERS[c].append(t["english_term"]) CLUSTER_NAMES = sorted(CLUSTERS.keys()) CLUSTER_CHOICES = ["All"] + CLUSTER_NAMES # Story tracks index TRACK_TERMS = defaultdict(list) for t in TERMS: story = t.get("story_assignments_v2", {}) or {} for assignment in story.get("story_assignments", []): track_id = assignment.get("track") if track_id: TRACK_TERMS[track_id].append({ "term": t["english_term"], "arabic": t.get("arabic_term", ""), "position": assignment.get("position_in_track", 5), "role": assignment.get("role", "supporting"), "hook_ar": assignment.get("one_line_hook_ar", ""), "hook_en": assignment.get("one_line_hook_en", ""), "chapter": assignment.get("chapter_hint", ""), }) for track in TRACK_TERMS: TRACK_TERMS[track].sort(key=lambda x: x["position"]) TRACK_DISPLAY = { "DATA_FOUNDATIONS": "Data Foundations / أساسيات البيانات", "HOW_A_MODEL_LEARNS": "How a Model Learns / كيف يتعلم النموذج", "NEURAL_NETWORKS_AND_DEEP": "Neural Networks / الشبكات العصبية", "CLASSICAL_ML_AND_STATS": "Classical ML / تعلم الآلة التقليدي", "APPLIED_AI": "Applied AI / الذكاء التطبيقي", "TRUSTWORTHY_AI": "Trustworthy AI / الذكاء الموثوق", "AI_INFRASTRUCTURE": "AI Infrastructure / البنية التحتية", } TRACK_CHOICES = [k for k in TRACK_DISPLAY.keys() if k in TRACK_TERMS] # ----------------------------------------------------------------------------- # Mermaid rendering — inject the Mermaid JS library # (Gradio 6's Markdown supports ```mermaid``` blocks but requires the JS) # ----------------------------------------------------------------------------- MERMAID_JS = """ """ # ----------------------------------------------------------------------------- # View helpers # ----------------------------------------------------------------------------- def render_term_detail(term_name): if not term_name or term_name not in TERMS_BY_NAME: return "Select a term…", "" t = TERMS_BY_NAME[term_name] feel = t.get("one_sentence_feel", {}) or {} detailed = t.get("detailed_explanation", {}) or {} graph_raw = t.get("graph_raw", {}) or {} md = f"""## {t.get('arabic_term', '')} ### {t.get('english_term', '')} **Cluster:** `{t.get('primary_cluster', '')}` · **Difficulty:** {t.get('difficulty', '')} --- ### Feel / الإحساس > {feel.get('ar', '')} > > *{feel.get('en', '')}* --- ### Definition / التعريف {t.get('arabic_def', '')} {t.get('english_def', '')} --- ### Detailed explanation {detailed.get('ar', '')} {detailed.get('en', '')} --- ### Relationships """ for edge_type in ["prerequisites", "unlocks", "part_of", "alternative_to", "contrasts_with", "related_concepts"]: items = graph_raw.get(edge_type, []) if items: md += f"- **{edge_type.replace('_', ' ').title()}:** " md += ", ".join(f"`{i}`" for i in items) + "\n" if t.get("code_example_python"): md += f"\n---\n\n### Code example\n```python\n{t['code_example_python']}\n```\n" mermaid = t.get("ai_mermaid", "") mermaid_md = f"```mermaid\n{mermaid}\n```" if mermaid and mermaid.strip() else "" return md, mermaid_md def list_cluster_terms(cluster_id): if not cluster_id: return "" terms = CLUSTERS.get(cluster_id, []) if not terms: return "No terms in this cluster." md = f"### {cluster_id} — {len(terms)} terms\n\n" for name in sorted(terms): t = TERMS_BY_NAME.get(name, {}) ar = t.get("arabic_term", "") diff = t.get("difficulty", "") md += f"- **{name}** — {ar} · *{diff}*\n" return md def render_track(track_id): if not track_id: return "" items = TRACK_TERMS.get(track_id, []) title = TRACK_DISPLAY.get(track_id, track_id) md = f"## {title}\n\n{len(items)} terms in this track.\n\n---\n\n" prev_chapter = None for i, item in enumerate(items, 1): chapter = item["chapter"] or "general" if chapter != prev_chapter: md += f"\n### Chapter: {chapter}\n" prev_chapter = chapter md += f"\n**{i}. {item['term']}** — {item['arabic']} · _{item['role']}_\n\n" if item["hook_ar"]: md += f"> {item['hook_ar']}\n\n" if item["hook_en"]: md += f"> *{item['hook_en']}*\n\n" return md def render_graph_html(cluster_filter): net = Network(height="600px", width="100%", bgcolor="#fafaf7", font_color="#1a1a19", notebook=False, cdn_resources="in_line") net.barnes_hut(spring_length=200) if cluster_filter and cluster_filter != "All": node_set = set(CLUSTERS.get(cluster_filter, [])) else: degrees = dict(G.degree()) node_set = set(sorted(degrees, key=lambda n: -degrees[n])[:60]) color_map = { "TRANSFORMER_ARCHITECTURE": "#7F77DD", "NEURAL_NETWORK_BASICS": "#7F77DD", "LAYERS_AND_ACTIVATIONS": "#7F77DD", "OPTIMIZATION_ALGORITHMS": "#BA7517", "LOSS_FUNCTIONS": "#BA7517", "DATA_COLLECTION_AND_LABELING": "#1D9E75", "DATA_QUALITY_AND_CLEANING": "#1D9E75", "AI_ETHICS_PRINCIPLES": "#D85A30", "BIAS_AND_FAIRNESS_TYPES": "#D85A30", "FAIRNESS_METRICS": "#D85A30", "PROMPTING_TECHNIQUES": "#378ADD", "LARGE_LANGUAGE_MODELS": "#378ADD", } for n in node_set: attrs = G.nodes[n] color = color_map.get(attrs.get("cluster"), "#888780") label = f"{attrs.get('arabic', '')}\n{n}" net.add_node(n, label=label, color=color, title=f"{n}\n{attrs.get('cluster', '')}", size=15) for src, tgt, data in G.edges(data=True): if src in node_set and tgt in node_set: net.add_edge(src, tgt, color="#cfccc1", width=0.6) with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as f: net.save_graph(f.name) with open(f.name, "r", encoding="utf-8") as rf: return rf.read() def search_terms(query): if not query or len(query.strip()) < 2: return "Type at least 2 characters." q = query.strip().lower() hits = [] for t in TERMS: if (q in t.get("english_term", "").lower() or q in t.get("arabic_term", "") or q in t.get("arabic_def", "").lower() or q in t.get("english_def", "").lower()): hits.append(t) if len(hits) >= 20: break if not hits: return f"No matches for **{query}**." md = f"### {len(hits)} matches for *{query}*\n\n" for t in hits: md += f"- **{t['english_term']}** — {t.get('arabic_term', '')}\n" return md # ----------------------------------------------------------------------------- # Build the Gradio interface (Gradio 6 idioms) # ----------------------------------------------------------------------------- default_term = "Gradient Descent" if "Gradient Descent" in TERMS_BY_NAME else ALL_TERMS[0] with gr.Blocks( title="Mustalih Living — مُصطلِح الحيّ", theme=gr.themes.Soft(primary_hue="slate"), head=MERMAID_JS, ) as demo: gr.Markdown( f"# Mustalih Living · مُصطلِح الحيّ\n" f"**Arabic-first interactive AI glossary** · {len(TERMS)} terms · " f"7 story tracks · {G.number_of_edges()} typed relationships" ) with gr.Tabs(): with gr.Tab("📖 Term detail"): with gr.Row(): term_picker = gr.Dropdown( choices=ALL_TERMS, value=default_term, label="Pick a term", allow_custom_value=False, ) random_btn = gr.Button("🎲 Random term") term_md = gr.Markdown() term_mermaid = gr.Markdown() term_picker.change( render_term_detail, inputs=term_picker, outputs=[term_md, term_mermaid], ) random_btn.click( lambda: random.choice(ALL_TERMS), outputs=term_picker, ) with gr.Tab("🕸 Knowledge graph"): cluster_picker = gr.Dropdown( choices=CLUSTER_CHOICES, value="All", label="Filter by cluster", ) graph_html = gr.HTML(render_graph_html("All")) cluster_picker.change( render_graph_html, inputs=cluster_picker, outputs=graph_html, ) with gr.Tab("📚 Story tracks"): track_picker = gr.Dropdown( choices=TRACK_CHOICES, value=TRACK_CHOICES[0] if TRACK_CHOICES else None, label="Pick a story track", ) track_md = gr.Markdown( render_track(TRACK_CHOICES[0]) if TRACK_CHOICES else "" ) track_picker.change( render_track, inputs=track_picker, outputs=track_md, ) with gr.Tab("🔖 Clusters"): cluster_browse = gr.Dropdown( choices=CLUSTER_NAMES, value=CLUSTER_NAMES[0] if CLUSTER_NAMES else None, label="Explore a cluster", ) cluster_md = gr.Markdown( list_cluster_terms(CLUSTER_NAMES[0]) if CLUSTER_NAMES else "" ) cluster_browse.change( list_cluster_terms, inputs=cluster_browse, outputs=cluster_md, ) with gr.Tab("🔍 Search"): query = gr.Textbox( label="Search in Arabic or English", placeholder="e.g., Attention, الانتباه, gradient…", ) search_btn = gr.Button("Search") search_md = gr.Markdown() search_btn.click(search_terms, inputs=query, outputs=search_md) query.submit(search_terms, inputs=query, outputs=search_md) with gr.Tab("📊 Stats"): has_feel = sum(1 for t in TERMS if t.get("one_sentence_feel")) has_mermaid = sum(1 for t in TERMS if t.get("ai_mermaid")) has_story = sum(1 for t in TERMS if t.get("story_assignments_v2")) has_graph = sum(1 for t in TERMS if t.get("graph_raw")) gr.Markdown(f""" ### Dataset coverage | Field | Coverage | |---|---| | Total terms | {len(TERMS)} | | With feel metaphor | {has_feel} ({100*has_feel/len(TERMS):.1f}%) | | With Mermaid UML | {has_mermaid} ({100*has_mermaid/len(TERMS):.1f}%) | | With story assignment | {has_story} ({100*has_story/len(TERMS):.1f}%) | | With graph edges | {has_graph} ({100*has_graph/len(TERMS):.1f}%) | ### Clusters Found {len(CLUSTER_NAMES)} clusters across the corpus. """) # Initial render on page load (Gradio 6 idiom: demo.load inside blocks) demo.load( fn=lambda: render_term_detail(default_term), outputs=[term_md, term_mermaid], ) if __name__ == "__main__": demo.launch()