Spaces:
Runtime error
Runtime error
Safoura Banihashemi commited on
Commit ·
fb3203b
1
Parent(s): e661e27
Support pipeline result JSON format
Browse files
app.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
"""
|
| 2 |
Argumentation Graph Visualizer — Hugging Face Space
|
| 3 |
-
Supports
|
| 4 |
-
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import re
|
|
@@ -24,7 +26,7 @@ def parse_xml(filepath):
|
|
| 24 |
nid = elem.get("ID")
|
| 25 |
if not nid:
|
| 26 |
continue
|
| 27 |
-
nodes[nid] = elem.tag
|
| 28 |
for t in filter(None, elem.get("SUP", "").split("|")):
|
| 29 |
sup_edges.append((nid, t.strip()))
|
| 30 |
for t in filter(None, elem.get("ATT", "").split("|")):
|
|
@@ -32,7 +34,8 @@ def parse_xml(filepath):
|
|
| 32 |
return nodes, sup_edges, att_edges
|
| 33 |
|
| 34 |
|
| 35 |
-
def
|
|
|
|
| 36 |
with open(filepath, "r", encoding="utf-8") as f:
|
| 37 |
data = json.load(f)
|
| 38 |
nodes, sup_edges, att_edges = {}, [], []
|
|
@@ -46,7 +49,7 @@ def parse_json(filepath):
|
|
| 46 |
if not id_m:
|
| 47 |
continue
|
| 48 |
nid = id_m.group(1)
|
| 49 |
-
nodes[nid] = tag
|
| 50 |
sup_m = re.search(r'SUP="([^"]+)"', attrs)
|
| 51 |
if sup_m:
|
| 52 |
for t in filter(None, sup_m.group(1).split("|")):
|
|
@@ -58,13 +61,126 @@ def parse_json(filepath):
|
|
| 58 |
return nodes, sup_edges, att_edges, data.get("document", "")
|
| 59 |
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
# ── HTML builder ──────────────────────────────────────────────────────────────
|
| 62 |
|
| 63 |
-
def build_graph_html(
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
return """<!DOCTYPE html>
|
| 70 |
<html lang="en">
|
|
@@ -117,8 +233,7 @@ def build_graph_html(nodes, sup_edges, att_edges, doc_name=""):
|
|
| 117 |
.link-sup { stroke:var(--green); stroke-width:1.8; stroke-opacity:.75; fill:none; }
|
| 118 |
.link-att { stroke:var(--red); stroke-width:2.2; stroke-opacity:.9;
|
| 119 |
stroke-dasharray:7 3; fill:none; }
|
| 120 |
-
.ng { cursor:grab; }
|
| 121 |
-
.ng:active { cursor:grabbing; }
|
| 122 |
.cp { fill:var(--blue-bg); stroke:var(--blue-br); stroke-width:1.5; transition:fill .15s; }
|
| 123 |
.cc { fill:var(--pur-bg); stroke:var(--pur-br); stroke-width:1.5; transition:fill .15s; }
|
| 124 |
.ng:hover .cp { fill:var(--blue-br); }
|
|
@@ -127,10 +242,10 @@ def build_graph_html(nodes, sup_edges, att_edges, doc_name=""):
|
|
| 127 |
text-anchor:middle; dominant-baseline:central; pointer-events:none; }
|
| 128 |
#tt {
|
| 129 |
position:fixed; display:none; background:#1c2030;
|
| 130 |
-
border:1px solid var(--border); border-radius:7px; padding:
|
| 131 |
-
font-family:'IBM Plex Mono',monospace; font-size:11px; line-height:1.
|
| 132 |
-
pointer-events:none; z-index:999; max-width:
|
| 133 |
-
box-shadow:0 8px 32px rgba(0,0,0,.
|
| 134 |
}
|
| 135 |
</style>
|
| 136 |
</head>
|
|
@@ -173,16 +288,15 @@ const g = svg.append("g");
|
|
| 173 |
svg.call(d3.zoom().scaleExtent([0.1, 5]).on("zoom", e => g.attr("transform", e.transform)));
|
| 174 |
|
| 175 |
const sim = d3.forceSimulation(NODES)
|
| 176 |
-
.force("link", d3.forceLink(links).id(d => d.id).distance(
|
| 177 |
-
.force("charge", d3.forceManyBody().strength(-
|
| 178 |
.force("center", d3.forceCenter(W / 2, H / 2))
|
| 179 |
-
.force("collision", d3.forceCollide(
|
| 180 |
|
| 181 |
const lsel = g.append("g").selectAll("line").data(links).join("line")
|
| 182 |
.attr("class", d => d.kind === "sup" ? "link-sup" : "link-att")
|
| 183 |
.attr("marker-end", d => d.kind === "sup" ? "url(#ms)" : "url(#ma)");
|
| 184 |
|
| 185 |
-
// adjacency for tooltip
|
| 186 |
const adj = {};
|
| 187 |
NODES.forEach(n => { adj[n.id] = {sf:[], st:[], af:[], at:[]}; });
|
| 188 |
SUP.forEach(e => { adj[e.target].sf.push(e.source); adj[e.source].st.push(e.target); });
|
|
@@ -198,7 +312,12 @@ const nsel = g.append("g").selectAll("g").data(NODES).join("g")
|
|
| 198 |
.on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }))
|
| 199 |
.on("mouseover", (e, d) => {
|
| 200 |
const a = adj[d.id];
|
| 201 |
-
let h = "<b style='font-size:13px'>" + d.id + "</b>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
if (a.sf.length) h += "<div style='color:#22c55e'>◀ SUP from: " + a.sf.join(", ") + "</div>";
|
| 203 |
if (a.st.length) h += "<div style='color:#22c55e'>▶ SUP to: " + a.st.join(", ") + "</div>";
|
| 204 |
if (a.af.length) h += "<div style='color:#ef4444'>◀ ATT from: " + a.af.join(", ") + "</div>";
|
|
@@ -211,9 +330,9 @@ const nsel = g.append("g").selectAll("g").data(NODES).join("g")
|
|
| 211 |
.on("mousemove", e => { tt.style.left = (e.clientX + 14) + "px"; tt.style.top = (e.clientY - 12) + "px"; })
|
| 212 |
.on("mouseleave", () => { tt.style.display = "none"; });
|
| 213 |
|
| 214 |
-
nsel.append("circle").attr("r",
|
| 215 |
nsel.append("text").attr("class", "nl")
|
| 216 |
-
.style("font-size", d => d.id.length >
|
| 217 |
.text(d => d.id);
|
| 218 |
|
| 219 |
sim.on("tick", () => {
|
|
@@ -226,10 +345,7 @@ sim.on("tick", () => {
|
|
| 226 |
</html>"""
|
| 227 |
|
| 228 |
|
| 229 |
-
# ──
|
| 230 |
-
|
| 231 |
-
# We save the HTML to a temp file and serve it via gr.File so it opens in browser,
|
| 232 |
-
# AND embed it in an iframe inside gr.HTML for inline preview.
|
| 233 |
|
| 234 |
def process_file(file_obj):
|
| 235 |
if file_obj is None:
|
|
@@ -241,26 +357,27 @@ def process_file(file_obj):
|
|
| 241 |
try:
|
| 242 |
if ext == "xml":
|
| 243 |
nodes, sup_edges, att_edges = parse_xml(path)
|
| 244 |
-
doc_name
|
|
|
|
|
|
|
| 245 |
elif ext == "json":
|
| 246 |
-
|
| 247 |
else:
|
| 248 |
return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>Upload .xml or .json</p>"
|
| 249 |
except Exception as ex:
|
| 250 |
-
|
|
|
|
| 251 |
|
| 252 |
-
if not
|
| 253 |
-
return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>No nodes found.</p>"
|
| 254 |
|
| 255 |
-
html = build_graph_html(
|
| 256 |
|
| 257 |
-
# Write to a named temp file
|
| 258 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
|
| 259 |
tmp.write(html)
|
| 260 |
tmp.close()
|
| 261 |
|
| 262 |
-
|
| 263 |
-
srcdoc = html.replace('"', """)
|
| 264 |
iframe = f'<iframe srcdoc="{srcdoc}" style="width:100%;height:680px;border:none;border-radius:8px;"></iframe>'
|
| 265 |
|
| 266 |
return tmp.name, iframe
|
|
@@ -294,7 +411,7 @@ with gr.Blocks(
|
|
| 294 |
with gr.Row(elem_id="title-row"):
|
| 295 |
gr.HTML("""
|
| 296 |
<h1>⚖ Argumentation Graph Visualizer</h1>
|
| 297 |
-
<p>Upload a Demosthenes <b>.xml</b> or
|
| 298 |
→ interactive force-directed argument graph.</p>
|
| 299 |
""")
|
| 300 |
|
|
@@ -302,7 +419,7 @@ with gr.Blocks(
|
|
| 302 |
with gr.Column(scale=1, min_width=260):
|
| 303 |
file_input = gr.File(label="Upload XML or JSON", file_types=[".xml", ".json"])
|
| 304 |
run_btn = gr.Button("Build Graph", variant="primary", size="lg")
|
| 305 |
-
dl_out = gr.File(label="Download graph HTML"
|
| 306 |
gr.HTML("""
|
| 307 |
<div style="font-family:monospace;font-size:11px;color:#64748b;
|
| 308 |
border:1px solid #2a2f42;border-radius:8px;padding:12px;margin-top:6px;">
|
|
@@ -330,8 +447,9 @@ with gr.Blocks(
|
|
| 330 |
<circle cx="6" cy="6" r="5" fill="#4c1d95" stroke="#a855f7" stroke-width="1.5"/>
|
| 331 |
</svg> conc (conclusion)
|
| 332 |
</div>
|
| 333 |
-
<div style="margin-top:8px;color:#475569;">
|
| 334 |
-
|
|
|
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
""")
|
|
|
|
| 1 |
"""
|
| 2 |
Argumentation Graph Visualizer — Hugging Face Space
|
| 3 |
+
Supports:
|
| 4 |
+
- Demosthenes XML (.xml)
|
| 5 |
+
- Structured JSON with inline <prem>/<conc> tags (.json, format A)
|
| 6 |
+
- Pipeline result JSON with segment dicts (.json, format B)
|
| 7 |
"""
|
| 8 |
|
| 9 |
import re
|
|
|
|
| 26 |
nid = elem.get("ID")
|
| 27 |
if not nid:
|
| 28 |
continue
|
| 29 |
+
nodes[nid] = elem.tag.lower()
|
| 30 |
for t in filter(None, elem.get("SUP", "").split("|")):
|
| 31 |
sup_edges.append((nid, t.strip()))
|
| 32 |
for t in filter(None, elem.get("ATT", "").split("|")):
|
|
|
|
| 34 |
return nodes, sup_edges, att_edges
|
| 35 |
|
| 36 |
|
| 37 |
+
def parse_json_inline(filepath):
|
| 38 |
+
"""Format A: sections with 'text' fields containing inline <prem>/<conc> XML tags."""
|
| 39 |
with open(filepath, "r", encoding="utf-8") as f:
|
| 40 |
data = json.load(f)
|
| 41 |
nodes, sup_edges, att_edges = {}, [], []
|
|
|
|
| 49 |
if not id_m:
|
| 50 |
continue
|
| 51 |
nid = id_m.group(1)
|
| 52 |
+
nodes[nid] = tag.lower()
|
| 53 |
sup_m = re.search(r'SUP="([^"]+)"', attrs)
|
| 54 |
if sup_m:
|
| 55 |
for t in filter(None, sup_m.group(1).split("|")):
|
|
|
|
| 61 |
return nodes, sup_edges, att_edges, data.get("document", "")
|
| 62 |
|
| 63 |
|
| 64 |
+
def _clean_id(raw_id):
|
| 65 |
+
"""Convert '[3]' -> '3', strip brackets."""
|
| 66 |
+
return raw_id.strip().strip("[]").strip()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def parse_json_result(filepath):
|
| 70 |
+
"""
|
| 71 |
+
Format B: pipeline result JSON.
|
| 72 |
+
Structure: { doc_name: { section_1: { segment1: {id, role, sup, att, text, ...} } } }
|
| 73 |
+
IDs restart per section, so we prefix them: S1_3, S2_1, etc.
|
| 74 |
+
"""
|
| 75 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 76 |
+
data = json.load(f)
|
| 77 |
+
|
| 78 |
+
doc_name = list(data.keys())[0]
|
| 79 |
+
doc = data[doc_name]
|
| 80 |
+
|
| 81 |
+
nodes, sup_edges, att_edges = {}, [], []
|
| 82 |
+
|
| 83 |
+
# Section index map: section_1 -> 1, section_2 -> 2, ...
|
| 84 |
+
sec_index = {}
|
| 85 |
+
for i, sec_k in enumerate(sorted(doc.keys(), key=lambda k: int(re.search(r'\d+', k).group())), 1):
|
| 86 |
+
sec_index[sec_k] = i
|
| 87 |
+
|
| 88 |
+
# First pass: register all nodes with prefixed IDs
|
| 89 |
+
# node_map[sec_k][raw_id] = prefixed_id
|
| 90 |
+
node_map = {}
|
| 91 |
+
for sec_k, sec in doc.items():
|
| 92 |
+
si = sec_index[sec_k]
|
| 93 |
+
node_map[sec_k] = {}
|
| 94 |
+
for seg in sec.values():
|
| 95 |
+
raw_id = _clean_id(seg.get("id", ""))
|
| 96 |
+
if not raw_id:
|
| 97 |
+
continue
|
| 98 |
+
role = seg.get("role", "PREM").upper()
|
| 99 |
+
ntype = "conc" if role == "CONC" else "prem"
|
| 100 |
+
pid = f"S{si}_{raw_id}"
|
| 101 |
+
node_map[sec_k][raw_id] = pid
|
| 102 |
+
# Store extra info for tooltip
|
| 103 |
+
nodes[pid] = {
|
| 104 |
+
"type": ntype,
|
| 105 |
+
"text": seg.get("text", "")[:120],
|
| 106 |
+
"scheme": seg.get("scheme", ""),
|
| 107 |
+
"atype": seg.get("type", ""),
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
# Second pass: build edges using prefixed IDs
|
| 111 |
+
for sec_k, sec in doc.items():
|
| 112 |
+
si = sec_index[sec_k]
|
| 113 |
+
nmap = node_map[sec_k]
|
| 114 |
+
for seg in sec.values():
|
| 115 |
+
raw_id = _clean_id(seg.get("id", ""))
|
| 116 |
+
if not raw_id or raw_id not in nmap:
|
| 117 |
+
continue
|
| 118 |
+
src_pid = nmap[raw_id]
|
| 119 |
+
|
| 120 |
+
sup_raw = seg.get("sup", "")
|
| 121 |
+
if sup_raw:
|
| 122 |
+
for t in sup_raw.split(","):
|
| 123 |
+
t = _clean_id(t)
|
| 124 |
+
if t and t in nmap:
|
| 125 |
+
sup_edges.append((src_pid, nmap[t]))
|
| 126 |
+
|
| 127 |
+
att_raw = seg.get("att", "")
|
| 128 |
+
if att_raw:
|
| 129 |
+
for t in att_raw.split(","):
|
| 130 |
+
t = _clean_id(t)
|
| 131 |
+
if t and t in nmap:
|
| 132 |
+
att_edges.append((src_pid, nmap[t]))
|
| 133 |
+
|
| 134 |
+
return nodes, sup_edges, att_edges, doc_name
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def detect_and_parse(filepath):
|
| 138 |
+
"""Auto-detect JSON format and dispatch to correct parser."""
|
| 139 |
+
with open(filepath, "r", encoding="utf-8") as f:
|
| 140 |
+
data = json.load(f)
|
| 141 |
+
|
| 142 |
+
# Format B: single top-level key whose value has section_N keys
|
| 143 |
+
# containing segment dicts with an 'id' field like '[1]'
|
| 144 |
+
if isinstance(data, dict) and len(data) == 1:
|
| 145 |
+
inner = list(data.values())[0]
|
| 146 |
+
if isinstance(inner, dict):
|
| 147 |
+
first_val = next(iter(inner.values()), None)
|
| 148 |
+
if isinstance(first_val, dict):
|
| 149 |
+
first_seg = next(iter(first_val.values()), None)
|
| 150 |
+
if isinstance(first_seg, dict) and "id" in first_seg:
|
| 151 |
+
nodes, sup_edges, att_edges, doc_name = parse_json_result(filepath)
|
| 152 |
+
# nodes here is dict of pid -> {type, text, ...}
|
| 153 |
+
# convert to simple type map for graph builder
|
| 154 |
+
node_types = {k: v["type"] for k, v in nodes.items()}
|
| 155 |
+
node_meta = nodes
|
| 156 |
+
return node_types, sup_edges, att_edges, doc_name, node_meta
|
| 157 |
+
|
| 158 |
+
# Format A: inline XML tags in text fields
|
| 159 |
+
nodes, sup_edges, att_edges, doc_name = parse_json_inline(filepath)
|
| 160 |
+
node_types = nodes
|
| 161 |
+
node_meta = {k: {"type": v, "text": "", "scheme": "", "atype": ""} for k, v in nodes.items()}
|
| 162 |
+
return node_types, sup_edges, att_edges, doc_name, node_meta
|
| 163 |
+
|
| 164 |
+
|
| 165 |
# ── HTML builder ──────────────────────────────────────────────────────────────
|
| 166 |
|
| 167 |
+
def build_graph_html(node_types, sup_edges, att_edges, doc_name="", node_meta=None):
|
| 168 |
+
if node_meta is None:
|
| 169 |
+
node_meta = {}
|
| 170 |
+
|
| 171 |
+
nodes_js = json.dumps([
|
| 172 |
+
{
|
| 173 |
+
"id": k,
|
| 174 |
+
"type": v,
|
| 175 |
+
"text": node_meta.get(k, {}).get("text", ""),
|
| 176 |
+
"scheme": node_meta.get(k, {}).get("scheme", ""),
|
| 177 |
+
"atype": node_meta.get(k, {}).get("atype", ""),
|
| 178 |
+
}
|
| 179 |
+
for k, v in node_types.items()
|
| 180 |
+
])
|
| 181 |
+
sup_js = json.dumps([{"source": s, "target": t} for s, t in sup_edges])
|
| 182 |
+
att_js = json.dumps([{"source": s, "target": t} for s, t in att_edges])
|
| 183 |
+
label = doc_name.replace("_", " ") if doc_name else "argumentation graph"
|
| 184 |
|
| 185 |
return """<!DOCTYPE html>
|
| 186 |
<html lang="en">
|
|
|
|
| 233 |
.link-sup { stroke:var(--green); stroke-width:1.8; stroke-opacity:.75; fill:none; }
|
| 234 |
.link-att { stroke:var(--red); stroke-width:2.2; stroke-opacity:.9;
|
| 235 |
stroke-dasharray:7 3; fill:none; }
|
| 236 |
+
.ng { cursor:grab; } .ng:active { cursor:grabbing; }
|
|
|
|
| 237 |
.cp { fill:var(--blue-bg); stroke:var(--blue-br); stroke-width:1.5; transition:fill .15s; }
|
| 238 |
.cc { fill:var(--pur-bg); stroke:var(--pur-br); stroke-width:1.5; transition:fill .15s; }
|
| 239 |
.ng:hover .cp { fill:var(--blue-br); }
|
|
|
|
| 242 |
text-anchor:middle; dominant-baseline:central; pointer-events:none; }
|
| 243 |
#tt {
|
| 244 |
position:fixed; display:none; background:#1c2030;
|
| 245 |
+
border:1px solid var(--border); border-radius:7px; padding:10px 14px;
|
| 246 |
+
font-family:'IBM Plex Mono',monospace; font-size:11px; line-height:1.8;
|
| 247 |
+
pointer-events:none; z-index:999; max-width:300px; color:var(--text);
|
| 248 |
+
box-shadow:0 8px 32px rgba(0,0,0,.6);
|
| 249 |
}
|
| 250 |
</style>
|
| 251 |
</head>
|
|
|
|
| 288 |
svg.call(d3.zoom().scaleExtent([0.1, 5]).on("zoom", e => g.attr("transform", e.transform)));
|
| 289 |
|
| 290 |
const sim = d3.forceSimulation(NODES)
|
| 291 |
+
.force("link", d3.forceLink(links).id(d => d.id).distance(90).strength(0.5))
|
| 292 |
+
.force("charge", d3.forceManyBody().strength(-360))
|
| 293 |
.force("center", d3.forceCenter(W / 2, H / 2))
|
| 294 |
+
.force("collision", d3.forceCollide(22));
|
| 295 |
|
| 296 |
const lsel = g.append("g").selectAll("line").data(links).join("line")
|
| 297 |
.attr("class", d => d.kind === "sup" ? "link-sup" : "link-att")
|
| 298 |
.attr("marker-end", d => d.kind === "sup" ? "url(#ms)" : "url(#ma)");
|
| 299 |
|
|
|
|
| 300 |
const adj = {};
|
| 301 |
NODES.forEach(n => { adj[n.id] = {sf:[], st:[], af:[], at:[]}; });
|
| 302 |
SUP.forEach(e => { adj[e.target].sf.push(e.source); adj[e.source].st.push(e.target); });
|
|
|
|
| 312 |
.on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }))
|
| 313 |
.on("mouseover", (e, d) => {
|
| 314 |
const a = adj[d.id];
|
| 315 |
+
let h = "<b style='font-size:13px'>" + d.id + "</b>";
|
| 316 |
+
h += " <span style='color:#64748b'>" + d.type + "</span>";
|
| 317 |
+
if (d.scheme) h += " <span style='color:#94a3b8'>· " + d.scheme + "</span>";
|
| 318 |
+
if (d.atype) h += " <span style='color:#94a3b8'>· type:" + d.atype + "</span>";
|
| 319 |
+
h += "<hr style='border:none;border-top:1px solid #2a2f42;margin:5px 0'>";
|
| 320 |
+
if (d.text) h += "<div style='color:#94a3b8;font-size:10px;margin-bottom:5px;line-height:1.5'>" + d.text + (d.text.length >= 120 ? "…" : "") + "</div>";
|
| 321 |
if (a.sf.length) h += "<div style='color:#22c55e'>◀ SUP from: " + a.sf.join(", ") + "</div>";
|
| 322 |
if (a.st.length) h += "<div style='color:#22c55e'>▶ SUP to: " + a.st.join(", ") + "</div>";
|
| 323 |
if (a.af.length) h += "<div style='color:#ef4444'>◀ ATT from: " + a.af.join(", ") + "</div>";
|
|
|
|
| 330 |
.on("mousemove", e => { tt.style.left = (e.clientX + 14) + "px"; tt.style.top = (e.clientY - 12) + "px"; })
|
| 331 |
.on("mouseleave", () => { tt.style.display = "none"; });
|
| 332 |
|
| 333 |
+
nsel.append("circle").attr("r", 14).attr("class", d => d.type === "prem" ? "cp" : "cc");
|
| 334 |
nsel.append("text").attr("class", "nl")
|
| 335 |
+
.style("font-size", d => d.id.length > 5 ? "6.5px" : "8px")
|
| 336 |
.text(d => d.id);
|
| 337 |
|
| 338 |
sim.on("tick", () => {
|
|
|
|
| 345 |
</html>"""
|
| 346 |
|
| 347 |
|
| 348 |
+
# ── Gradio handler ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
def process_file(file_obj):
|
| 351 |
if file_obj is None:
|
|
|
|
| 357 |
try:
|
| 358 |
if ext == "xml":
|
| 359 |
nodes, sup_edges, att_edges = parse_xml(path)
|
| 360 |
+
doc_name = os.path.basename(path).replace(".xml", "").replace("_", " ")
|
| 361 |
+
node_meta = {k: {"type": v, "text": "", "scheme": "", "atype": ""} for k, v in nodes.items()}
|
| 362 |
+
node_types = nodes
|
| 363 |
elif ext == "json":
|
| 364 |
+
node_types, sup_edges, att_edges, doc_name, node_meta = detect_and_parse(path)
|
| 365 |
else:
|
| 366 |
return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>Upload .xml or .json</p>"
|
| 367 |
except Exception as ex:
|
| 368 |
+
import traceback
|
| 369 |
+
return None, f"<pre style='color:#ef4444;font-family:monospace;padding:20px'>{traceback.format_exc()}</pre>"
|
| 370 |
|
| 371 |
+
if not node_types:
|
| 372 |
+
return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>No nodes found in file.</p>"
|
| 373 |
|
| 374 |
+
html = build_graph_html(node_types, sup_edges, att_edges, doc_name, node_meta)
|
| 375 |
|
|
|
|
| 376 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
|
| 377 |
tmp.write(html)
|
| 378 |
tmp.close()
|
| 379 |
|
| 380 |
+
srcdoc = html.replace("&", "&").replace('"', """)
|
|
|
|
| 381 |
iframe = f'<iframe srcdoc="{srcdoc}" style="width:100%;height:680px;border:none;border-radius:8px;"></iframe>'
|
| 382 |
|
| 383 |
return tmp.name, iframe
|
|
|
|
| 411 |
with gr.Row(elem_id="title-row"):
|
| 412 |
gr.HTML("""
|
| 413 |
<h1>⚖ Argumentation Graph Visualizer</h1>
|
| 414 |
+
<p>Upload a Demosthenes <b>.xml</b> or pipeline result <b>.json</b>
|
| 415 |
→ interactive force-directed argument graph.</p>
|
| 416 |
""")
|
| 417 |
|
|
|
|
| 419 |
with gr.Column(scale=1, min_width=260):
|
| 420 |
file_input = gr.File(label="Upload XML or JSON", file_types=[".xml", ".json"])
|
| 421 |
run_btn = gr.Button("Build Graph", variant="primary", size="lg")
|
| 422 |
+
dl_out = gr.File(label="Download graph HTML")
|
| 423 |
gr.HTML("""
|
| 424 |
<div style="font-family:monospace;font-size:11px;color:#64748b;
|
| 425 |
border:1px solid #2a2f42;border-radius:8px;padding:12px;margin-top:6px;">
|
|
|
|
| 447 |
<circle cx="6" cy="6" r="5" fill="#4c1d95" stroke="#a855f7" stroke-width="1.5"/>
|
| 448 |
</svg> conc (conclusion)
|
| 449 |
</div>
|
| 450 |
+
<div style="margin-top:8px;color:#475569;line-height:1.6;">
|
| 451 |
+
Accepts .xml · .json (inline tags) · .json (pipeline result)<br><br>
|
| 452 |
+
Hover node �� see text snippet, type, scheme, relations
|
| 453 |
</div>
|
| 454 |
</div>
|
| 455 |
""")
|