"""Scripture Knowledge Graph — Gradio app for Hugging Face Spaces. NLP entity extraction, NetworkX graph algorithms, an Ant Colony System swarm, and server-side streaming Claude insights, wired to a D3.js visualization injected via gr.HTML. """ import json import os import random import re from collections import defaultdict import anthropic import gradio as gr import networkx as nx # ═══════════════════════════════════════════════════════════════════════════ # Entity dictionaries # ═══════════════════════════════════════════════════════════════════════════ PEOPLE = { "Jesus", "Christ", "God", "Yahweh", "Jehovah", "Holy Spirit", "Spirit", "Lord", "Father", "Paul", "Peter", "John", "James", "Mary", "Moses", "Abraham", "David", "Solomon", "Isaiah", "Jeremiah", "Ezekiel", "Daniel", "Elijah", "Elisha", "Joshua", "Samuel", "Ruth", "Naomi", "Esther", "Mordecai", "Job", "Adam", "Eve", "Noah", "Jacob", "Israel", "Joseph", "Isaac", "Rebekah", "Rachel", "Leah", "Benjamin", "Judah", "Reuben", "Levi", "Saul", "Jonathan", "Barnabas", "Timothy", "Silas", "Luke", "Mark", "Matthew", "Stephen", "Philip", "Andrew", "Thomas", "Bartholomew", "Thaddaeus", "Simon", "Judas", "Iscariot", "Lazarus", "Martha", "Mary Magdalene", "Herod", "Pilate", "Satan", "Nicodemus", "Zacchaeus", "Cornelius", "Lydia", "Priscilla", "Aquila", "Apollos", "Titus", "Philemon", "Onesimus", "Gideon", "Deborah", "Barak", "Samson", "Delilah", "Boaz", "Hannah", "Eli", "Nathan", "Bathsheba", "Absalom", "Rehoboam", "Jeroboam", "Ahab", "Jezebel", "Elihu", "Amos", "Hosea", "Joel", "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", "Ezra", "Nehemiah", "Melchizedek", "Enoch", "Methuselah", "Lamech", "Cain", "Abel", "Seth", "Terah", "Lot", "Hagar", "Ishmael", "Laban", "Dinah", "Tamar", "Er", "Onan", "Perez", "Pharaoh", "Aaron", "Miriam", "Caleb", "Balaam", "Rahab", "Achan", "Othniel", "Ehud", "Shamgar", "Tola", "Jair", "Jephthah", "Ibzan", "Elon", "Abdon", "Uriah", "Joab", "Amnon", "Adonijah", "Hiram", "Asa", "Jehoshaphat", "Josiah", "Hezekiah", "Manasseh", "Amon", "Jehoiakim", "Zedekiah", "Nebuchadnezzar", "Cyrus", "Darius", "Shadrach", "Meshach", "Abednego", "Belshazzar", "Haman", "Vashti", "Tobiah", "Sanballat", "Simeon", "Anna", "Zechariah the priest", "Elizabeth", "Gabriel", "Michael", "Herod Antipas", "Herodias", "Salome", "Caiaphas", "Annas", "Barabbas", "Simon of Cyrene", "Joseph of Arimathea", "Nathanael", "Matthias", "Ananias", "Sapphira", "Gamaliel", "Dorcas", "Tabitha", "Rhoda", "Felix", "Festus", "Agrippa", "Sosthenes", "Crispus", "Erastus", "Gaius", "Tertius", "Phoebe", "Junia", "Andronicus", "Epaphras", "Epaphroditus", "Demas", "Tychicus", "Trophimus", "Archippus", "Clement", "Diotrephes", "Demetrius", "Cephas", } PLACES = { "Jerusalem", "Bethlehem", "Nazareth", "Galilee", "Judea", "Israel", "Egypt", "Babylon", "Rome", "Corinth", "Ephesus", "Antioch", "Athens", "Philippi", "Thessalonica", "Colossae", "Galatia", "Samaria", "Jordan", "Sinai", "Canaan", "Eden", "Zion", "Capernaum", "Bethsaida", "Jericho", "Hebron", "Bethany", "Gethsemane", "Calvary", "Golgotha", "Mount Sinai", "Mount Zion", "Mount Carmel", "Mount of Olives", "Dead Sea", "Red Sea", "Sea of Galilee", "Mediterranean", "Persia", "Assyria", "Syria", "Macedonia", "Crete", "Cyprus", "Malta", "Arabia", "Moab", "Edom", "Ammon", "Midian", "Philistia", "Gaza", "Ashkelon", "Ashdod", "Gath", "Ekron", "Shiloh", "Gilgal", "Shechem", "Bethel", "Dan", "Beersheba", "Tyre", "Sidon", "Damascus", "Nineveh", "Ur", "Haran", "Padan Aram", "Goshen", "Rameses", "Kadesh Barnea", "Mount Nebo", "Jabbok", "Peniel", "Ai", "Gibeon", "Ramah", "Mizpah", "Gilead", "Bashan", "Perga", "Lystra", "Derbe", "Iconium", "Troas", "Berea", "Cenchreae", "Miletus", "Patmos", "Laodicea", "Smyrna", "Pergamum", "Thyatira", "Sardis", "Philadelphia", "Caesarea", "Joppa", "Emmaus", "Cana", "Tiberias", "Decapolis", "Perea", "Idumea", "Susa", "Ecbatana", "Chaldea", } BOOK_ORDER = [ "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra", "Nehemiah", "Esther", "Job", "Psalms", "Proverbs", "Ecclesiastes", "Song of Songs", "Isaiah", "Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", "Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians", "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter", "1 John", "2 John", "3 John", "Jude", "Revelation", ] BOOK_ABBREVIATIONS = { "gen": "Genesis", "gn": "Genesis", "ex": "Exodus", "exod": "Exodus", "lev": "Leviticus", "lv": "Leviticus", "num": "Numbers", "nm": "Numbers", "deut": "Deuteronomy", "dt": "Deuteronomy", "josh": "Joshua", "judg": "Judges", "jdg": "Judges", "ruth": "Ruth", "1 sam": "1 Samuel", "2 sam": "2 Samuel", "1 kgs": "1 Kings", "2 kgs": "2 Kings", "1 chr": "1 Chronicles", "2 chr": "2 Chronicles", "ezra": "Ezra", "neh": "Nehemiah", "esth": "Esther", "job": "Job", "ps": "Psalms", "psa": "Psalms", "psalm": "Psalms", "prov": "Proverbs", "eccl": "Ecclesiastes", "eccles": "Ecclesiastes", "song": "Song of Songs", "isa": "Isaiah", "jer": "Jeremiah", "lam": "Lamentations", "ezek": "Ezekiel", "eze": "Ezekiel", "dan": "Daniel", "hos": "Hosea", "joel": "Joel", "amos": "Amos", "obad": "Obadiah", "jonah": "Jonah", "mic": "Micah", "nah": "Nahum", "hab": "Habakkuk", "zeph": "Zephaniah", "hag": "Haggai", "zech": "Zechariah", "mal": "Malachi", "matt": "Matthew", "mt": "Matthew", "mk": "Mark", "mrk": "Mark", "lk": "Luke", "luk": "Luke", "jn": "John", "jhn": "John", "acts": "Acts", "rom": "Romans", "1 cor": "1 Corinthians", "2 cor": "2 Corinthians", "gal": "Galatians", "eph": "Ephesians", "phil": "Philippians", "col": "Colossians", "1 thess": "1 Thessalonians", "2 thess": "2 Thessalonians", "1 tim": "1 Timothy", "2 tim": "2 Timothy", "titus": "Titus", "phlm": "Philemon", "heb": "Hebrews", "jas": "James", "1 pet": "1 Peter", "2 pet": "2 Peter", "1 jn": "1 John", "2 jn": "2 John", "3 jn": "3 John", "jude": "Jude", "rev": "Revelation", } BOOKS = set(BOOK_ORDER) | { "Samuel", "Kings", "Chronicles", "Corinthians", "Thessalonians", "Timothy", "Peter", "John", } THEMES = { "faith", "grace", "love", "salvation", "redemption", "covenant", "forgiveness", "mercy", "justice", "righteousness", "holiness", "prayer", "worship", "sin", "repentance", "hope", "peace", "joy", "obedience", "discipleship", "kingdom", "gospel", "resurrection", "eternal life", "sanctification", "justification", "atonement", "baptism", "communion", "prophecy", "wisdom", "truth", "light", "darkness", "healing", "miracles", "creation", "judgment", "reconciliation", "fellowship", "community", "sacrifice", "suffering", "perseverance", "humility", "trust", "guidance", "provision", "protection", "power", "glory", "praise", "servanthood", "stewardship", "generosity", "temptation", "victory", "identity", "adoption", "inheritance", "election", "predestination", "assurance", "confession", "intercession", "fasting", "tithing", "hospitality", "purity", "integrity", "patience", "gentleness", "self-control", "spiritual warfare", "discernment", "revelation", "blessing", "curse", "restoration", "renewal", "transformation", "surrender", "gratitude", "unity", "diversity", "witness", "mission", "evangelism", "compassion", "loyalty", } THEOLOGICAL_CONCEPTS = { "trinity", "incarnation", "eschatology", "hermeneutics", "typology", "soteriology", "ecclesiology", "pneumatology", "christology", "theodicy", "hypostatic union", "imputation", "propitiation", "expiation", "kenosis", "parousia", "millennialism", "dispensationalism", "covenant theology", "replacement theology", "supersessionism", "inerrancy", "sola scriptura", "sola fide", "total depravity", "common grace", } EVENTS = { "crucifixion", "resurrection", "ascension", "pentecost", "creation", "flood", "exodus", "last supper", "baptism of jesus", "transfiguration", "temptation", "birth", "nativity", "second coming", "rapture", "great commission", "sermon on the mount", "feeding of the 5000", "raising of lazarus", "wedding at cana", "tower of babel", "fall of man", "passover", "day of atonement", "tabernacle", "temple dedication", "burning bush", "parting of the red sea", "ten commandments", "golden calf", "fall of jericho", "anointing of david", "fall of jerusalem", "babylonian exile", "return from exile", "rebuilding of the temple", "annunciation", "triumphal entry", "last judgment", "great flood", "covenant with abraham", "binding of isaac", "jacob's ladder", "joseph sold into slavery", "plagues of egypt", "wilderness wandering", "conquest of canaan", "united monarchy", "divided kingdom", "fall of samaria", "council of jerusalem", "conversion of paul", "stoning of stephen", "damascus road", "road to emmaus", "doubting thomas", "great flood narrative", "tower of siloam", "cleansing of the temple", "washing of feet", "gethsemane prayer", "trial of jesus", "empty tomb", "road to calvary", "day of the lord", } TIME_PERIODS = { "old testament", "new testament", "early church", "exile", "wilderness", "intertestamental period", "apostolic age", "patriarchal age", "monarchy period", "judges period", "second temple period", "diaspora", } RELATION_PATTERNS = [ (re.compile(r"\b(\w[\w' ]*?)\s+said to\s+(\w[\w' ]*?)\b", re.I), "said to"), (re.compile(r"\b(\w[\w' ]*?)\s+spoke to\s+(\w[\w' ]*?)\b", re.I), "spoke to"), (re.compile(r"\b(\w[\w' ]*?)\s+wrote to\s+(\w[\w' ]*?)\b", re.I), "wrote to"), (re.compile(r"\b(\w[\w' ]*?)\s+taught\s+(\w[\w' ]*?)\b", re.I), "taught"), (re.compile(r"\b(\w[\w' ]*?)\s+healed\s+(\w[\w' ]*?)\b", re.I), "healed"), (re.compile(r"\b(\w[\w' ]*?)\s+baptized\s+(\w[\w' ]*?)\b", re.I), "baptized"), (re.compile(r"\b(\w[\w' ]*?)\s+killed\s+(\w[\w' ]*?)\b", re.I), "killed"), (re.compile(r"\b(\w[\w' ]*?)\s+anointed\s+(\w[\w' ]*?)\b", re.I), "anointed"), (re.compile(r"\b(\w[\w' ]*?)\s+sent\s+(\w[\w' ]*?)\b", re.I), "sent"), (re.compile(r"\b(\w[\w' ]*?)\s+called\s+(\w[\w' ]*?)\b", re.I), "called"), (re.compile(r"\b(\w[\w' ]*?)\s+appeared to\s+(\w[\w' ]*?)\b", re.I), "appeared to"), (re.compile(r"\b(\w[\w' ]*?)\s+rebuked\s+(\w[\w' ]*?)\b", re.I), "rebuked"), (re.compile(r"\b(\w[\w' ]*?)\s+blessed\s+(\w[\w' ]*?)\b", re.I), "blessed"), (re.compile(r"\b(\w[\w' ]*?)\s+betrayed\s+(\w[\w' ]*?)\b", re.I), "betrayed"), (re.compile(r"\b(\w[\w' ]*?)\s+forgave\s+(\w[\w' ]*?)\b", re.I), "forgave"), ] VERSE_RE = re.compile( r"\b([1-3]?\s?[A-Z][a-z]+\.?)\s+(\d{1,3}):(\d{1,3})(?:[-–](\d{1,3}))?\b" ) NODE_TYPE_ORDER = ["person", "place", "theme", "concept", "book", "verse", "event", "period"] def _canon_book(word_or_phrase): key = word_or_phrase.lower().strip().rstrip(".") if key in BOOK_ABBREVIATIONS: return BOOK_ABBREVIATIONS[key] for b in BOOK_ORDER: if b.lower() == key: return b return None def extract_entities(text): """Pure-Python NLP extraction: entities + co-occurrence/relationship edges.""" nodes = {} edge_weights = defaultdict(int) relation_edges = [] def add_node(key, ntype, label): key = key.lower().strip() if not key: return None if key not in nodes: nodes[key] = {"id": key, "label": label.strip(), "type": ntype, "weight": 1} else: nodes[key]["weight"] += 1 return key # Verse references for m in VERSE_RE.finditer(text): verse_label = m.group(0).strip() add_node(verse_label.lower(), "verse", verse_label) # Directed relationship extraction for pattern, relation in RELATION_PATTERNS: for m in pattern.finditer(text): a_raw, b_raw = m.group(1).strip(), m.group(2).strip() a_cap = a_raw.split()[-1].capitalize() if a_raw else "" b_cap = b_raw.split()[0].capitalize() if b_raw else "" if a_cap in PEOPLE and b_cap in PEOPLE: ka = add_node(a_cap.lower(), "person", a_cap) kb = add_node(b_cap.lower(), "person", b_cap) if ka and kb and ka != kb: relation_edges.append({"source": ka, "target": kb, "label": relation}) sentences = re.split(r"[.!?;\n]+", text) for sentence in sentences: if len(sentence.strip()) < 3: continue words = sentence.split() sentence_keys = [] for i, raw in enumerate(words): w = re.sub(r"[^a-zA-Z']", "", raw) if not w: continue w_cap = w[0].upper() + w[1:] if w else w w_low = w.lower() matched = False if w_cap in PEOPLE or w in PEOPLE: sentence_keys.append(add_node(w_low, "person", w_cap)) matched = True elif w_cap in PLACES: sentence_keys.append(add_node(w_low, "place", w_cap)) matched = True elif w_cap in BOOKS or w_low in BOOK_ABBREVIATIONS: canon = _canon_book(w_cap) or w_cap sentence_keys.append(add_node(canon.lower(), "book", canon)) matched = True elif w_low in THEMES: sentence_keys.append(add_node(w_low, "theme", w_low)) matched = True elif w_low in THEOLOGICAL_CONCEPTS: sentence_keys.append(add_node(w_low, "concept", w_low)) matched = True elif w_low in EVENTS: sentence_keys.append(add_node(w_low, "event", w_low)) matched = True if not matched and i + 1 < len(words): nxt = re.sub(r"[^a-zA-Z']", "", words[i + 1]).lower() bigram = f"{w_low} {nxt}" if bigram in EVENTS: sentence_keys.append(add_node(bigram, "event", bigram)) elif bigram in THEMES: sentence_keys.append(add_node(bigram, "theme", bigram)) elif bigram in THEOLOGICAL_CONCEPTS: sentence_keys.append(add_node(bigram, "concept", bigram)) elif bigram in TIME_PERIODS: sentence_keys.append(add_node(bigram, "period", bigram)) if not matched and i + 2 < len(words): w2 = re.sub(r"[^a-zA-Z']", "", words[i + 1]).lower() w3 = re.sub(r"[^a-zA-Z']", "", words[i + 2]).lower() trigram = f"{w_low} {w2} {w3}" if trigram in TIME_PERIODS: sentence_keys.append(add_node(trigram, "period", trigram)) elif trigram in EVENTS: sentence_keys.append(add_node(trigram, "event", trigram)) sentence_keys = [k for k in sentence_keys if k] for a in range(len(sentence_keys)): for b in range(a + 1, len(sentence_keys)): if sentence_keys[a] != sentence_keys[b]: k = tuple(sorted((sentence_keys[a], sentence_keys[b]))) edge_weights[k] += 1 edges = [{"source": s, "target": t, "weight": w} for (s, t), w in edge_weights.items()] node_list = [n for n in nodes.values() if len(n["label"]) > 1] node_ids = {n["id"] for n in node_list} edges = [e for e in edges if e["source"] in node_ids and e["target"] in node_ids] return {"nodes": node_list, "edges": edges, "relations": relation_edges} # ═══════════════════════════════════════════════════════════════════════════ # Graph algorithms (NetworkX) # ═══════════════════════════════════════════════════════════════════════════ def build_graph(data): G = nx.Graph() for n in data["nodes"]: G.add_node(n["id"], **n) for e in data["edges"]: G.add_edge(e["source"], e["target"], weight=e["weight"]) return G def run_algorithms(G): if G.number_of_nodes() == 0: return None pagerank = nx.pagerank(G, weight="weight") if G.number_of_edges() else {n: 1 / max(G.number_of_nodes(), 1) for n in G.nodes()} communities_raw = list(nx.community.greedy_modularity_communities(G, weight="weight")) if G.number_of_edges() else [{n} for n in G.nodes()] community_map = {} for idx, comm in enumerate(communities_raw): for node in comm: community_map[node] = idx betweenness = nx.betweenness_centrality(G, weight="weight") if G.number_of_nodes() > 2 else {n: 0 for n in G.nodes()} degree_centrality = nx.degree_centrality(G) try: eigenvector = nx.eigenvector_centrality(G, weight="weight", max_iter=500) except (nx.PowerIterationFailedConvergence, nx.AmbiguousSolution, ZeroDivisionError): eigenvector = {n: 0 for n in G.nodes()} clustering = nx.average_clustering(G, weight="weight") if G.number_of_edges() else 0.0 density = nx.density(G) if G.number_of_nodes() > 1 and nx.is_connected(G): diameter = nx.diameter(G) else: comps = list(nx.connected_components(G)) largest = max(comps, key=len) if comps else set() sub = G.subgraph(largest) diameter = nx.diameter(sub) if len(largest) > 1 else 0 bridges = list(nx.bridges(G)) if G.number_of_nodes() > 1 else [] articulation_pts = list(nx.articulation_points(G)) if G.number_of_nodes() > 2 else [] return { "pagerank": pagerank, "communities": [list(c) for c in communities_raw], "community_map": community_map, "betweenness": betweenness, "degree_centrality": degree_centrality, "eigenvector": eigenvector, "clustering": clustering, "density": density, "diameter": diameter, "bridges": bridges, "articulation_points": articulation_pts, } # ═══════════════════════════════════════════════════════════════════════════ # ACO Swarm — Ant Colony System (Dorigo & Gambardella, 1997) # ═══════════════════════════════════════════════════════════════════════════ class ACS: """Ant Colony System on the conceptual graph. Traces the strongest conceptual pathway between the two highest-PageRank nodes.""" def __init__(self, G, pagerank, alpha=1.0, beta=2.0, rho=0.1, q0=0.9, Q=1.0): self.G = G self.pr = pagerank self.alpha = alpha self.beta = beta self.rho = rho self.q0 = q0 self.Q = Q n_edges = max(G.number_of_edges(), 1) self.tau0 = 1.0 / n_edges self.tau = {frozenset(e): self.tau0 for e in G.edges()} def _key(self, a, b): return frozenset((a, b)) def _weight(self, a, b): return self.G[a][b].get("weight", 1) def _choose_next(self, current, visited): neighbors = [n for n in self.G.neighbors(current) if n not in visited] if not neighbors: return None scores = [] for n in neighbors: tau = self.tau.get(self._key(current, n), self.tau0) eta = self._weight(current, n) scores.append((tau ** self.alpha) * (eta ** self.beta)) total = sum(scores) if total <= 0: return random.choice(neighbors) if random.random() < self.q0: return neighbors[scores.index(max(scores))] r = random.random() * total acc = 0.0 for n, s in zip(neighbors, scores): acc += s if r <= acc: return n return neighbors[-1] def _local_update(self, a, b): k = self._key(a, b) self.tau[k] = (1 - self.rho) * self.tau.get(k, self.tau0) + self.rho * self.tau0 def _global_update(self, path, strength): deposit = self.Q * strength for i in range(len(path) - 1): k = self._key(path[i], path[i + 1]) self.tau[k] = (1 - self.rho) * self.tau.get(k, self.tau0) + self.rho * deposit def _path_strength(self, path, reached_target): if len(path) < 2: return 0.0 w = sum(self._weight(path[i], path[i + 1]) for i in range(len(path) - 1)) pr = sum(self.pr.get(n, 0) for n in path) score = (w / len(path)) * (1 + pr) return score if reached_target else score * 0.4 def run(self, n_ants=40, n_iter=30, source=None, target=None, max_len=10): nodes = list(self.G.nodes()) if not nodes: yield {"iteration": 0, "n_iter": n_iter, "best_path": [], "best_score": 0, "convergence": [], "tau": {}} return if source is None: source = max(nodes, key=lambda n: self.pr.get(n, 0)) if target is None: ranked = sorted(nodes, key=lambda n: -self.pr.get(n, 0)) target = next((n for n in ranked if n != source), source) best_path, best_score = [source], 0.0 convergence = [] for iteration in range(n_iter): iter_best_path, iter_best_score, iter_reached = None, -1.0, False for _ in range(n_ants): path = [source] visited = {source} current = source reached = False for _step in range(max_len): if current == target: reached = True break nxt = self._choose_next(current, visited) if nxt is None: break self._local_update(current, nxt) path.append(nxt) visited.add(nxt) current = nxt if current == target: reached = True score = self._path_strength(path, reached) if score > iter_best_score: iter_best_score, iter_best_path, iter_reached = score, path, reached for k in list(self.tau.keys()): self.tau[k] = max((1 - self.rho) * self.tau[k], 0.001) if iter_best_path and iter_best_score > best_score: best_score, best_path = iter_best_score, iter_best_path if best_path and len(best_path) > 1: self._global_update(best_path, best_score) convergence.append(round(best_score, 4)) yield { "iteration": iteration + 1, "n_iter": n_iter, "best_path": best_path, "best_score": round(best_score, 4), "convergence": convergence, "tau": {"|||".join(sorted(k)): round(v, 4) for k, v in self.tau.items()}, } def compute_swarm_insights(G, algo, swarm_result): degrees = dict(G.degree()) hub_concepts = sorted(degrees.items(), key=lambda x: -x[1])[:5] isolated = [n for n, d in degrees.items() if d <= 1] community_map = algo["community_map"] bridges_by_node = defaultdict(set) for u, v in G.edges(): cu, cv = community_map.get(u), community_map.get(v) if cu != cv: bridges_by_node[u].add(cv) bridges_by_node[v].add(cu) community_bridges = sorted( ((n, len(c)) for n, c in bridges_by_node.items() if len(c) >= 1), key=lambda x: -x[1], )[:5] def label(nid): return G.nodes[nid].get("label", nid) if nid in G.nodes else nid return { "hub_concepts": [f"{label(n)} (degree {d})" for n, d in hub_concepts], "isolated_concepts": [label(n) for n in isolated[:6]], "community_bridges": [f"{label(n)} bridges {c} communities" for n, c in community_bridges], "dominant_pathway": " → ".join(label(n) for n in swarm_result["best_path"]), } # ═══════════════════════════════════════════════════════════════════════════ # Anthropic streaming insights # ═══════════════════════════════════════════════════════════════════════════ _client = None def get_client(): global _client if _client is None: api_key = os.environ.get("ANTHROPIC_API_KEY") if not api_key: return None _client = anthropic.Anthropic(api_key=api_key) return _client def build_insights_prompt(notes, G, algo, swarm_state): top_nodes = sorted(algo["pagerank"].items(), key=lambda x: -x[1])[:10] num_communities = len(algo["communities"]) top_pr_id = top_nodes[0][0] if top_nodes else None top_pr_label = G.nodes[top_pr_id].get("label", top_pr_id) if top_pr_id else "unknown" bridge_id = max(algo["betweenness"].items(), key=lambda x: x[1])[0] if algo["betweenness"] else None bridge_label = G.nodes[bridge_id].get("label", bridge_id) if bridge_id else "unknown" top_lines = "\n".join( f" {i+1}. {G.nodes[n].get('label', n)} (PR={v:.4f})" for i, (n, v) in enumerate(top_nodes[:8]) ) swarm_state = swarm_state or {} insights = swarm_state.get("insights", {}) pathway = insights.get("dominant_pathway", "not yet computed") strength = swarm_state.get("best_score", 0) hub_concepts = ", ".join(insights.get("hub_concepts", [])[:5]) isolated = ", ".join(insights.get("isolated_concepts", [])[:3]) diameter = algo["diameter"] prompt = f"""You are a biblical scholar, theologian, and data scientist analyzing a scripture knowledge graph built from a student's Bible study notes using graph theory and swarm intelligence algorithms. STUDY NOTES (excerpt): {notes[:2500]} GRAPH TOPOLOGY: - Nodes: {G.number_of_nodes()} biblical entities - Edges: {G.number_of_edges()} conceptual relationships - Communities detected (greedy modularity): {num_communities} - Graph density: {algo['density']:.4f} - Average clustering coefficient: {algo['clustering']:.4f} - Graph diameter: {diameter} TOP ENTITIES BY PAGERANK (most theologically central): {top_lines} BETWEENNESS CENTRALITY (conceptual bridges): Bridge node: {bridge_label} ACO SWARM RESULTS: Dominant conceptual pathway: {pathway} Path strength: {strength} Hub concepts identified by swarm: {hub_concepts} Isolated concepts (weak connections): {isolated} Provide deep theological and structural analysis in exactly these sections: ## CENTRAL THEME What is the dominant theological theme? Why does PageRank converge on {top_pr_label}? (3-4 sentences, theologically substantive) ## GRAPH STRUCTURE REVELATION What does the clustering into {num_communities} communities reveal about how these biblical concepts relate? What surprised you about the graph topology? (3-4 sentences) ## SWARM INTELLIGENCE FINDING The ACO ants, operating with no global knowledge, organically traced: {pathway}. Why is this path theologically significant? What does it reveal that a linear reading of the notes might miss? (3-4 sentences) ## UNEXPECTED CONNECTION Identify one non-obvious connection between entities in the graph — ideally involving the bridge node ({bridge_label}) — that reveals something theologically significant. (2-3 sentences) ## CROSS-REFERENCES Suggest 3 specific passages that deepen this study, explaining the graph-theoretic reason each is relevant (e.g., "Hebrews 11 would add 15+ faith connections, increasing graph density in the salvation community"): 1. [Book Chapter:Verse] — reason 2. [Book Chapter:Verse] — reason 3. [Book Chapter:Verse] — reason ## STUDY PROMPT One penetrating question for deeper reflection, grounded in what the swarm and graph revealed.""" return prompt def generate_insights(notes, graph_state, algo_state, swarm_state): if not graph_state or not algo_state: yield "Build a knowledge graph first." return client = get_client() if client is None: yield ("**No Anthropic API key found.**\n\nSet the `ANTHROPIC_API_KEY` secret in this " "Space's settings (Settings → Variables and secrets) to enable AI insights.") return G = build_graph(graph_state) prompt = build_insights_prompt(notes or "", G, algo_state, swarm_state) accumulated = "" try: with client.messages.stream( model="claude-opus-4-8", max_tokens=2400, messages=[{"role": "user", "content": prompt}], ) as stream: for text in stream.text_stream: accumulated += text yield accumulated except Exception as e: # noqa: BLE001 — surface any API error to the UI accumulated += f"\n\n---\n**Error generating insights:** {e}" yield accumulated # ═══════════════════════════════════════════════════════════════════════════ # HTML fragments rendered server-side # ═══════════════════════════════════════════════════════════════════════════ EMPTY_STATS_HTML = '
Build a graph to see statistics.
' EMPTY_SWARM_HTML = '
Build a graph, then run swarm analysis.
' HEADER_HTML = """

