|
1 | | -/** |
2 | | - * src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx |
3 | | - */ |
4 | 1 | import { useState } from "react"; |
| 2 | +import { GitMerge, ArrowRight, CheckCircle2, AlertCircle, Loader2 } from "lucide-react"; |
5 | 3 | import { logEvent } from "../../store/registryStore"; |
6 | 4 |
|
7 | | -const THEME_CSS = ` |
8 | | - .glass-panel { |
9 | | - background: linear-gradient(135deg, rgba(13,17,23,0.75), rgba(22,27,34,0.6)); |
10 | | - backdrop-filter: blur(16px) saturate(1.2); |
11 | | - -webkit-backdrop-filter: blur(16px) saturate(1.2); |
12 | | - border: 1px solid rgba(88,166,255,0.2); |
13 | | - box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 1px 1px 0 rgba(255,255,255,0.05); |
14 | | - } |
15 | | -`; |
| 5 | +interface FieldRow { label: string; primary: string; duplicate: string; differs: boolean } |
| 6 | + |
| 7 | +const MOCK_FIELDS: FieldRow[] = [ |
| 8 | + { label: "Name", primary: "Sample Company Inc.", duplicate: "Sample Company", differs: true }, |
| 9 | + { label: "Founded", primary: "2004-05-12", duplicate: "2004-05-12", differs: false }, |
| 10 | + { label: "Type", primary: "Organization", duplicate: "Organisation", differs: true }, |
| 11 | + { label: "Country", primary: "US", duplicate: "US", differs: false }, |
| 12 | +]; |
16 | 13 |
|
17 | 14 | export function DiffMergeWorkspace() { |
18 | | - const [primaryId, setPrimaryId] = useState("n-primary-1"); |
| 15 | + const [primaryId, setPrimaryId] = useState("n-primary-1"); |
19 | 16 | const [duplicateId, setDuplicateId] = useState("n-dup-2"); |
| 17 | + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); |
| 18 | + const [msg, setMsg] = useState(""); |
20 | 19 |
|
21 | | - const [msg, setMsg] = useState(""); |
22 | | - |
23 | | - const handleMerge = async () => { |
| 20 | + async function handleMerge() { |
| 21 | + setStatus("loading"); |
| 22 | + setMsg(""); |
24 | 23 | try { |
25 | 24 | const res = await fetch("/api/enrich/merge", { |
26 | 25 | method: "POST", |
27 | 26 | headers: { "Content-Type": "application/json" }, |
28 | | - body: JSON.stringify({ primary_id: primaryId, duplicate_ids: [duplicateId] }) |
| 27 | + body: JSON.stringify({ primary_id: primaryId, duplicate_ids: [duplicateId] }), |
29 | 28 | }); |
30 | 29 | const data = await res.json(); |
31 | 30 | if (data.merged_into) { |
32 | | - setMsg(`Merge success: redirected ${data.edges_updated} edges to ${data.merged_into}`); |
33 | | - logEvent("merge", `Merged ${duplicateId} → ${data.merged_into} · ${data.edges_updated} edges redirected`, { |
34 | | - primary: data.merged_into, |
35 | | - duplicate: duplicateId, |
36 | | - edgesUpdated: data.edges_updated, |
| 31 | + setStatus("success"); |
| 32 | + setMsg(`Merged → ${data.merged_into} · ${data.edges_updated ?? 0} edges redirected`); |
| 33 | + logEvent("merge", `Merged ${duplicateId} → ${data.merged_into} · ${data.edges_updated ?? 0} edges redirected`, { |
| 34 | + primary: data.merged_into, duplicate: duplicateId, edgesUpdated: data.edges_updated, |
37 | 35 | }); |
38 | 36 | } else { |
39 | | - setMsg("Merge failed..."); |
| 37 | + throw new Error(data.detail || "Unexpected response"); |
40 | 38 | } |
41 | | - } catch (err) { |
42 | | - setMsg("Error calling merge endpoint."); |
| 39 | + } catch (e: unknown) { |
| 40 | + setStatus("error"); |
| 41 | + setMsg(e instanceof Error ? e.message : "Merge failed"); |
43 | 42 | } |
44 | | - }; |
| 43 | + } |
45 | 44 |
|
46 | 45 | return ( |
47 | | - <div style={{ display: "flex", flexDirection: "column", width: "100%", height: "100%", background: "#0d1117", padding: 32, gap: 24, boxSizing: "border-box" }}> |
48 | | - <style>{THEME_CSS}</style> |
49 | | - <div> |
50 | | - <h1 style={{ margin: "0 0 8px 0", color: "#fff" }}>Entity Diff & Merge</h1> |
51 | | - <p style={{ margin: 0, color: "#8b949e" }}>Compare suspected duplicate entities and reconcile them.</p> |
52 | | - </div> |
53 | | - |
54 | | - <div style={{ display: "flex", gap: 24, flex: 1 }}> |
55 | | - {/* Primary View */} |
56 | | - <div className="glass-panel" style={{ flex: 1, borderRadius: 12, padding: 24 }}> |
57 | | - <h3 style={{ color: "#58a6ff", margin: "0 0 16px 0", borderBottom: "1px solid rgba(88,166,255,0.2)", paddingBottom: 8 }}>Primary Entity</h3> |
58 | | - <label style={{ display: "block", color: "#c9d1d9", marginBottom: 8, fontSize: 13 }}>Primary Node ID</label> |
59 | | - <input |
60 | | - value={primaryId} onChange={e => setPrimaryId(e.target.value)} |
61 | | - style={{ width: "100%", background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)", color: "#fff", padding: "8px 12px", borderRadius: 6, marginBottom: 24 }} |
62 | | - /> |
63 | | - |
64 | | - <div style={{ background: "rgba(0,0,0,0.2)", padding: 16, borderRadius: 6 }}> |
65 | | - <div style={{ color: "#8b949e", fontSize: 12, marginBottom: 4 }}>Name</div> |
66 | | - <div style={{ color: "#fff", fontSize: 14 }}>Sample Company Inc.</div> |
67 | | - |
68 | | - <div style={{ color: "#8b949e", fontSize: 12, marginTop: 16, marginBottom: 4 }}>Founded</div> |
69 | | - <div style={{ color: "#fff", fontSize: 14 }}>2004-05-12</div> |
| 46 | + <div className="ws-page ws-scroll"> |
| 47 | + <div className="ws-padded" style={{ display: "flex", flexDirection: "column", gap: 22, maxWidth: 1000, margin: "0 auto", width: "100%" }}> |
| 48 | + {/* Header */} |
| 49 | + <div style={{ display: "flex", alignItems: "center", gap: 14 }}> |
| 50 | + <div style={{ width: 42, height: 42, borderRadius: 13, background: "var(--ws-purple-soft)", border: "1px solid rgba(192,132,252,0.3)", display: "grid", placeItems: "center", color: "var(--ws-purple)", flexShrink: 0 }}> |
| 51 | + <GitMerge size={20} /> |
| 52 | + </div> |
| 53 | + <div> |
| 54 | + <h2 className="ws-title" style={{ fontSize: 18 }}>Entity Diff & Merge</h2> |
| 55 | + <div className="ws-body" style={{ marginTop: 2 }}>Compare suspected duplicates side-by-side and reconcile them into a single canonical entity.</div> |
70 | 56 | </div> |
71 | 57 | </div> |
72 | 58 |
|
73 | | - {/* Duplicate View */} |
74 | | - <div className="glass-panel" style={{ flex: 1, borderRadius: 12, padding: 24 }}> |
75 | | - <h3 style={{ color: "#ff7b72", margin: "0 0 16px 0", borderBottom: "1px solid rgba(255,123,114,0.2)", paddingBottom: 8 }}>Duplicate Entity</h3> |
76 | | - <label style={{ display: "block", color: "#c9d1d9", marginBottom: 8, fontSize: 13 }}>Duplicate Node ID</label> |
77 | | - <input |
78 | | - value={duplicateId} onChange={e => setDuplicateId(e.target.value)} |
79 | | - style={{ width: "100%", background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)", color: "#fff", padding: "8px 12px", borderRadius: 6, marginBottom: 24 }} |
80 | | - /> |
81 | | - |
82 | | - <div style={{ background: "rgba(0,0,0,0.2)", padding: 16, borderRadius: 6 }}> |
83 | | - <div style={{ color: "#d2a8ff", fontSize: 12, marginBottom: 4 }}>Name</div> |
84 | | - {/* Amber highlight for differing values */} |
85 | | - <div style={{ color: "#d29922", fontSize: 14, fontWeight: "bold" }}>Sample Company</div> |
| 59 | + {/* ID inputs */} |
| 60 | + <div style={{ display: "grid", gridTemplateColumns: "1fr auto 1fr", gap: 12, alignItems: "end" }}> |
| 61 | + <div> |
| 62 | + <label className="ws-label">Primary Node ID (keep)</label> |
| 63 | + <input className="ws-input" value={primaryId} onChange={(e) => { setPrimaryId(e.target.value); setStatus("idle"); }} placeholder="e.g. n-primary-1" /> |
| 64 | + </div> |
| 65 | + <div style={{ paddingBottom: 2, color: "var(--ws-text-dim)" }}> |
| 66 | + <ArrowRight size={18} /> |
| 67 | + </div> |
| 68 | + <div> |
| 69 | + <label className="ws-label">Duplicate Node ID (remove)</label> |
| 70 | + <input className="ws-input" value={duplicateId} onChange={(e) => { setDuplicateId(e.target.value); setStatus("idle"); }} placeholder="e.g. n-dup-2" /> |
| 71 | + </div> |
| 72 | + </div> |
86 | 73 |
|
87 | | - <div style={{ color: "#8b949e", fontSize: 12, marginTop: 16, marginBottom: 4 }}>Founded</div> |
88 | | - <div style={{ color: "#fff", fontSize: 14 }}>2004-05-12</div> |
| 74 | + {/* Diff table */} |
| 75 | + <div className="ws-card" style={{ padding: 0, overflow: "hidden" }}> |
| 76 | + <div style={{ padding: "6px 16px", background: "rgba(242,182,109,0.06)", borderBottom: "1px solid rgba(242,182,109,0.15)", display: "flex", alignItems: "center", gap: 6 }}> |
| 77 | + <span style={{ fontSize: 10, fontWeight: 700, color: "var(--ws-amber)", letterSpacing: "0.08em", textTransform: "uppercase" }}>Sample preview</span> |
| 78 | + <span style={{ fontSize: 11, color: "var(--ws-text-dim)" }}>— field comparison will load from the graph once the backend is connected</span> |
89 | 79 | </div> |
| 80 | + <div style={{ display: "grid", gridTemplateColumns: "140px 1fr 1fr", background: "rgba(0,0,0,0.28)", borderBottom: "1px solid var(--ws-border)" }}> |
| 81 | + <div style={{ padding: "10px 16px", fontSize: 11, fontWeight: 700, color: "var(--ws-text-dim)", letterSpacing: "0.08em", textTransform: "uppercase" }}>Field</div> |
| 82 | + <div style={{ padding: "10px 16px", fontSize: 11, fontWeight: 700, color: "var(--ws-accent)", letterSpacing: "0.08em", textTransform: "uppercase", borderLeft: "1px solid var(--ws-border)" }}>Primary (keep)</div> |
| 83 | + <div style={{ padding: "10px 16px", fontSize: 11, fontWeight: 700, color: "#fca5a5", letterSpacing: "0.08em", textTransform: "uppercase", borderLeft: "1px solid var(--ws-border)" }}>Duplicate (remove)</div> |
| 84 | + </div> |
| 85 | + {MOCK_FIELDS.map((row) => ( |
| 86 | + <div key={row.label} style={{ display: "grid", gridTemplateColumns: "140px 1fr 1fr", borderBottom: "1px solid rgba(74,163,255,0.06)", background: row.differs ? "rgba(242,182,109,0.03)" : "transparent" }}> |
| 87 | + <div style={{ padding: "12px 16px", fontSize: 12, fontWeight: 600, color: "var(--ws-text-dim)" }}>{row.label}</div> |
| 88 | + <div style={{ padding: "12px 16px", fontSize: 13, color: "var(--ws-text)", borderLeft: "1px solid var(--ws-border)" }}>{row.primary}</div> |
| 89 | + <div style={{ padding: "12px 16px", fontSize: 13, color: row.differs ? "#fbbf24" : "var(--ws-text)", fontWeight: row.differs ? 700 : 400, borderLeft: "1px solid var(--ws-border)" }}> |
| 90 | + {row.duplicate} |
| 91 | + {row.differs && <span className="ws-pill ws-pill--amber" style={{ marginLeft: 8, fontSize: 9 }}>diff</span>} |
| 92 | + </div> |
| 93 | + </div> |
| 94 | + ))} |
90 | 95 | </div> |
91 | | - </div> |
92 | 96 |
|
93 | | - <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> |
94 | | - <div style={{ color: "#58a6ff" }}>{msg}</div> |
95 | | - <button |
96 | | - onClick={handleMerge} |
97 | | - style={{ background: "#238636", color: "#fff", border: "none", padding: "10px 24px", borderRadius: 6, fontWeight: 600, cursor: "pointer", fontSize: 16 }} |
98 | | - > |
99 | | - Confirm Merge |
100 | | - </button> |
| 97 | + {/* Status & action */} |
| 98 | + <div style={{ display: "flex", alignItems: "center", gap: 12 }}> |
| 99 | + {status === "success" && ( |
| 100 | + <div style={{ display: "flex", alignItems: "center", gap: 7, color: "#6ee7b7", fontSize: 13 }}> |
| 101 | + <CheckCircle2 size={15} />{msg} |
| 102 | + </div> |
| 103 | + )} |
| 104 | + {status === "error" && ( |
| 105 | + <div style={{ display: "flex", alignItems: "center", gap: 7, color: "#fca5a5", fontSize: 13 }}> |
| 106 | + <AlertCircle size={15} />{msg} |
| 107 | + </div> |
| 108 | + )} |
| 109 | + <button |
| 110 | + className="ws-btn ws-btn--primary" |
| 111 | + onClick={handleMerge} |
| 112 | + disabled={status === "loading" || !primaryId || !duplicateId} |
| 113 | + style={{ marginLeft: "auto" }} |
| 114 | + > |
| 115 | + {status === "loading" ? <><Loader2 size={14} className="ws-spin" />Merging…</> : <><GitMerge size={14} />Confirm Merge</>} |
| 116 | + </button> |
| 117 | + </div> |
101 | 118 | </div> |
102 | | - |
103 | 119 | </div> |
104 | 120 | ); |
105 | 121 | } |
0 commit comments