✦ Scripture Knowledge Graph

Paste your notes · Extract entities · Run graph algorithms · Swarm intelligence · AI insights

NLP Graph Theory ACS Swarm Claude
""" GRAPH_CONTAINER_HTML = """
Person
Place
Theme
Book
Verse
Event
Your knowledge graph will materialize here
Paste notes → Build graph → Explore
""" NODE_DETAIL_HTML = """
Selected Node
Click a node to inspect
Entity Preview
Paste notes to see a live entity estimate.
""" def render_stats_html(G, algo): n_nodes, n_edges = G.number_of_nodes(), G.number_of_edges() top_pr = sorted(algo["pagerank"].items(), key=lambda x: -x[1])[:6] rank_items = "".join( f"
  • {i + 1}. {G.nodes[n].get('label', n)} {v:.4f}
  • " for i, (n, v) in enumerate(top_pr) ) bridge_id = max(algo["betweenness"].items(), key=lambda x: x[1])[0] if algo["betweenness"] else None bridge_label = G.nodes[bridge_id].get("label", bridge_id) if bridge_id else "—" articulation = ", ".join(G.nodes[n].get("label", n) for n in algo["articulation_points"][:8]) or "None" bridges_desc = ", ".join( f"{G.nodes[a].get('label', a)}–{G.nodes[b].get('label', b)}" for a, b in algo["bridges"][:6] ) or "None" diameter = algo["diameter"] if algo["diameter"] else "∞" return f"""
    {n_nodes}Nodes
    {n_edges}Edges
    {algo['density']:.3f}Density
    {len(algo['communities'])}Communities
    {algo['clustering']:.3f}Avg CC
    {diameter}Diameter
    Central bridge node{bridge_label}
    Centrality Rankings (PageRank)
    Articulation Points
    {articulation}
    Bridges
    {bridges_desc}
    """ def render_swarm_progress_html(G, progress, running, insights=None): n_iter = progress.get("n_iter", 30) or 30 pct = int(100 * progress.get("iteration", 0) / n_iter) status = 'RUNNING' if running else 'CONVERGED' path_labels = [G.nodes[n].get("label", n) for n in progress.get("best_path", []) if n in G.nodes] pathway = " → ".join(path_labels) if path_labels else "—" insights_html = "" if insights: hubs = "".join(f"
  • {h}
  • " for h in insights["hub_concepts"]) or "
  • " isolated = "".join(f"
  • {i}
  • " for i in insights["isolated_concepts"]) or "
  • None — fully connected
  • " bridges = "".join(f"
  • {b}
  • " for b in insights["community_bridges"]) or "
  • " insights_html = f"""
    Hub Concepts
    Isolated Concepts
    Community Bridges
    """ return f"""
    ACS Status{status}
    Ants active40
    Iteration{progress.get('iteration', 0)} / {n_iter}
    Dominant Pathway
    {pathway}
    Path strength{progress.get('best_score', 0)}
    {insights_html}
    """ def build_graph_payload(data, algo): deg = defaultdict(int) for e in data["edges"]: deg[e["source"]] += 1 deg[e["target"]] += 1 nodes = [] for n in data["nodes"]: nid = n["id"] nodes.append({ **n, "pagerank": algo["pagerank"].get(nid, 0), "betweenness": algo["betweenness"].get(nid, 0), "community": algo["community_map"].get(nid, 0), "degree": deg.get(nid, 0), }) max_pr = max(algo["pagerank"].values()) if algo["pagerank"] else 1 return { "nodes": nodes, "edges": data["edges"], "relations": data.get("relations", []), "maxPagerank": max_pr, "numCommunities": len(algo["communities"]), } # ═══════════════════════════════════════════════════════════════════════════ # Gradio event handlers # ═══════════════════════════════════════════════════════════════════════════ SAMPLE_NOTES = """John 3:16 is central to understanding God's grace and the nature of salvation. Jesus said to Nicodemus that faith in the Son of God leads to eternal life. This connects to Romans 8:28, where Paul reminds us that God works all things for good for those who love him. The theme of redemption runs through both the Old Testament and New Testament. Moses led the exodus from Egypt, foreshadowing the salvation Jesus brings. Abraham's covenant with God in Genesis established the foundation of faith that Paul later expounds in Galatians and Romans. In Ephesians, Paul wrote to the church about grace through faith, not works. This echoes Isaiah's prophecy about the suffering servant and connects to the crucifixion and resurrection of Jesus in Jerusalem. Peter also addresses this in his letters, emphasizing holiness and perseverance. Paul wrote to Timothy about guarding sound doctrine, and Paul wrote to Titus about godly living. The Psalms of David reflect themes of worship, trust, and God's provision and protection. These themes appear again in Matthew's Sermon on the Mount, where Jesus taught the crowds about prayer, forgiveness, and the Kingdom of God. Jesus healed the blind man and the lame man, revealing his compassion. The covenant between God and Israel runs from Genesis through Revelation. The Holy Spirit, promised by Jesus before his ascension, arrived at Pentecost in Jerusalem, empowering the early church in Acts. Paul's missionary journeys through Corinth, Ephesus, and Rome spread the gospel of grace. Daniel's prophecies in Babylon connect to Revelation's vision of John. Both speak of judgment, hope, and the glory of God. The resurrection is the cornerstone — without it, Paul says in 1 Corinthians, faith is empty. Jesus appeared to Thomas after the resurrection, and Thomas believed. Christ is risen, making reconciliation and eternal life possible for all who believe.""" def analyze_notes(notes): notes = (notes or "").strip() if not notes: raise gr.Error("Paste or upload some study notes first.") data = extract_entities(notes) if len(data["nodes"]) < 2: raise gr.Error("Not enough biblical entities found. Try adding more names, places, themes, or verse references.") G = build_graph(data) algo = run_algorithms(G) payload = build_graph_payload(data, algo) stats = render_stats_html(G, algo) return payload, stats, algo, data def load_sample(): return SAMPLE_NOTES def handle_upload(file): if file is None: raise gr.Error("No file selected.") with open(file.name, "r", encoding="utf-8", errors="ignore") as f: return f.read() def clear_all(): return ( "", None, EMPTY_STATS_HTML, EMPTY_SWARM_HTML, None, None, None, None, "Build a graph first, then click **Generate Insights**.", ) def run_swarm_gen(graph_state, algo_state): if not graph_state or not algo_state: raise gr.Error("Build a knowledge graph first.") G = build_graph(graph_state) if G.number_of_edges() == 0: raise gr.Error("This graph has no connections for the swarm to explore.") acs = ACS(G, algo_state["pagerank"]) last = None for progress in acs.run(n_ants=40, n_iter=30): last = progress yield render_swarm_progress_html(G, progress, running=True), progress, progress insights = compute_swarm_insights(G, algo_state, last) last = {**last, "insights": insights} yield render_swarm_progress_html(G, last, running=False, insights=insights), last, last # ═══════════════════════════════════════════════════════════════════════════ # Futuristic neon CSS # ═══════════════════════════════════════════════════════════════════════════ CUSTOM_CSS = """ :root { --void: #020408; --deep: #060d18; --panel: #0a1220; --border: rgba(0,200,255,0.14); --cyan: #00e5ff; --cyan-dim: #00a8cc; --plasma: #b44fff; --plasma-dim: #7a2eb8; --ember: #ff6b35; --lime: #39ff14; --rose: #ff2d6b; --ice: #a8f0ff; --fog: #9db9cc; --font-head: 'Orbitron', 'Share Tech Mono', monospace; --font-mono: 'JetBrains Mono', 'Space Mono', monospace; --font-body: 'Inter', sans-serif; } gradio-app, .gradio-container, body { background: var(--void) !important; color: var(--ice) !important; } .gradio-container, .gradio-container * { font-family: var(--font-body); } .gradio-container { max-width: 100% !important; } .gradio-container .block, .gradio-container .form, .gradio-container .panel, .gradio-container .tabitem, .gradio-container .tabs { background: transparent !important; border-color: var(--border) !important; box-shadow: none !important; } .gradio-container label span, .gradio-container .label-wrap span { color: var(--fog) !important; } .gradio-container [role="tab"] { color: var(--fog) !important; font-family: var(--font-mono) !important; text-transform: uppercase; letter-spacing: 0.05em; font-size: 12px !important; } .gradio-container [role="tab"][aria-selected="true"] { color: var(--cyan) !important; } #skg-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 22px; border-bottom: 1px solid var(--border); background: linear-gradient(180deg, rgba(10,18,32,0.9), rgba(2,4,8,0.3)); backdrop-filter: blur(10px); margin-bottom: 8px; border-radius: 6px; } #skg-header h1 { font-family: var(--font-head); font-size: 18px; letter-spacing: 0.16em; color: var(--cyan); text-shadow: 0 0 16px rgba(0,229,255,0.55); margin: 0; } #skg-header p { font-family: var(--font-mono); font-size: 10px; color: var(--fog) !important; letter-spacing: 0.05em; margin-top: 5px; } .skg-pills { display: flex; gap: 8px; } .skg-pill { font-family: var(--font-mono); font-size: 9px; padding: 4px 11px; border: 1px solid var(--border); border-radius: 20px; color: var(--plasma) !important; letter-spacing: 0.1em; text-transform: uppercase; } .skg-panel { background: rgba(10,18,32,0.55); border: 1px solid var(--border); border-radius: 6px; backdrop-filter: blur(14px); padding: 13px 15px; margin-bottom: 10px; } .nd-name { font-family: var(--font-head); font-size: 14px; margin-bottom: 4px; color: var(--cyan) !important; } .nd-type { font-family: var(--font-mono); font-size: 9px; color: var(--fog) !important; letter-spacing: 0.08em; margin-bottom: 6px; } .nd-info { font-family: var(--font-body); font-size: 11.5px; color: var(--ice) !important; opacity: 0.95; line-height: 1.55; } .nd-info b { color: var(--cyan) !important; font-weight: 700; } .nd-placeholder { color: var(--fog) !important; font-style: italic; font-size: 11px; } #entity-preview { font-family: var(--font-mono); font-size: 10.5px; color: var(--lime) !important; } .section-label { font-family: var(--font-mono); font-size: 9px; color: var(--plasma) !important; text-transform: uppercase; letter-spacing: 0.1em; margin: 10px 0 4px; } .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; margin-bottom: 8px; } .stat-cell { background: rgba(10,18,32,0.75); padding: 10px; text-align: center; } .stat-val { display: block; font-family: var(--font-head); font-size: 19px; color: var(--cyan) !important; text-shadow: 0 0 6px rgba(0,229,255,0.35); } .stat-lbl { font-family: var(--font-mono); font-size: 8px; color: var(--fog) !important; text-transform: uppercase; letter-spacing: 0.08em; } .algo-row { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid rgba(0,229,255,0.06); font-size: 11px; } .algo-name { font-family: var(--font-mono); font-size: 9.5px; color: var(--fog) !important; } .algo-val { font-family: var(--font-mono); font-size: 10px; color: var(--cyan) !important; } .rank-list, .flag-list { list-style: none; margin: 4px 0 0; padding: 0; font-family: var(--font-mono); font-size: 10px; } .rank-list li, .flag-list li { padding: 3px 0; color: var(--ice) !important; opacity: 1; } .pathway-text { font-family: var(--font-mono); font-size: 11px; color: var(--lime) !important; line-height: 1.6; word-break: break-word; } .swarm-status { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 10px; margin-bottom: 6px; } .swarm-status span { color: var(--ice) !important; } .swarm-status span.val-running { color: var(--lime) !important; text-shadow: 0 0 8px rgba(57,255,20,0.5); } .swarm-status span.val-idle { color: var(--fog) !important; } .conv-bar-outer { height: 8px; background: rgba(255,255,255,0.06); border-radius: 4px; overflow: hidden; border: 1px solid var(--border); margin: 6px 0 10px; } .conv-bar-inner { height: 100%; background: linear-gradient(90deg, var(--cyan), var(--lime)); box-shadow: 0 0 10px rgba(57,255,20,0.5); transition: width 0.3s ease; } #notes-input textarea { background: rgba(10,18,32,0.6) !important; border: 1px solid var(--border) !important; color: var(--ice) !important; font-family: var(--font-mono) !important; font-size: 12.5px !important; backdrop-filter: blur(12px); border-radius: 6px !important; } #notes-input textarea:focus { border-color: var(--cyan) !important; box-shadow: 0 0 0 1px var(--cyan), 0 0 18px rgba(0,229,255,0.25) !important; } #notes-input label span { font-family: var(--font-mono) !important; font-size: 9px !important; color: var(--fog) !important; text-transform: uppercase; letter-spacing: 0.08em; } .skg-btn, button.skg-btn { font-family: var(--font-mono) !important; letter-spacing: 0.07em; text-transform: uppercase; font-size: 10.5px !important; background: rgba(0,229,255,0.05) !important; border: 1px solid var(--border) !important; color: var(--ice) !important; border-radius: 4px !important; } .skg-btn:hover { border-color: var(--cyan) !important; background: rgba(0,229,255,0.15) !important; color: var(--cyan) !important; box-shadow: 0 0 14px rgba(0,229,255,0.3); } .skg-btn-primary, button.skg-btn-primary { font-family: var(--font-mono) !important; letter-spacing: 0.08em; text-transform: uppercase; font-size: 11px !important; background: linear-gradient(90deg, rgba(0,229,255,0.18), rgba(180,79,255,0.18)) !important; border: 1px solid var(--cyan) !important; color: var(--cyan) !important; border-radius: 4px !important; box-shadow: 0 0 16px rgba(0,229,255,0.2); } .skg-btn-primary:hover { box-shadow: 0 0 26px rgba(0,229,255,0.45); } .skg-btn-swarm, button.skg-btn-swarm { border-color: var(--lime) !important; color: var(--lime) !important; } .skg-btn-swarm:hover { box-shadow: 0 0 20px rgba(57,255,20,0.35); } #graph-outer { padding: 0 !important; border: none !important; background: none !important; } #graph-canvas-wrap { position: relative; height: 74vh; min-height: 480px; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: radial-gradient(ellipse at 50% 28%, rgba(0,229,255,0.07) 0%, transparent 55%), var(--void); } .grid-bg { position: absolute; inset: 0; pointer-events: none; background-image: linear-gradient(rgba(0,229,255,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0,229,255,0.05) 1px, transparent 1px); background-size: 34px 34px; mask-image: radial-gradient(ellipse at 50% 40%, black 5%, transparent 78%); -webkit-mask-image: radial-gradient(ellipse at 50% 40%, black 5%, transparent 78%); } .scanline { position: absolute; left: 0; right: 0; height: 2px; top: -2%; background: linear-gradient(90deg, transparent, rgba(0,229,255,0.6), transparent); animation: scan 5.5s linear infinite; pointer-events: none; } @keyframes scan { 0% { top: -2%; } 100% { top: 102%; } } #graph-svg { position: absolute; inset: 0; width: 100%; height: 100%; } #minimap-svg { position: absolute; left: 12px; bottom: 12px; width: 150px; height: 96px; background: rgba(6,13,24,0.78); border: 1px solid var(--border); border-radius: 4px; backdrop-filter: blur(6px); } .ov-toolbar { position: absolute; top: 12px; left: 12px; display: flex; gap: 6px; flex-wrap: wrap; z-index: 5; max-width: 70%; } .ov-btn { font-family: var(--font-mono) !important; font-size: 9px !important; letter-spacing: 0.07em; text-transform: uppercase; padding: 6px 10px; border: 1px solid var(--border) !important; background: rgba(6,13,24,0.9) !important; color: var(--fog) !important; cursor: pointer; backdrop-filter: blur(6px); border-radius: 3px; transition: all 0.15s; box-shadow: none !important; } .ov-btn:hover, .ov-btn.active { border-color: var(--cyan) !important; color: var(--cyan) !important; background: rgba(0,229,255,0.16) !important; box-shadow: 0 0 10px rgba(0,229,255,0.3) !important; } .legend { position: absolute; bottom: 12px; right: 12px; display: flex; gap: 10px; flex-wrap: wrap; z-index: 5; max-width: 55%; justify-content: flex-end; } .legend-item { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 9px; color: var(--fog) !important; } .legend-dot { width: 8px; height: 8px; border-radius: 2px; } #empty-state { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; z-index: 1; } #empty-state .es-icon { font-size: 44px; color: var(--cyan) !important; text-shadow: 0 0 26px rgba(0,229,255,0.6); animation: pulseIcon 3s ease-in-out infinite; } @keyframes pulseIcon { 0%, 100% { opacity: 0.35; transform: scale(1); } 50% { opacity: 1; transform: scale(1.08); } } #empty-state .es-text { font-family: var(--font-mono); font-size: 12px; color: var(--fog) !important; letter-spacing: 0.05em; } #empty-state .es-sub { font-family: var(--font-mono); font-size: 10px; color: var(--fog) !important; opacity: 0.6; } .node { cursor: pointer; } .node-shape { transition: fill-opacity 0.15s, stroke-width 0.15s; } .node-label { font-family: var(--font-mono); font-size: 9.5px; fill: var(--ice); pointer-events: none; text-shadow: 0 0 6px var(--void), 0 0 10px var(--void); } .pulse-ring { animation: pulseRing 2.4s ease-out infinite; transform-box: fill-box; transform-origin: center; } @keyframes pulseRing { 0% { transform: scale(0.9); opacity: 0.7; } 100% { transform: scale(1.9); opacity: 0; } } .aco-trail { stroke-dasharray: 4 3; animation: marchAnts 1s linear infinite; } @keyframes marchAnts { to { stroke-dashoffset: -14; } } .context-menu { position: fixed; z-index: 1000; background: rgba(6,13,24,0.97); border: 1px solid var(--border); border-radius: 4px; backdrop-filter: blur(10px); min-width: 170px; box-shadow: 0 8px 30px rgba(0,0,0,0.55); } .cm-item { padding: 9px 13px; font-family: var(--font-mono); font-size: 10px; color: var(--ice) !important; cursor: pointer; letter-spacing: 0.03em; } .cm-item:hover { background: rgba(0,229,255,0.12); color: var(--cyan) !important; } #insights-output { font-family: var(--font-body) !important; } #insights-output, #insights-output p, #insights-output li, #insights-output span, #insights-output div { color: var(--ice) !important; } #insights-output strong, #insights-output b { color: var(--cyan) !important; } #insights-output em, #insights-output i { color: var(--ice) !important; opacity: 0.92; font-style: italic; } #insights-output h2 { font-family: var(--font-head) !important; font-size: 12px !important; letter-spacing: 0.07em; text-transform: uppercase; color: var(--plasma) !important; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin-top: 16px !important; } @media (max-width: 1024px) { #graph-canvas-wrap { height: 58vh; min-height: 400px; } } """ # ═══════════════════════════════════════════════════════════════════════════ # Head: fonts, D3, and the client-side SKG visualization module # ═══════════════════════════════════════════════════════════════════════════ _PREVIEW_WORDS = sorted( {w.lower() for w in PEOPLE} | {w.lower() for w in PLACES} | {b.lower() for b in BOOKS} | set(BOOK_ABBREVIATIONS.keys()) | THEMES | THEOLOGICAL_CONCEPTS | EVENTS | TIME_PERIODS ) HEAD_EXTRA = f""" """ # ═══════════════════════════════════════════════════════════════════════════ # JS bridges for Gradio events (post-process outputs client-side) # ═══════════════════════════════════════════════════════════════════════════ JS_RENDER_GRAPH = """ (payload) => { if (window.SKG && payload && payload.nodes) { window.SKG.renderGraph(payload); } } """ JS_CLEAR_GRAPH = """ () => { if (window.SKG) { window.SKG.clearGraph(); } } """ JS_RENDER_ACO = """ (swarmState) => { if (window.SKG && swarmState && swarmState.tau) { window.SKG.renderACOTrails(swarmState); } } """ # ═══════════════════════════════════════════════════════════════════════════ # Gradio Blocks app # ═══════════════════════════════════════════════════════════════════════════ with gr.Blocks(title="Scripture Knowledge Graph") as demo: graph_state = gr.State(value=None) algo_state = gr.State(value=None) swarm_state = gr.State(value=None) gr.HTML(HEADER_HTML) with gr.Row(equal_height=True): with gr.Column(scale=1, min_width=280, elem_id="sidebar-left"): notes_input = gr.Textbox( lines=16, placeholder="Paste or type your Bible study notes here…", label="Study Notes", elem_id="notes-input", ) with gr.Row(): upload_btn = gr.UploadButton("⬆ Upload", file_types=[".txt", ".md"], elem_classes="skg-btn", size="sm") sample_btn = gr.Button("📄 Sample", elem_id="sample-btn", elem_classes="skg-btn", size="sm") clear_btn = gr.Button("✕ Clear", elem_id="clear-btn", elem_classes="skg-btn", size="sm") analyze_btn = gr.Button("✦ Build Knowledge Graph", elem_id="analyze-btn", elem_classes="skg-btn-primary") with gr.Column(scale=3, min_width=480): graph_data_json = gr.JSON(visible=False) swarm_data_json = gr.JSON(visible=False) gr.HTML(GRAPH_CONTAINER_HTML, elem_id="graph-outer") with gr.Column(scale=1, min_width=300, elem_id="sidebar-right"): gr.HTML(NODE_DETAIL_HTML) with gr.Tabs(): with gr.Tab("Graph"): stats_html = gr.HTML(EMPTY_STATS_HTML) with gr.Tab("Swarm"): swarm_html = gr.HTML(EMPTY_SWARM_HTML) run_swarm_btn = gr.Button("▶ Run Swarm Analysis", elem_id="swarm-btn", elem_classes="skg-btn skg-btn-swarm") with gr.Tab("AI Insights"): insights_btn = gr.Button("✦ Generate Insights", elem_id="insights-btn", elem_classes="skg-btn-primary") insights_out = gr.Markdown("Build a graph first, then click **Generate Insights**.", elem_id="insights-output") analyze_outputs = [graph_data_json, stats_html, algo_state, graph_state] analyze_btn.click(fn=analyze_notes, inputs=[notes_input], outputs=analyze_outputs).then( fn=None, inputs=[graph_data_json], outputs=[], js=JS_RENDER_GRAPH ) sample_btn.click(fn=load_sample, outputs=[notes_input]).then( fn=analyze_notes, inputs=[notes_input], outputs=analyze_outputs ).then( fn=None, inputs=[graph_data_json], outputs=[], js=JS_RENDER_GRAPH ) upload_btn.upload(fn=handle_upload, inputs=[upload_btn], outputs=[notes_input]).then( fn=analyze_notes, inputs=[notes_input], outputs=analyze_outputs ).then( fn=None, inputs=[graph_data_json], outputs=[], js=JS_RENDER_GRAPH ) clear_btn.click( fn=clear_all, outputs=[notes_input, graph_data_json, stats_html, swarm_html, algo_state, graph_state, swarm_state, swarm_data_json, insights_out], ).then(fn=None, outputs=[], js=JS_CLEAR_GRAPH) run_swarm_btn.click( fn=run_swarm_gen, inputs=[graph_state, algo_state], outputs=[swarm_html, swarm_data_json, swarm_state] ).then( fn=None, inputs=[swarm_data_json], outputs=[], js=JS_RENDER_ACO ) insights_btn.click(fn=generate_insights, inputs=[notes_input, graph_state, algo_state, swarm_state], outputs=[insights_out]) if __name__ == "__main__": demo.queue().launch( theme=gr.themes.Base(primary_hue="cyan", neutral_hue="slate"), css=CUSTOM_CSS, head=HEAD_EXTRA, )