pi-weave 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/package.json +69 -0
- package/skills/.gitkeep +0 -0
- package/skills/weave-explore/SKILL.md +64 -0
- package/skills/weave-notepad/SKILL.md +77 -0
- package/src/core/frontmatter.ts +119 -0
- package/src/core/git.ts +194 -0
- package/src/core/graph/build.ts +258 -0
- package/src/core/graph/current.ts +107 -0
- package/src/core/graph/model.ts +63 -0
- package/src/core/graph/wikilinks.ts +28 -0
- package/src/core/index.ts +25 -0
- package/src/core/languages.ts +76 -0
- package/src/core/mutex.ts +34 -0
- package/src/core/paths.ts +37 -0
- package/src/core/repoIndex.ts +404 -0
- package/src/core/slug.ts +28 -0
- package/src/core/summaries.ts +300 -0
- package/src/core/types.ts +155 -0
- package/src/core/vault.ts +254 -0
- package/src/core/workspace.ts +85 -0
- package/src/pi/index.ts +191 -0
- package/src/pi/summarize.ts +134 -0
- package/src/pi/tools/noteTool.ts +178 -0
- package/src/pi/tools/repoTool.ts +94 -0
- package/src/pi/viewer/browser.ts +29 -0
- package/src/pi/viewer/page.ts +1316 -0
- package/src/pi/viewer/server.ts +229 -0
- package/src/pi/viewer/tui/explorer.ts +597 -0
- package/src/pi/viewer/tui/model.ts +1011 -0
- package/src/pi/viewer/tui/run.ts +69 -0
- package/src/pi/viewer/tui/theme.ts +90 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The weave-view page (v2): a single self-contained HTML string — inline
|
|
3
|
+
* CSS+JS, zero external resources (docs/weave-view.md §5, docs/weave-view-v2.md).
|
|
4
|
+
*
|
|
5
|
+
* v2 pillars: provenance is the hero (ring style + glyph + filter, never
|
|
6
|
+
* color alone); three surfaces over one model (Graph / List / Detail);
|
|
7
|
+
* overview-first status strip; explicit focus mode (1-hop, visibility-only).
|
|
8
|
+
* The List surface is an expandable index tree over the `contains`
|
|
9
|
+
* hierarchy (repository → modules → files), so you can drill into the index
|
|
10
|
+
* and navigate to any file.
|
|
11
|
+
*
|
|
12
|
+
* NOTE for contributors: the rendered page must contain NO backtick and NO
|
|
13
|
+
* `${` — the page is a TS template literal, and a CI guard asserts the
|
|
14
|
+
* rendered output stays clean. Regex backslashes are `\\`-escaped; backtick
|
|
15
|
+
* matching uses the `\x60` hex escape so no literal backtick reaches output.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export function renderPage(): string {
|
|
19
|
+
return PAGE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PAGE = `<!DOCTYPE html>
|
|
23
|
+
<html lang="en" data-theme="dark">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8">
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
27
|
+
<title>pi-weave · knowledge view</title>
|
|
28
|
+
<style>
|
|
29
|
+
:root[data-theme="dark"] {
|
|
30
|
+
--bg:#0b1020; --surface:#131a2e; --raised:#1a2340; --line:#26324d;
|
|
31
|
+
--line-strong:#33415f; --text:#e8ecf6; --muted:#93a0bd; --faint:#64748b;
|
|
32
|
+
--accent:#a78bfa; --ok:#34d399; --warn:#f59e0b; --danger:#f87171;
|
|
33
|
+
}
|
|
34
|
+
:root[data-theme="light"] {
|
|
35
|
+
--bg:#f8fafc; --surface:#ffffff; --raised:#f1f5f9; --line:#e2e8f0;
|
|
36
|
+
--line-strong:#cbd5e1; --text:#0f172a; --muted:#64748b; --faint:#94a3b8;
|
|
37
|
+
--accent:#7c3aed; --ok:#059669; --warn:#d97706; --danger:#dc2626;
|
|
38
|
+
}
|
|
39
|
+
* { box-sizing: border-box; }
|
|
40
|
+
html, body { height: 100%; }
|
|
41
|
+
body { margin: 0; font: 13px/1.5 -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
42
|
+
background: var(--bg); color: var(--text); overflow: hidden; }
|
|
43
|
+
button, select, input { font: inherit; color: inherit; }
|
|
44
|
+
button { background: var(--raised); color: var(--text); border: 1px solid var(--line);
|
|
45
|
+
border-radius: 7px; padding: 4px 10px; cursor: pointer; }
|
|
46
|
+
button:hover { border-color: var(--accent); }
|
|
47
|
+
button:focus-visible, select:focus-visible, input:focus-visible, .row:focus-visible,
|
|
48
|
+
.link-row:focus-visible, g.node:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
49
|
+
.muted { color: var(--muted); }
|
|
50
|
+
|
|
51
|
+
/* ---------- header ---------- */
|
|
52
|
+
header { position: fixed; inset: 0 0 auto 0; padding: 8px 14px; display: flex; gap: 10px;
|
|
53
|
+
align-items: center; flex-wrap: wrap; background: var(--surface);
|
|
54
|
+
border-bottom: 1px solid var(--line); z-index: 10; }
|
|
55
|
+
header h1 { font-size: 15px; margin: 0; font-weight: 600; letter-spacing: -0.01em; }
|
|
56
|
+
header h1 span { color: var(--accent); }
|
|
57
|
+
.tabs { display: flex; gap: 2px; background: var(--raised); border: 1px solid var(--line);
|
|
58
|
+
border-radius: 8px; padding: 2px; }
|
|
59
|
+
.tabs button { border: none; background: none; padding: 3px 10px; font-size: 12px; font-weight: 600;
|
|
60
|
+
text-transform: uppercase; letter-spacing: .08em; color: var(--muted); border-radius: 6px; }
|
|
61
|
+
.tabs button.active { background: var(--accent); color: #0b1020; }
|
|
62
|
+
#search { background: var(--raised); border: 1px solid var(--line); color: var(--text);
|
|
63
|
+
border-radius: 7px; padding: 5px 10px; width: 200px; }
|
|
64
|
+
#search:focus { outline: none; border-color: var(--accent); }
|
|
65
|
+
.zoomgrp button { width: 30px; }
|
|
66
|
+
#status { display: flex; align-items: center; gap: 8px; margin-left: auto; font-size: 12px;
|
|
67
|
+
font-feature-settings: "tnum"; color: var(--muted); }
|
|
68
|
+
#status .stat { white-space: nowrap; }
|
|
69
|
+
#status .stat.stale { color: var(--warn); font-weight: 600; }
|
|
70
|
+
#stamp { color: var(--faint); font-size: 11px; white-space: nowrap; }
|
|
71
|
+
.provbar { display: inline-flex; width: 64px; height: 8px; border-radius: 4px; overflow: hidden;
|
|
72
|
+
background: var(--line); vertical-align: middle; }
|
|
73
|
+
.provbar i { display: block; height: 100%; }
|
|
74
|
+
.provbar .p-human { background: var(--ok); }
|
|
75
|
+
.provbar .p-agent { background: var(--accent); }
|
|
76
|
+
.provbar .p-generated { background: var(--faint); }
|
|
77
|
+
.provbar.big { width: 100%; height: 12px; }
|
|
78
|
+
|
|
79
|
+
/* ---------- graph ---------- */
|
|
80
|
+
#graph { display: block; width: 100vw; height: 100vh; touch-action: none;
|
|
81
|
+
background: radial-gradient(1200px 800px at 50% 40%, var(--surface) 0%, var(--bg) 70%); }
|
|
82
|
+
text.label { fill: var(--text); font-size: 12px; pointer-events: none;
|
|
83
|
+
paint-order: stroke; stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round; }
|
|
84
|
+
text.glyph { font-size: 7px; fill: var(--bg); pointer-events: none; text-anchor: middle; }
|
|
85
|
+
g.node { cursor: pointer; }
|
|
86
|
+
g.node .shape { transition: transform 120ms ease; }
|
|
87
|
+
g.node.hovered .shape { transform: scale(1.06); }
|
|
88
|
+
g.node.hovered .halo { opacity: .15; }
|
|
89
|
+
g.node .halo { transition: opacity 120ms ease; }
|
|
90
|
+
.dim { opacity: .14; }
|
|
91
|
+
|
|
92
|
+
/* ---------- surfaces ---------- */
|
|
93
|
+
.surface { position: fixed; top: 48px; bottom: 0; overflow-y: auto; background: var(--bg); }
|
|
94
|
+
#list { left: 0; width: 340px; border-right: 1px solid var(--line); display: none; padding: 12px; }
|
|
95
|
+
body.list-open #list { display: block; }
|
|
96
|
+
#list-toggle[aria-pressed="true"] { background: var(--accent); color: #0b1020; }
|
|
97
|
+
#health { left: 0; right: 0; display: none; padding: 20px 24px; max-width: 760px; }
|
|
98
|
+
#health h2 { font-size: 18px; line-height: 1.3; font-weight: 650; margin: 0 0 4px; }
|
|
99
|
+
#health h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted);
|
|
100
|
+
margin: 20px 0 8px; }
|
|
101
|
+
.list-toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
|
|
102
|
+
.list-toolbar select { background: var(--raised); border: 1px solid var(--line); border-radius: 6px;
|
|
103
|
+
padding: 3px 6px; font-size: 12px; }
|
|
104
|
+
#list-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
105
|
+
.row { display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 7px;
|
|
106
|
+
cursor: pointer; border: 1px solid transparent; }
|
|
107
|
+
.row:hover { background: var(--raised); }
|
|
108
|
+
.row.selected { background: var(--raised); border-color: var(--accent); }
|
|
109
|
+
.row-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
110
|
+
.row-meta { color: var(--faint); font-size: 11px; white-space: nowrap; }
|
|
111
|
+
.chev { display: inline-block; width: 14px; flex: none; text-align: center; color: var(--muted);
|
|
112
|
+
font-size: 10px; user-select: none; }
|
|
113
|
+
.chev.empty { visibility: hidden; }
|
|
114
|
+
.row-kind { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
|
|
115
|
+
border: 1px solid var(--line); border-radius: 4px; padding: 0 4px; }
|
|
116
|
+
#show-more { width: 100%; margin-top: 8px; }
|
|
117
|
+
|
|
118
|
+
/* ---------- detail panel ---------- */
|
|
119
|
+
#panel { position: fixed; top: 48px; right: 0; bottom: 0; width: 420px; overflow-y: auto;
|
|
120
|
+
background: var(--surface); border-left: 1px solid var(--line); padding: 18px 20px 40px;
|
|
121
|
+
transform: translateX(100%); transition: transform 250ms cubic-bezier(.2,.7,.2,1); z-index: 9; }
|
|
122
|
+
#panel.open { transform: translateX(0); }
|
|
123
|
+
#pclose { position: absolute; top: 12px; right: 12px; width: 26px; height: 26px;
|
|
124
|
+
border-radius: 50%; padding: 0; color: var(--muted); }
|
|
125
|
+
#panel h2 { font-size: 18px; margin: 0 30px 4px 0; line-height: 1.3; font-weight: 650; }
|
|
126
|
+
#pkicker { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
|
|
127
|
+
margin-bottom: 8px; }
|
|
128
|
+
.pactions { display: flex; gap: 6px; margin: 8px 0 12px; }
|
|
129
|
+
.ptabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: 12px; }
|
|
130
|
+
.ptabs button { border: none; background: none; padding: 5px 10px; font-size: 12px; font-weight: 600;
|
|
131
|
+
color: var(--muted); border-bottom: 2px solid transparent; border-radius: 0; }
|
|
132
|
+
.ptabs button.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
133
|
+
#pcontent { font-size: 14px; line-height: 1.6; }
|
|
134
|
+
#pcontent h1, #pcontent h2, #pcontent h3, #pcontent h4 { margin: .9em 0 .35em; line-height: 1.3; }
|
|
135
|
+
#pcontent p { margin: .5em 0; }
|
|
136
|
+
#pcontent ul { margin: .4em 0; padding-left: 22px; }
|
|
137
|
+
#pcontent li { margin: .15em 0; }
|
|
138
|
+
#pcontent code { background: var(--raised); border: 1px solid var(--line); border-radius: 4px;
|
|
139
|
+
padding: 1px 4px; font-size: 12px; }
|
|
140
|
+
#pcontent pre { background: var(--raised); border: 1px solid var(--line); border-radius: 8px;
|
|
141
|
+
padding: 10px 12px; overflow-x: auto; }
|
|
142
|
+
#pcontent pre code { background: none; border: none; padding: 0; }
|
|
143
|
+
#pcontent blockquote { border-left: 3px solid var(--line); margin: .6em 0; padding-left: 12px;
|
|
144
|
+
color: var(--muted); }
|
|
145
|
+
#pcontent a { color: var(--accent); }
|
|
146
|
+
.wikilink { color: var(--accent); border-bottom: 1px dashed var(--accent); cursor: pointer; }
|
|
147
|
+
.chip { display: inline-block; background: var(--raised); color: var(--accent); border-radius: 999px;
|
|
148
|
+
padding: 1px 9px; font-size: 11px; margin: 0 5px 5px 0; }
|
|
149
|
+
#pmeta { border-top: 1px solid var(--line); margin: 4px 0 14px; padding-top: 10px; font-size: 12px; }
|
|
150
|
+
#pmeta .k { color: var(--muted); display: inline-block; min-width: 88px; }
|
|
151
|
+
#pbody { border-top: 1px solid var(--line); padding-top: 12px; }
|
|
152
|
+
.link-row { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 6px;
|
|
153
|
+
cursor: pointer; }
|
|
154
|
+
.link-row:hover { background: var(--raised); }
|
|
155
|
+
.prov { font-size: 11px; border-radius: 999px; padding: 1px 8px; margin-left: 7px; }
|
|
156
|
+
.prov.human { background: var(--ok); color: #0b1020; }
|
|
157
|
+
.prov.agent { background: var(--accent); color: #0b1020; }
|
|
158
|
+
.prov.generated { background: var(--faint); color: #0b1020; }
|
|
159
|
+
|
|
160
|
+
/* ---------- legend ---------- */
|
|
161
|
+
#legend { position: fixed; left: 10px; bottom: 10px; background: var(--surface);
|
|
162
|
+
border: 1px solid var(--line); border-radius: 9px; padding: 7px 10px; z-index: 8;
|
|
163
|
+
font-size: 11px; color: var(--muted); }
|
|
164
|
+
#legend .legend-head { display: flex; align-items: center; gap: 6px; cursor: pointer; background: none;
|
|
165
|
+
border: none; color: var(--muted); font: inherit; font-size: 11px; padding: 0; width: 100%; text-align: left; }
|
|
166
|
+
#legend .legend-head .caret { transition: transform 140ms; }
|
|
167
|
+
#legend.collapsed .legend-head .caret { transform: rotate(-90deg); }
|
|
168
|
+
#legend .legend-body { margin-top: 6px; }
|
|
169
|
+
#legend.collapsed .legend-body { display: none; }
|
|
170
|
+
#legend .row { display: flex; align-items: center; gap: 7px; margin: 2px 0; white-space: nowrap; }
|
|
171
|
+
#legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
|
172
|
+
#legend .ring { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
|
|
173
|
+
#legend .ring.human { border: 2px solid var(--ok); }
|
|
174
|
+
#legend .ring.agent { border: 2px dashed var(--accent); }
|
|
175
|
+
#legend .ring.generated { border: 2px dotted var(--faint); }
|
|
176
|
+
|
|
177
|
+
/* ---------- overlays ---------- */
|
|
178
|
+
#overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
179
|
+
background: var(--bg); z-index: 20; }
|
|
180
|
+
#overlay.hidden { display: none; }
|
|
181
|
+
#overlay .card { text-align: center; color: var(--muted); }
|
|
182
|
+
#overlay .spinner { width: 26px; height: 26px; border: 3px solid var(--line);
|
|
183
|
+
border-top-color: var(--accent); border-radius: 50%; margin: 0 auto 12px;
|
|
184
|
+
animation: spin 1s linear infinite; }
|
|
185
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
186
|
+
#help { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
187
|
+
background: rgba(0,0,0,.5); z-index: 30; }
|
|
188
|
+
#help.hidden { display: none; }
|
|
189
|
+
#help .card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
|
|
190
|
+
padding: 20px 24px; min-width: 320px; }
|
|
191
|
+
#help h2 { margin: 0 0 12px; font-size: 16px; }
|
|
192
|
+
#help table { border-collapse: collapse; width: 100%; }
|
|
193
|
+
#help td { padding: 4px 8px; font-size: 12px; }
|
|
194
|
+
#help td:first-child { color: var(--accent); font-weight: 600; white-space: nowrap; }
|
|
195
|
+
#help .close { margin-top: 12px; width: 100%; }
|
|
196
|
+
#toast { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
|
|
197
|
+
background: var(--raised); border: 1px solid var(--line); color: var(--text);
|
|
198
|
+
padding: 8px 14px; border-radius: 8px; z-index: 40; opacity: 0; transition: opacity 200ms; }
|
|
199
|
+
#toast.show { opacity: 1; }
|
|
200
|
+
|
|
201
|
+
@media (prefers-reduced-motion: reduce) {
|
|
202
|
+
* { animation: none !important; transition: none !important; }
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
205
|
+
</head>
|
|
206
|
+
<body class="list-open">
|
|
207
|
+
<header>
|
|
208
|
+
<h1>pi-weave <span>knowledge view</span></h1>
|
|
209
|
+
<nav class="tabs" aria-label="surface">
|
|
210
|
+
<button data-surface="graph" class="tab active">Explore</button>
|
|
211
|
+
<button data-surface="health" class="tab">Health</button>
|
|
212
|
+
<button id="list-toggle" title="Toggle list sidebar" aria-pressed="true">▤</button>
|
|
213
|
+
</nav>
|
|
214
|
+
<input id="search" placeholder="search…" aria-label="search">
|
|
215
|
+
<span class="zoomgrp">
|
|
216
|
+
<button id="zoom-in" title="Zoom in">+</button><button id="zoom-out" title="Zoom out">−</button><button id="zoom-reset" title="Reset view">⌂</button>
|
|
217
|
+
</span>
|
|
218
|
+
<button id="theme" title="Toggle theme">◐</button>
|
|
219
|
+
<button id="help-btn" title="Shortcuts (?)">?</button>
|
|
220
|
+
<span id="status"></span>
|
|
221
|
+
<span id="stamp"></span>
|
|
222
|
+
</header>
|
|
223
|
+
<div id="overlay"><div class="card"><div class="spinner"></div>loading knowledge graph…</div></div>
|
|
224
|
+
<svg id="graph"></svg>
|
|
225
|
+
<aside id="list" class="surface">
|
|
226
|
+
<div class="list-toolbar">
|
|
227
|
+
<select id="sort" aria-label="sort">
|
|
228
|
+
<option value="name">name</option>
|
|
229
|
+
<option value="updated">updated</option>
|
|
230
|
+
<option value="links">links</option>
|
|
231
|
+
<option value="provenance">provenance</option>
|
|
232
|
+
</select>
|
|
233
|
+
<select id="kind-filter" aria-label="kind"><option value="">all kinds</option></select>
|
|
234
|
+
<select id="prov-filter" aria-label="provenance">
|
|
235
|
+
<option value="">all provenance</option>
|
|
236
|
+
<option value="human">human</option>
|
|
237
|
+
<option value="agent">agent</option>
|
|
238
|
+
<option value="generated">generated</option>
|
|
239
|
+
</select>
|
|
240
|
+
<select id="recent-filter" aria-label="recency">
|
|
241
|
+
<option value="">any time</option>
|
|
242
|
+
<option value="7">last 7 days</option>
|
|
243
|
+
<option value="30">last 30 days</option>
|
|
244
|
+
<option value="90">last 90 days</option>
|
|
245
|
+
</select>
|
|
246
|
+
<label class="internals"><input id="internals" type="checkbox" aria-label="show internals"> show internals</label>
|
|
247
|
+
</div>
|
|
248
|
+
<div id="list-rows"></div>
|
|
249
|
+
<button id="show-more" style="display:none">Show more</button>
|
|
250
|
+
</aside>
|
|
251
|
+
<aside id="panel">
|
|
252
|
+
<button id="pclose" title="Close (Esc)">✕</button>
|
|
253
|
+
<h2 id="ptitle"></h2>
|
|
254
|
+
<div id="pkicker"></div>
|
|
255
|
+
<div class="pactions">
|
|
256
|
+
<button id="pfocus">Focus</button>
|
|
257
|
+
<button id="popen">Open in editor</button>
|
|
258
|
+
</div>
|
|
259
|
+
<nav class="ptabs">
|
|
260
|
+
<button data-ptab="overview" class="active">Overview</button>
|
|
261
|
+
<button data-ptab="links">Links</button>
|
|
262
|
+
<button data-ptab="backlinks">Backlinks</button>
|
|
263
|
+
</nav>
|
|
264
|
+
<div id="pcontent"></div>
|
|
265
|
+
</aside>
|
|
266
|
+
<section id="health" class="surface">
|
|
267
|
+
<h2>Health</h2>
|
|
268
|
+
<div id="health-content"></div>
|
|
269
|
+
</section>
|
|
270
|
+
<div id="legend" class="collapsed"></div>
|
|
271
|
+
<div id="help" class="hidden">
|
|
272
|
+
<div class="card">
|
|
273
|
+
<h2>Shortcuts</h2>
|
|
274
|
+
<table>
|
|
275
|
+
<tr><td>1 / 3</td><td>Graph / Health</td></tr>
|
|
276
|
+
<tr><td>2</td><td>Toggle list sidebar</td></tr>
|
|
277
|
+
<tr><td>f</td><td>Focus selected node</td></tr>
|
|
278
|
+
<tr><td>g</td><td>Exit focus</td></tr>
|
|
279
|
+
<tr><td>▸ / ▾</td><td>Expand / collapse in List</td></tr>
|
|
280
|
+
<tr><td>/</td><td>Focus search</td></tr>
|
|
281
|
+
<tr><td>?</td><td>This help</td></tr>
|
|
282
|
+
<tr><td>+ / −</td><td>Zoom in / out</td></tr>
|
|
283
|
+
<tr><td>Esc</td><td>Close / clear / exit</td></tr>
|
|
284
|
+
<tr><td>Enter</td><td>Activate node / row</td></tr>
|
|
285
|
+
</table>
|
|
286
|
+
<button class="close" id="help-close">Close</button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<div id="toast"></div>
|
|
290
|
+
<script>
|
|
291
|
+
(function () {
|
|
292
|
+
"use strict";
|
|
293
|
+
|
|
294
|
+
// ===== pure =====
|
|
295
|
+
function focusNeighborhood(id, edges) {
|
|
296
|
+
var out = {}; out[id] = 1;
|
|
297
|
+
edges.forEach(function (e) {
|
|
298
|
+
if (e.source === id) out[e.target] = 1;
|
|
299
|
+
if (e.target === id) out[e.source] = 1;
|
|
300
|
+
});
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
function deriveBacklinks(edges) {
|
|
304
|
+
var out = {};
|
|
305
|
+
edges.forEach(function (e) {
|
|
306
|
+
if (e.kind !== "links-to") return;
|
|
307
|
+
(out[e.target] = out[e.target] || []).push(e.source);
|
|
308
|
+
});
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
function applyFilter(items, kind, prov) {
|
|
312
|
+
return items.filter(function (n) {
|
|
313
|
+
if (kind && n.kind !== kind) return false;
|
|
314
|
+
if (prov && n.provenance !== prov) return false;
|
|
315
|
+
return true;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function sortRows(rows, key) {
|
|
319
|
+
var arr = rows.slice();
|
|
320
|
+
arr.sort(function (a, b) {
|
|
321
|
+
if (key === "name") return a.label.localeCompare(b.label);
|
|
322
|
+
if (key === "updated") return (b.updated || "").localeCompare(a.updated || "");
|
|
323
|
+
if (key === "links") return b.links - a.links;
|
|
324
|
+
if (key === "provenance") return (a.provenance || "").localeCompare(b.provenance || "");
|
|
325
|
+
return 0;
|
|
326
|
+
});
|
|
327
|
+
return arr;
|
|
328
|
+
}
|
|
329
|
+
function counts(nodes) {
|
|
330
|
+
var out = { total: nodes.length, human: 0, agent: 0, generated: 0, structural: 0 };
|
|
331
|
+
nodes.forEach(function (n) {
|
|
332
|
+
if (n.provenance === "human") out.human++;
|
|
333
|
+
else if (n.provenance === "agent") out.agent++;
|
|
334
|
+
else if (n.provenance === "generated") out.generated++;
|
|
335
|
+
else out.structural++;
|
|
336
|
+
});
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
function relTime(iso, now) {
|
|
340
|
+
if (!iso) return "";
|
|
341
|
+
var t = new Date(iso).getTime();
|
|
342
|
+
if (isNaN(t)) return "";
|
|
343
|
+
var s = Math.max(0, Math.floor((now - t) / 1000));
|
|
344
|
+
if (s < 60) return "just now";
|
|
345
|
+
var m = Math.floor(s / 60);
|
|
346
|
+
if (m < 60) return m + "m ago";
|
|
347
|
+
var h = Math.floor(m / 60);
|
|
348
|
+
if (h < 24) return h + "h ago";
|
|
349
|
+
var d = Math.floor(h / 24);
|
|
350
|
+
if (d < 30) return d + "d ago";
|
|
351
|
+
var mo = Math.floor(d / 30);
|
|
352
|
+
if (mo < 12) return mo + "mo ago";
|
|
353
|
+
return Math.floor(mo / 12) + "y ago";
|
|
354
|
+
}
|
|
355
|
+
function linksOf(id, edges) {
|
|
356
|
+
var d = 0;
|
|
357
|
+
edges.forEach(function (e) { if (e.source === id || e.target === id) d++; });
|
|
358
|
+
return d;
|
|
359
|
+
}
|
|
360
|
+
// Build the List surface as an expandable index tree over the contains
|
|
361
|
+
// hierarchy. Entry points (files) are nested under the module whose path is
|
|
362
|
+
// their directory prefix, so the repository reads as a real file tree.
|
|
363
|
+
// Returns rows [{id, depth, hasKids, expanded}] in display order.
|
|
364
|
+
function listTree(model, state) {
|
|
365
|
+
var byId = {};
|
|
366
|
+
model.nodes.forEach(function (n) { byId[n.id] = n; });
|
|
367
|
+
var contains = {}; // strict contains (used for file/module placement)
|
|
368
|
+
var tree = {}; // contains + anchored-at (nesting hierarchy)
|
|
369
|
+
var incoming = {};
|
|
370
|
+
model.edges.forEach(function (e) {
|
|
371
|
+
if (e.kind === "contains") {
|
|
372
|
+
(contains[e.source] = contains[e.source] || []).push(e.target);
|
|
373
|
+
}
|
|
374
|
+
// Nest under a parent via contains OR anchored-at (the git anchor
|
|
375
|
+
// belongs under its repository); only nodes with no parent become roots.
|
|
376
|
+
if (e.kind === "contains" || e.kind === "anchored-at") {
|
|
377
|
+
(tree[e.source] = tree[e.source] || []).push(e.target);
|
|
378
|
+
incoming[e.target] = 1;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
function moduleFor(entryId) {
|
|
382
|
+
var entry = byId[entryId];
|
|
383
|
+
if (!entry || entry.kind !== "entryPoint" || !entry.detail.path) return null;
|
|
384
|
+
var p = entry.detail.path, best = null, bestLen = 0;
|
|
385
|
+
(contains["repository"] || []).forEach(function (cid) {
|
|
386
|
+
var c = byId[cid];
|
|
387
|
+
if (c && c.kind === "module" && c.detail.path) {
|
|
388
|
+
var mp = c.detail.path;
|
|
389
|
+
if (p.indexOf(mp + "/") === 0 && mp.length > bestLen) { best = cid; bestLen = mp.length; }
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
return best;
|
|
393
|
+
}
|
|
394
|
+
var moduleEntries = {};
|
|
395
|
+
model.nodes.forEach(function (n) {
|
|
396
|
+
if (n.kind !== "entryPoint") return;
|
|
397
|
+
var m = moduleFor(n.id);
|
|
398
|
+
if (m) (moduleEntries[m] = moduleEntries[m] || []).push(n.id);
|
|
399
|
+
});
|
|
400
|
+
function children(id) {
|
|
401
|
+
var kids = (tree[id] || []).filter(function (kid) {
|
|
402
|
+
return !(byId[kid] && byId[kid].kind === "entryPoint" && moduleFor(kid));
|
|
403
|
+
});
|
|
404
|
+
kids = kids.concat(moduleEntries[id] || []);
|
|
405
|
+
// Knowledge-first default: hide repo plumbing (git anchor, remotes,
|
|
406
|
+
// packages, entry points) unless the user explicitly reveals internals.
|
|
407
|
+
if (!state.showInternals) {
|
|
408
|
+
kids = kids.filter(function (k) {
|
|
409
|
+
var kind = byId[k] && byId[k].kind;
|
|
410
|
+
return kind !== "gitState" && kind !== "external" && kind !== "package" && kind !== "entryPoint";
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return kids;
|
|
414
|
+
}
|
|
415
|
+
var roots = model.nodes.filter(function (n) { return !incoming[n.id]; }).map(function (n) { return n.id; });
|
|
416
|
+
var filtering = state.kindFilter || state.provFilter || state.recentDays || state.query;
|
|
417
|
+
var rows = [];
|
|
418
|
+
function sortKids(kids) {
|
|
419
|
+
var arr = kids.map(function (k) {
|
|
420
|
+
var n = byId[k];
|
|
421
|
+
return { id: k, label: n.label, kind: n.kind, provenance: n.provenance || "",
|
|
422
|
+
updated: n.detail.updated || "", links: linksOf(k, model.edges) };
|
|
423
|
+
});
|
|
424
|
+
return sortRows(arr, state.listSort).map(function (r) { return r.id; });
|
|
425
|
+
}
|
|
426
|
+
function matches(node) {
|
|
427
|
+
if (state.recentDays) {
|
|
428
|
+
var u = node.detail.updated;
|
|
429
|
+
if (!u || new Date(u).getTime() < Date.now() - state.recentDays * 86400000) return false;
|
|
430
|
+
}
|
|
431
|
+
if (state.query && node.label.toLowerCase().indexOf(state.query) < 0) return false;
|
|
432
|
+
return applyFilter([node], state.kindFilter || null, state.provFilter || null).length > 0;
|
|
433
|
+
}
|
|
434
|
+
// Pass 1: mark which nodes are visible (self-match or a visible descendant).
|
|
435
|
+
var visibleSet = {};
|
|
436
|
+
function mark(id) {
|
|
437
|
+
var node = byId[id];
|
|
438
|
+
if (!node) return false;
|
|
439
|
+
var kids = sortKids(children(id));
|
|
440
|
+
var any = false;
|
|
441
|
+
kids.forEach(function (k) { if (mark(k)) any = true; });
|
|
442
|
+
var show = matches(node) || any;
|
|
443
|
+
if (show) visibleSet[id] = 1;
|
|
444
|
+
return show;
|
|
445
|
+
}
|
|
446
|
+
roots.forEach(function (r) { mark(r); });
|
|
447
|
+
// Pass 2: pre-order walk so parents precede their children.
|
|
448
|
+
function walk(id, depth) {
|
|
449
|
+
if (!visibleSet[id]) return;
|
|
450
|
+
var node = byId[id];
|
|
451
|
+
var kids = sortKids(children(id));
|
|
452
|
+
var visibleKids = kids.filter(function (k) { return visibleSet[k]; });
|
|
453
|
+
var expanded = filtering ? visibleKids.length > 0 : !!state.listExpanded[id];
|
|
454
|
+
rows.push({ id: id, depth: depth, hasKids: kids.length > 0, expanded: expanded });
|
|
455
|
+
if (expanded) visibleKids.forEach(function (k) { walk(k, depth + 1); });
|
|
456
|
+
}
|
|
457
|
+
roots.forEach(function (r) { walk(r, 0); });
|
|
458
|
+
return rows;
|
|
459
|
+
}
|
|
460
|
+
// Disambiguate labels that would otherwise read as the same entry twice
|
|
461
|
+
// (a remote URL whose tail matches the repo name, an npm package named
|
|
462
|
+
// after the repo). The raw node label still drives sorting and search.
|
|
463
|
+
function listLabel(n) {
|
|
464
|
+
if (n.kind === "external" && n.detail.url) {
|
|
465
|
+
var u = n.detail.url.replace(/^[a-z]+:\\/\\//, "").replace(/^[^@\\/]+@/, "").replace(/\\.git$/, "").replace(/\\/+$/, "");
|
|
466
|
+
if (u) return u;
|
|
467
|
+
}
|
|
468
|
+
if (n.kind === "package" && n.detail.manifest) {
|
|
469
|
+
return n.label + " (" + n.detail.manifest + ")";
|
|
470
|
+
}
|
|
471
|
+
return n.label;
|
|
472
|
+
}
|
|
473
|
+
// Split YAML front matter (--- … ---) from a markdown body, returning the
|
|
474
|
+
// stripped body plus the fields (and tags, when present). Pure/derivable.
|
|
475
|
+
function parseFrontMatter(text) {
|
|
476
|
+
if (typeof text !== "string" || text.slice(0, 3) !== "---") return { body: text || "", meta: [], tags: [] };
|
|
477
|
+
var m = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/.exec(text);
|
|
478
|
+
if (!m) return { body: text, meta: [], tags: [] };
|
|
479
|
+
var meta = [], tags = [];
|
|
480
|
+
m[1].split(/\\r?\\n/).forEach(function (line) {
|
|
481
|
+
var i = line.indexOf(":");
|
|
482
|
+
if (i <= 0) return;
|
|
483
|
+
var k = line.slice(0, i).trim();
|
|
484
|
+
var v = line.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
485
|
+
if (k === "tags") { tags = v.split(",").map(function (t) { return t.trim(); }).filter(Boolean); return; }
|
|
486
|
+
meta.push([k, v]);
|
|
487
|
+
});
|
|
488
|
+
return { body: text.slice(m[0].length), meta: meta, tags: tags };
|
|
489
|
+
}
|
|
490
|
+
// ===== end pure =====
|
|
491
|
+
|
|
492
|
+
var COLORS = { vault: "#8b5cf6", note: "#c4b5fd", repository: "#3b82f6",
|
|
493
|
+
module: "#22c55e", "package": "#14b8a6", entryPoint: "#a3e635",
|
|
494
|
+
gitState: "#facc15", external: "#fb923c", file: "#6ee7b7" };
|
|
495
|
+
var PROV_COLOR = { human: "#a7f3d0", agent: "#e9d5ff", generated: "#94a3b8" };
|
|
496
|
+
var PROV_GLYPH = { human: "●", agent: "◐", generated: "○" };
|
|
497
|
+
var EDGE_COLORS = { contains: "#2e3a55", "anchored-at": "#a16207", "links-to": "#7c3aed", mentions: "#525252" };
|
|
498
|
+
var EDGE_DASH = { "links-to": "4 3", "mentions": "2 3" };
|
|
499
|
+
var REST = { contains: 105, "anchored-at": 130, "links-to": 160, mentions: 160 };
|
|
500
|
+
var svgNS = "http://www.w3.org/2000/svg";
|
|
501
|
+
var svg = document.getElementById("graph");
|
|
502
|
+
var panel = document.getElementById("panel");
|
|
503
|
+
var searchEl = document.getElementById("search");
|
|
504
|
+
var model = null, lastJson = "";
|
|
505
|
+
var surface = "graph", selectedId = null, focusId = null, focusSet = null;
|
|
506
|
+
var query = "", kindFilter = "", provFilter = "", recentDays = 0, listSort = "name";
|
|
507
|
+
var listLimit = 100;
|
|
508
|
+
var listExpanded = { vault: 1, repository: 1 };
|
|
509
|
+
var showInternals = false;
|
|
510
|
+
var sim = {}, collapsed = {}, alpha = 0;
|
|
511
|
+
var W = window.innerWidth, H = window.innerHeight, world = null;
|
|
512
|
+
var cam = { x: 0, y: 0, k: 1 };
|
|
513
|
+
var panning = null, helpOpen = false;
|
|
514
|
+
|
|
515
|
+
function resize() {
|
|
516
|
+
W = window.innerWidth; H = window.innerHeight;
|
|
517
|
+
svg.setAttribute("width", W); svg.setAttribute("height", H);
|
|
518
|
+
}
|
|
519
|
+
window.addEventListener("resize", resize);
|
|
520
|
+
resize();
|
|
521
|
+
|
|
522
|
+
function el(name, attrs) {
|
|
523
|
+
var n = document.createElementNS(svgNS, name);
|
|
524
|
+
for (var k in attrs) n.setAttribute(k, attrs[k]);
|
|
525
|
+
return n;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ===== markdown =====
|
|
529
|
+
function esc(s) { return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
|
530
|
+
function capLabel(s) { return s.length > 30 ? s.slice(0, 29) + "…" : s; }
|
|
531
|
+
function mdInline(t) {
|
|
532
|
+
t = t.replace(/\\[\\[([^\\[\\]|]+)(?:\\|([^\\]\\[]*))?\\]\\]/g, function (m, slug, al) {
|
|
533
|
+
return "<a class='wikilink' href='#' data-slug='" + esc(slug) + "'>" + esc(al || slug) + "</a>";
|
|
534
|
+
});
|
|
535
|
+
t = t.replace(/\\x60([^\\x60]+)\\x60/g, "<code>$1</code>");
|
|
536
|
+
t = t.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>");
|
|
537
|
+
t = t.replace(/(^|[^*])\\*([^*\\s][^*]*)\\*/g, "$1<em>$2</em>");
|
|
538
|
+
t = t.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, function (m, txt, url) {
|
|
539
|
+
if (!/^(https?:\\/\\/|mailto:|#|\\/)/.test(url)) return txt;
|
|
540
|
+
return "<a href=\\"" + url + "\\" target=\\"_blank\\" rel=\\"noopener\\">" + txt + "</a>";
|
|
541
|
+
});
|
|
542
|
+
return t;
|
|
543
|
+
}
|
|
544
|
+
function renderMd(rawSrc) {
|
|
545
|
+
var src = esc(rawSrc); // escape FIRST: everything we add afterwards is our own markup
|
|
546
|
+
var lines = src.replace(/\\r/g, "").split("\\n"), out = "", inCode = false, list = false, para = [];
|
|
547
|
+
function flushPara() {
|
|
548
|
+
if (para.length) { out += "<p>" + mdInline(para.join(" ")) + "</p>"; para = []; }
|
|
549
|
+
}
|
|
550
|
+
function flushList() { if (list) { out += "</ul>"; list = false; } }
|
|
551
|
+
for (var i = 0; i < lines.length; i++) {
|
|
552
|
+
var line = lines[i], m;
|
|
553
|
+
if (/^\\s*\\x60\\x60\\x60/.test(line)) {
|
|
554
|
+
flushPara(); flushList();
|
|
555
|
+
out += inCode ? "</code></pre>" : "<pre><code>";
|
|
556
|
+
inCode = !inCode; continue;
|
|
557
|
+
}
|
|
558
|
+
if (inCode) { out += line + "\\n"; continue; }
|
|
559
|
+
if ((m = /^(#{1,4})\\s+(.*)$/.exec(line))) {
|
|
560
|
+
flushPara(); flushList();
|
|
561
|
+
out += "<h" + m[1].length + ">" + mdInline(m[2]) + "</h" + m[1].length + ">"; continue;
|
|
562
|
+
}
|
|
563
|
+
if ((m = /^\\s*[-*]\\s+(.*)$/.exec(line))) {
|
|
564
|
+
flushPara();
|
|
565
|
+
if (!list) { out += "<ul>"; list = true; }
|
|
566
|
+
out += "<li>" + mdInline(m[1]) + "</li>"; continue;
|
|
567
|
+
}
|
|
568
|
+
if ((m = /^>\\s?(.*)$/.exec(line))) {
|
|
569
|
+
flushPara(); flushList();
|
|
570
|
+
out += "<blockquote>" + mdInline(m[1]) + "</blockquote>"; continue;
|
|
571
|
+
}
|
|
572
|
+
flushList();
|
|
573
|
+
if (line.trim().length === 0) flushPara(); else para.push(line.trim());
|
|
574
|
+
}
|
|
575
|
+
flushPara(); flushList();
|
|
576
|
+
if (inCode) out += "</code></pre>";
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
// ===== end markdown =====
|
|
580
|
+
|
|
581
|
+
// ---------- graph helpers ----------
|
|
582
|
+
function degreeOf(id, edges) {
|
|
583
|
+
var d = 0;
|
|
584
|
+
edges.forEach(function (e) { if (e.source === id || e.target === id) d++; });
|
|
585
|
+
return d;
|
|
586
|
+
}
|
|
587
|
+
function radiusFor(node, edges) {
|
|
588
|
+
if (node.kind === "vault" || node.kind === "repository") return 14;
|
|
589
|
+
return 7 + Math.min(6, Math.sqrt(degreeOf(node.id, edges)) * 1.2);
|
|
590
|
+
}
|
|
591
|
+
function shapeEl(kind, r) {
|
|
592
|
+
var n;
|
|
593
|
+
if (kind === "repository" || kind === "module" || kind === "package") {
|
|
594
|
+
n = document.createElementNS(svgNS, "rect");
|
|
595
|
+
n.setAttribute("x", -r); n.setAttribute("y", -r);
|
|
596
|
+
n.setAttribute("width", r * 2); n.setAttribute("height", r * 2);
|
|
597
|
+
n.setAttribute("rx", r * 0.35);
|
|
598
|
+
} else if (kind === "entryPoint") {
|
|
599
|
+
n = document.createElementNS(svgNS, "polygon");
|
|
600
|
+
n.setAttribute("points", "0," + (-r) + " " + (r * 0.9) + "," + (r * 0.7) + " " + (-r * 0.9) + "," + (r * 0.7));
|
|
601
|
+
} else if (kind === "gitState") {
|
|
602
|
+
n = document.createElementNS(svgNS, "polygon");
|
|
603
|
+
n.setAttribute("points", "0," + (-r) + " " + (r * 0.8) + ",0 0," + r + " " + (-r * 0.8) + ",0");
|
|
604
|
+
} else if (kind === "external") {
|
|
605
|
+
n = document.createElementNS(svgNS, "polygon");
|
|
606
|
+
var pts = [];
|
|
607
|
+
for (var i = 0; i < 6; i++) {
|
|
608
|
+
var a = Math.PI / 6 + i * Math.PI / 3;
|
|
609
|
+
pts.push((Math.cos(a) * r).toFixed(1) + "," + (Math.sin(a) * r).toFixed(1));
|
|
610
|
+
}
|
|
611
|
+
n.setAttribute("points", pts.join(" "));
|
|
612
|
+
} else {
|
|
613
|
+
n = document.createElementNS(svgNS, "circle");
|
|
614
|
+
n.setAttribute("r", r);
|
|
615
|
+
}
|
|
616
|
+
n.setAttribute("class", "shape");
|
|
617
|
+
return n;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ---------- visibility (progressive disclosure) ----------
|
|
621
|
+
function childrenOf(id) {
|
|
622
|
+
var out = [];
|
|
623
|
+
model.edges.forEach(function (e) {
|
|
624
|
+
if (e.kind === "contains" && e.source === id) out.push(e.target);
|
|
625
|
+
});
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
function hiddenSet() {
|
|
629
|
+
var hidden = {};
|
|
630
|
+
function bury(id) {
|
|
631
|
+
childrenOf(id).forEach(function (c) {
|
|
632
|
+
if (!hidden[c]) { hidden[c] = 1; bury(c); }
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
Object.keys(collapsed).forEach(bury);
|
|
636
|
+
return hidden;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---------- force layout (calm: capped forces, warm-up before paint) ----------
|
|
640
|
+
function tick() {
|
|
641
|
+
var ids = Object.keys(sim).filter(function (id) { return sim[id].visible; });
|
|
642
|
+
var i, j, a, b, dx, dy, d2, d, f;
|
|
643
|
+
for (i = 0; i < ids.length; i++) {
|
|
644
|
+
a = sim[ids[i]];
|
|
645
|
+
for (j = i + 1; j < ids.length; j++) {
|
|
646
|
+
b = sim[ids[j]];
|
|
647
|
+
dx = a.x - b.x; dy = a.y - b.y;
|
|
648
|
+
d2 = dx * dx + dy * dy;
|
|
649
|
+
if (d2 > 67600) continue; // 260px repulsion cutoff
|
|
650
|
+
if (d2 < 400) { dx = (i - j) * 1.3 + 0.2; dy = 0.6; d2 = dx * dx + dy * dy; }
|
|
651
|
+
d = Math.sqrt(d2);
|
|
652
|
+
f = 380 * alpha / d2;
|
|
653
|
+
a.vx += dx * f / d; a.vy += dy * f / d;
|
|
654
|
+
b.vx -= dx * f / d; b.vy -= dy * f / d;
|
|
655
|
+
}
|
|
656
|
+
a.vx += (W / 2 - a.x) * 0.006 * alpha;
|
|
657
|
+
a.vy += (H / 2 - a.y) * 0.006 * alpha;
|
|
658
|
+
}
|
|
659
|
+
model.edges.forEach(function (e) {
|
|
660
|
+
var s = sim[e.source], t = sim[e.target];
|
|
661
|
+
if (!s || !t || !s.visible || !t.visible) return;
|
|
662
|
+
var ddx = t.x - s.x, ddy = t.y - s.y;
|
|
663
|
+
var dd = Math.sqrt(ddx * ddx + ddy * ddy) || 1;
|
|
664
|
+
var k = (dd - (REST[e.kind] || 120)) * 0.018 * alpha;
|
|
665
|
+
s.vx += ddx * k; s.vy += ddy * k;
|
|
666
|
+
t.vx -= ddx * k; t.vy -= ddy * k;
|
|
667
|
+
});
|
|
668
|
+
ids.forEach(function (id) {
|
|
669
|
+
var n = sim[id];
|
|
670
|
+
if (n.fixed) return;
|
|
671
|
+
var sp = Math.sqrt(n.vx * n.vx + n.vy * n.vy);
|
|
672
|
+
if (sp > 14) { n.vx = n.vx * 14 / sp; n.vy = n.vy * 14 / sp; } // speed cap
|
|
673
|
+
n.vx *= .82; n.vy *= .82;
|
|
674
|
+
n.x += n.vx; n.y += n.vy;
|
|
675
|
+
});
|
|
676
|
+
var touched = false;
|
|
677
|
+
for (i = 0; i < ids.length; i++) {
|
|
678
|
+
for (j = i + 1; j < ids.length; j++) {
|
|
679
|
+
a = sim[ids[i]]; b = sim[ids[j]];
|
|
680
|
+
dx = b.x - a.x; dy = b.y - a.y;
|
|
681
|
+
var gap2 = dx * dx + dy * dy;
|
|
682
|
+
if (gap2 > 484) continue; // 22px minimum separation
|
|
683
|
+
var gap = Math.sqrt(gap2) || 0.01;
|
|
684
|
+
var push = (22 - gap) / 2;
|
|
685
|
+
touched = true;
|
|
686
|
+
if (!a.fixed) { a.x -= dx / gap * push; a.y -= dy / gap * push; }
|
|
687
|
+
if (!b.fixed) { b.x += dx / gap * push; b.y += dy / gap * push; }
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
ids.forEach(function (id) {
|
|
691
|
+
var n = sim[id];
|
|
692
|
+
n.x = Math.max(20, Math.min(W - 20, n.x));
|
|
693
|
+
n.y = Math.max(60, Math.min(H - 20, n.y));
|
|
694
|
+
});
|
|
695
|
+
if (touched) alpha = Math.max(alpha, 0.008);
|
|
696
|
+
alpha *= 0.995;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
var edgeLines = [];
|
|
700
|
+
function paint() {
|
|
701
|
+
var hidden = hiddenSet();
|
|
702
|
+
Object.keys(sim).forEach(function (id) {
|
|
703
|
+
var n = sim[id];
|
|
704
|
+
n.visible = !hidden[id];
|
|
705
|
+
n.g.style.display = n.visible ? "" : "none";
|
|
706
|
+
if (n.visible) {
|
|
707
|
+
var dimmed = (query.length > 0 && n.node.label.toLowerCase().indexOf(query) < 0) ||
|
|
708
|
+
(focusSet && !focusSet[id]);
|
|
709
|
+
n.g.setAttribute("class", dimmed ? "node dim" : "node");
|
|
710
|
+
n.g.setAttribute("transform", "translate(" + n.x + "," + n.y + ")");
|
|
711
|
+
if (n.selRing) n.selRing.style.display = selectedId === id ? "" : "none";
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
edgeLines.forEach(function (rec) {
|
|
715
|
+
var s = sim[rec.e.source], t = sim[rec.e.target];
|
|
716
|
+
var vis = s && t && s.visible && t.visible;
|
|
717
|
+
rec.line.style.display = vis ? "" : "none";
|
|
718
|
+
if (vis) {
|
|
719
|
+
rec.line.setAttribute("x1", s.x); rec.line.setAttribute("y1", s.y);
|
|
720
|
+
rec.line.setAttribute("x2", t.x); rec.line.setAttribute("y2", t.y);
|
|
721
|
+
var inFocus = !focusSet || (focusSet[rec.e.source] && focusSet[rec.e.target]);
|
|
722
|
+
rec.line.setAttribute("opacity", inFocus ? "1" : "0.12");
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function loop() { if (alpha > 0.006) { tick(); paint(); } requestAnimationFrame(loop); }
|
|
728
|
+
|
|
729
|
+
// ---------- camera ----------
|
|
730
|
+
function camApply() {
|
|
731
|
+
if (world) world.setAttribute("transform", "translate(" + cam.x + "," + cam.y + ") scale(" + cam.k + ")");
|
|
732
|
+
}
|
|
733
|
+
function zoomAt(mx, my, dk) {
|
|
734
|
+
var nk = Math.min(4, Math.max(0.25, cam.k * dk));
|
|
735
|
+
cam.x = mx - (mx - cam.x) * (nk / cam.k);
|
|
736
|
+
cam.y = my - (my - cam.y) * (nk / cam.k);
|
|
737
|
+
cam.k = nk; camApply();
|
|
738
|
+
}
|
|
739
|
+
svg.addEventListener("wheel", function (ev) {
|
|
740
|
+
ev.preventDefault();
|
|
741
|
+
var rect = svg.getBoundingClientRect();
|
|
742
|
+
zoomAt(ev.clientX - rect.left, ev.clientY - rect.top, Math.exp(-ev.deltaY * 0.0012));
|
|
743
|
+
}, { passive: false });
|
|
744
|
+
svg.addEventListener("pointerdown", function (ev) {
|
|
745
|
+
if (ev.target !== svg) return;
|
|
746
|
+
panning = { px: ev.clientX, py: ev.clientY, cx: cam.x, cy: cam.y };
|
|
747
|
+
svg.setPointerCapture(ev.pointerId);
|
|
748
|
+
closePanel();
|
|
749
|
+
});
|
|
750
|
+
svg.addEventListener("pointermove", function (ev) {
|
|
751
|
+
if (!panning) return;
|
|
752
|
+
cam.x = panning.cx + (ev.clientX - panning.px);
|
|
753
|
+
cam.y = panning.cy + (ev.clientY - panning.py);
|
|
754
|
+
camApply();
|
|
755
|
+
});
|
|
756
|
+
svg.addEventListener("pointerup", function () { panning = null; });
|
|
757
|
+
document.getElementById("zoom-in").addEventListener("click", function () { zoomAt(W / 2, H / 2, 1.25); });
|
|
758
|
+
document.getElementById("zoom-out").addEventListener("click", function () { zoomAt(W / 2, H / 2, 0.8); });
|
|
759
|
+
document.getElementById("zoom-reset").addEventListener("click", function () {
|
|
760
|
+
cam = { x: 0, y: 0, k: 1 }; camApply();
|
|
761
|
+
});
|
|
762
|
+
function toWorld(ev) {
|
|
763
|
+
var rect = svg.getBoundingClientRect();
|
|
764
|
+
return { x: (ev.clientX - rect.left - cam.x) / cam.k, y: (ev.clientY - rect.top - cam.y) / cam.k };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ---------- surfaces ----------
|
|
768
|
+
// Graph and List are merged into one "Explore" surface: the graph canvas is
|
|
769
|
+
// the main view with the index tree as a collapsible left sidebar (there is
|
|
770
|
+
// no separate List tab). Health stays a distinct full surface.
|
|
771
|
+
var listOpen = true;
|
|
772
|
+
function showSurface(s) {
|
|
773
|
+
surface = s;
|
|
774
|
+
document.querySelectorAll(".tab").forEach(function (b) {
|
|
775
|
+
b.classList.toggle("active", b.getAttribute("data-surface") === s);
|
|
776
|
+
});
|
|
777
|
+
svg.style.display = s === "graph" ? "block" : "none";
|
|
778
|
+
document.body.classList.toggle("list-open", s === "graph" && listOpen);
|
|
779
|
+
document.getElementById("health").style.display = s === "health" ? "block" : "none";
|
|
780
|
+
if (s === "graph") renderList();
|
|
781
|
+
if (s === "health") renderHealth();
|
|
782
|
+
}
|
|
783
|
+
function toggleListPanel() {
|
|
784
|
+
listOpen = !listOpen;
|
|
785
|
+
document.body.classList.toggle("list-open", surface === "graph" && listOpen);
|
|
786
|
+
var t = document.getElementById("list-toggle");
|
|
787
|
+
t.setAttribute("aria-pressed", listOpen ? "true" : "false");
|
|
788
|
+
}
|
|
789
|
+
document.querySelectorAll(".tab").forEach(function (b) {
|
|
790
|
+
b.addEventListener("click", function () { showSurface(b.getAttribute("data-surface")); });
|
|
791
|
+
});
|
|
792
|
+
document.getElementById("list-toggle").addEventListener("click", toggleListPanel);
|
|
793
|
+
|
|
794
|
+
function provBar(c) {
|
|
795
|
+
var total = Math.max(1, c.human + c.agent + c.generated);
|
|
796
|
+
var hw = Math.round(c.human / total * 100);
|
|
797
|
+
var aw = Math.round(c.agent / total * 100);
|
|
798
|
+
var gw = Math.round(c.generated / total * 100);
|
|
799
|
+
return "<span class='provbar' title='human/agent/generated'>" +
|
|
800
|
+
"<i class='p-human' style='width:" + hw + "%'></i>" +
|
|
801
|
+
"<i class='p-agent' style='width:" + aw + "%'></i>" +
|
|
802
|
+
"<i class='p-generated' style='width:" + gw + "%'></i></span>";
|
|
803
|
+
}
|
|
804
|
+
function renderStatus() {
|
|
805
|
+
var el = document.getElementById("status");
|
|
806
|
+
if (!model) { el.innerHTML = ""; return; }
|
|
807
|
+
var c = counts(model.nodes);
|
|
808
|
+
var html = "<span class='stat'>" + c.total + " nodes</span>" + provBar(c);
|
|
809
|
+
if (model.staleness && model.staleness.state === "stale") {
|
|
810
|
+
html += "<span class='stat stale'>stale</span>";
|
|
811
|
+
}
|
|
812
|
+
el.innerHTML = html;
|
|
813
|
+
document.getElementById("stamp").textContent =
|
|
814
|
+
model.generatedAt ? "indexed " + relTime(model.generatedAt, Date.now()) : "";
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ---------- list (tree) ----------
|
|
818
|
+
function listById() {
|
|
819
|
+
var byId = {};
|
|
820
|
+
model.nodes.forEach(function (n) { byId[n.id] = n; });
|
|
821
|
+
return byId;
|
|
822
|
+
}
|
|
823
|
+
function toggleList(id) {
|
|
824
|
+
if (listExpanded[id]) delete listExpanded[id]; else listExpanded[id] = 1;
|
|
825
|
+
renderList();
|
|
826
|
+
}
|
|
827
|
+
function renderList() {
|
|
828
|
+
if (!model) return;
|
|
829
|
+
var container = document.getElementById("list-rows");
|
|
830
|
+
var byId = listById();
|
|
831
|
+
var rows = listTree(model, {
|
|
832
|
+
kindFilter: kindFilter, provFilter: provFilter, recentDays: recentDays,
|
|
833
|
+
query: query, listSort: listSort, listExpanded: listExpanded, showInternals: showInternals,
|
|
834
|
+
});
|
|
835
|
+
var shown = rows.slice(0, listLimit);
|
|
836
|
+
var html = "";
|
|
837
|
+
shown.forEach(function (r) {
|
|
838
|
+
var n = byId[r.id];
|
|
839
|
+
var chev = r.hasKids ? (r.expanded ? "▾" : "▸") : "";
|
|
840
|
+
html += "<div class='row" + (selectedId === r.id ? " selected" : "") + "' data-id='" + esc(r.id) + "' tabindex='0' role='button' style='padding-left:" + (8 + r.depth * 16) + "px'>" +
|
|
841
|
+
"<span class='chev" + (r.hasKids ? "" : " empty") + "' data-toggle='" + esc(r.id) + "'>" + chev + "</span>" +
|
|
842
|
+
"<span class='row-label' title='" + esc(listLabel(n)) + "'>" + esc(listLabel(n)) + "</span>" +
|
|
843
|
+
"<span class='row-kind'>" + esc(n.kind) + "</span>" +
|
|
844
|
+
(n.provenance ? "<span class='prov " + esc(n.provenance) + "'>" + esc(n.provenance) + "</span>" : "") +
|
|
845
|
+
"<span class='row-meta'>" + (n.detail.updated ? relTime(n.detail.updated, Date.now()) : "") + " · " + linksOf(n.id, model.edges) + "</span>" +
|
|
846
|
+
"</div>";
|
|
847
|
+
});
|
|
848
|
+
container.innerHTML = html;
|
|
849
|
+
document.getElementById("show-more").style.display = rows.length > listLimit ? "" : "none";
|
|
850
|
+
container.querySelectorAll(".row").forEach(function (row) {
|
|
851
|
+
row.addEventListener("click", function (ev) {
|
|
852
|
+
var t = ev.target;
|
|
853
|
+
if (t && t.getAttribute && t.getAttribute("data-toggle")) {
|
|
854
|
+
toggleList(t.getAttribute("data-toggle"));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
selectById(row.getAttribute("data-id"));
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
document.getElementById("show-more").addEventListener("click", function () {
|
|
862
|
+
listLimit += 100; renderList();
|
|
863
|
+
});
|
|
864
|
+
document.getElementById("sort").addEventListener("change", function (e) { listSort = e.target.value; renderList(); });
|
|
865
|
+
document.getElementById("kind-filter").addEventListener("change", function (e) { kindFilter = e.target.value; renderList(); });
|
|
866
|
+
document.getElementById("prov-filter").addEventListener("change", function (e) { provFilter = e.target.value; renderList(); });
|
|
867
|
+
document.getElementById("recent-filter").addEventListener("change", function (e) {
|
|
868
|
+
recentDays = Number(e.target.value) || 0; renderList();
|
|
869
|
+
});
|
|
870
|
+
document.getElementById("internals").addEventListener("change", function (e) {
|
|
871
|
+
showInternals = !!e.target.checked; renderList();
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// ---------- health ----------
|
|
875
|
+
function renderHealth() {
|
|
876
|
+
var content = document.getElementById("health-content");
|
|
877
|
+
if (!model) { content.innerHTML = ""; return; }
|
|
878
|
+
var backlinks = deriveBacklinks(model.edges);
|
|
879
|
+
var c = counts(model.nodes);
|
|
880
|
+
var orphans = model.nodes.filter(function (n) {
|
|
881
|
+
return n.kind === "note" && !(backlinks[n.id] && backlinks[n.id].length);
|
|
882
|
+
});
|
|
883
|
+
var dangling = model.nodes.filter(function (n) {
|
|
884
|
+
var d = n.detail["dangling links"];
|
|
885
|
+
return d && d !== "0" && d !== "";
|
|
886
|
+
});
|
|
887
|
+
var hubs = model.nodes.slice().sort(function (a, b) {
|
|
888
|
+
return degreeOf(b.id, model.edges) - degreeOf(a.id, model.edges);
|
|
889
|
+
}).slice(0, 10);
|
|
890
|
+
var html = "";
|
|
891
|
+
html += "<h3>Coverage</h3>" + provBar(c) +
|
|
892
|
+
"<p class='muted'>" + c.human + " human · " + c.agent + " agent · " + c.generated + " generated · " + c.structural + " structural</p>";
|
|
893
|
+
html += "<h3>Orphans (" + orphans.length + ")</h3>";
|
|
894
|
+
html += orphans.length ? orphans.slice(0, 20).map(function (n) {
|
|
895
|
+
return "<div class='link-row' data-id='" + esc(n.id) + "' tabindex='0' role='button'>" + esc(n.label) + "</div>";
|
|
896
|
+
}).join("") : "<p class='muted'>none — every note is linked</p>";
|
|
897
|
+
html += "<h3>Dangling links (" + dangling.length + ")</h3>";
|
|
898
|
+
html += dangling.length ? dangling.slice(0, 20).map(function (n) {
|
|
899
|
+
return "<div class='link-row' data-id='" + esc(n.id) + "' tabindex='0' role='button'>" + esc(n.label) +
|
|
900
|
+
" <span class='muted'>→ " + esc(n.detail["dangling links"]) + "</span></div>";
|
|
901
|
+
}).join("") : "<p class='muted'>none</p>";
|
|
902
|
+
html += "<h3>Hubs</h3>";
|
|
903
|
+
html += hubs.map(function (n) {
|
|
904
|
+
return "<div class='link-row' data-id='" + esc(n.id) + "' tabindex='0' role='button'>" + esc(n.label) +
|
|
905
|
+
" <span class='muted'>" + degreeOf(n.id, model.edges) + " links</span></div>";
|
|
906
|
+
}).join("");
|
|
907
|
+
html += "<h3>Export</h3><button id='export'>Export snapshot (JSON)</button>";
|
|
908
|
+
content.innerHTML = html;
|
|
909
|
+
content.querySelectorAll(".link-row").forEach(function (row) {
|
|
910
|
+
row.addEventListener("click", function () { selectById(row.getAttribute("data-id")); });
|
|
911
|
+
});
|
|
912
|
+
document.getElementById("export").addEventListener("click", exportSnapshot);
|
|
913
|
+
}
|
|
914
|
+
function exportSnapshot() {
|
|
915
|
+
var data = { generatedAt: model.generatedAt, staleness: model.staleness, nodes: model.nodes, edges: model.edges };
|
|
916
|
+
var blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
917
|
+
var url = URL.createObjectURL(blob);
|
|
918
|
+
var a = document.createElement("a");
|
|
919
|
+
a.href = url; a.download = "weave-snapshot.json";
|
|
920
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
921
|
+
URL.revokeObjectURL(url);
|
|
922
|
+
toast("snapshot exported");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ---------- detail ----------
|
|
926
|
+
function closePanel() { panel.className = ""; }
|
|
927
|
+
document.getElementById("pclose").addEventListener("click", closePanel);
|
|
928
|
+
function linkRow(id) {
|
|
929
|
+
var n = model.nodes.find(function (x) { return x.id === id; });
|
|
930
|
+
if (!n) return "<div class='link-row muted'>" + esc(id) + "</div>";
|
|
931
|
+
return "<div class='link-row' data-id='" + esc(id) + "' tabindex='0' role='button'>" +
|
|
932
|
+
esc(n.label) + "<span class='row-kind'>" + esc(n.kind) + "</span></div>";
|
|
933
|
+
}
|
|
934
|
+
function wireLinks(container) {
|
|
935
|
+
container.querySelectorAll(".link-row").forEach(function (row) {
|
|
936
|
+
row.addEventListener("click", function () { selectById(row.getAttribute("data-id")); });
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function wireWikilinks(container) {
|
|
940
|
+
container.querySelectorAll(".wikilink").forEach(function (a) {
|
|
941
|
+
a.addEventListener("click", function (ev) {
|
|
942
|
+
ev.preventDefault();
|
|
943
|
+
var slug = a.getAttribute("data-slug");
|
|
944
|
+
if (slug) openNote(slug);
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
function openNote(slug) {
|
|
949
|
+
var node = model.nodes.find(function (n) { return n.detail.slug === slug; });
|
|
950
|
+
if (node) selectById(node.id);
|
|
951
|
+
}
|
|
952
|
+
function renderBodyInto(node, container) {
|
|
953
|
+
if (node.kind === "note" && node.detail.slug) {
|
|
954
|
+
container.innerHTML = "<p class='muted'>loading note…</p>";
|
|
955
|
+
fetch("note/" + encodeURIComponent(node.detail.slug)).then(function (r) {
|
|
956
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
957
|
+
return r.json();
|
|
958
|
+
}).then(function (noteData) {
|
|
959
|
+
container.innerHTML = renderMd(noteData.body || "(empty note)");
|
|
960
|
+
wireWikilinks(container);
|
|
961
|
+
}).catch(function () {
|
|
962
|
+
container.innerHTML = "<p class='muted'>(could not load note body — it may have moved)</p>";
|
|
963
|
+
});
|
|
964
|
+
} else if (node.kind === "file" && node.detail.path) {
|
|
965
|
+
// A derived index file (.okf/…) — fetch its real body from the viewer server.
|
|
966
|
+
container.innerHTML = "<p class='muted'>loading file…</p>";
|
|
967
|
+
fetch("okffile/" + encodeURIComponent(node.detail.path)).then(function (r) {
|
|
968
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
969
|
+
return r.json();
|
|
970
|
+
}).then(function (fileData) {
|
|
971
|
+
var pf = parseFrontMatter(fileData.body || "");
|
|
972
|
+
renderOverviewMeta(pf.meta, pf.tags);
|
|
973
|
+
container.innerHTML = renderMd(pf.body || "(empty file)");
|
|
974
|
+
wireWikilinks(container);
|
|
975
|
+
}).catch(function () {
|
|
976
|
+
container.innerHTML = "<p class='muted'>(could not load file body)</p>";
|
|
977
|
+
});
|
|
978
|
+
} else {
|
|
979
|
+
var body = node.detail.summary ? renderMd(node.detail.summary)
|
|
980
|
+
: node.detail.preview ? renderMd(node.detail.preview) : "<p class='muted'>(no body)</p>";
|
|
981
|
+
container.innerHTML = body;
|
|
982
|
+
wireWikilinks(container);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// Fill the Overview meta table (#pmeta) and tag chips (#ptags) from a
|
|
986
|
+
// front-matter parse (used for both notes and derived .okf files).
|
|
987
|
+
function renderOverviewMeta(meta, tags) {
|
|
988
|
+
var pt = document.getElementById("ptags");
|
|
989
|
+
var pm = document.getElementById("pmeta");
|
|
990
|
+
if (pt) pt.innerHTML = tags.map(function (t) { return "<span class='chip'>" + esc(t) + "</span>"; }).join("");
|
|
991
|
+
if (pm) pm.innerHTML = meta.map(function (p) {
|
|
992
|
+
return "<div><span class='k'>" + esc(p[0]) + "</span>" + esc(p[1]) + "</div>";
|
|
993
|
+
}).join("");
|
|
994
|
+
}
|
|
995
|
+
function renderPtab(node, tab) {
|
|
996
|
+
var content = document.getElementById("pcontent");
|
|
997
|
+
if (tab === "overview") {
|
|
998
|
+
var meta = [];
|
|
999
|
+
if (node.detail.slug) meta.push(["slug", node.detail.slug]);
|
|
1000
|
+
if (node.detail.updated) meta.push(["updated", node.detail.updated]);
|
|
1001
|
+
if (node.detail["dangling links"]) meta.push(["dangling links", node.detail["dangling links"]]);
|
|
1002
|
+
var tags = (node.detail.tags || "").split(",").map(function (t) { return t.trim(); }).filter(Boolean);
|
|
1003
|
+
content.innerHTML = "<div id='ptags'></div><div id='pmeta'></div><div id='pbody'></div>";
|
|
1004
|
+
renderOverviewMeta(meta, tags);
|
|
1005
|
+
renderBodyInto(node, document.getElementById("pbody"));
|
|
1006
|
+
} else if (tab === "links") {
|
|
1007
|
+
var out = [];
|
|
1008
|
+
model.edges.forEach(function (e) { if (e.source === node.id) out.push(e.target); });
|
|
1009
|
+
content.innerHTML = out.length ? out.map(function (t) { return linkRow(t); }).join("")
|
|
1010
|
+
: "<p class='muted'>no outgoing links</p>";
|
|
1011
|
+
wireLinks(content);
|
|
1012
|
+
} else if (tab === "backlinks") {
|
|
1013
|
+
var bl = deriveBacklinks(model.edges)[node.id] || [];
|
|
1014
|
+
content.innerHTML = bl.length ? bl.map(function (s) { return linkRow(s); }).join("")
|
|
1015
|
+
: "<p class='muted'>no backlinks</p>";
|
|
1016
|
+
wireLinks(content);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function selectById(id) {
|
|
1020
|
+
selectedId = id;
|
|
1021
|
+
var node = model.nodes.find(function (n) { return n.id === id; });
|
|
1022
|
+
if (!node) return;
|
|
1023
|
+
panel.className = "open";
|
|
1024
|
+
document.getElementById("ptitle").innerHTML = esc(node.label) +
|
|
1025
|
+
(node.provenance ? "<span class='prov " + node.provenance + "'>" + node.provenance + "</span>" : "");
|
|
1026
|
+
document.getElementById("pkicker").textContent = node.kind;
|
|
1027
|
+
document.getElementById("pfocus").onclick = function () { focusOn(node.id); };
|
|
1028
|
+
var openBtn = document.getElementById("popen");
|
|
1029
|
+
if (node.kind === "note" && node.detail.slug) {
|
|
1030
|
+
openBtn.style.display = "";
|
|
1031
|
+
openBtn.onclick = function () {
|
|
1032
|
+
fetch("open/" + encodeURIComponent(node.detail.slug), { method: "POST" })
|
|
1033
|
+
.then(function (r) { if (r.ok) toast("opened in editor"); })
|
|
1034
|
+
.catch(function () { toast("could not open in editor"); });
|
|
1035
|
+
};
|
|
1036
|
+
} else {
|
|
1037
|
+
openBtn.style.display = "none";
|
|
1038
|
+
}
|
|
1039
|
+
document.querySelectorAll(".ptabs button").forEach(function (b) {
|
|
1040
|
+
b.classList.toggle("active", b.getAttribute("data-ptab") === "overview");
|
|
1041
|
+
});
|
|
1042
|
+
renderPtab(node, "overview");
|
|
1043
|
+
if (surface === "graph" && listOpen) renderList();
|
|
1044
|
+
paint();
|
|
1045
|
+
}
|
|
1046
|
+
document.querySelectorAll(".ptabs button").forEach(function (b) {
|
|
1047
|
+
b.addEventListener("click", function () {
|
|
1048
|
+
var node = model.nodes.find(function (n) { return n.id === selectedId; });
|
|
1049
|
+
if (!node) return;
|
|
1050
|
+
document.querySelectorAll(".ptabs button").forEach(function (x) {
|
|
1051
|
+
x.classList.toggle("active", x === b);
|
|
1052
|
+
});
|
|
1053
|
+
renderPtab(node, b.getAttribute("data-ptab"));
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// ---------- focus ----------
|
|
1058
|
+
function focusOn(id) {
|
|
1059
|
+
focusId = id;
|
|
1060
|
+
focusSet = focusNeighborhood(id, model.edges);
|
|
1061
|
+
var n = sim[id];
|
|
1062
|
+
if (n) {
|
|
1063
|
+
cam.k = Math.max(1.2, cam.k);
|
|
1064
|
+
cam.x = W / 2 - n.x * cam.k;
|
|
1065
|
+
cam.y = H / 2 - n.y * cam.k;
|
|
1066
|
+
camApply();
|
|
1067
|
+
}
|
|
1068
|
+
paint();
|
|
1069
|
+
}
|
|
1070
|
+
function exitFocus() {
|
|
1071
|
+
focusId = null; focusSet = null;
|
|
1072
|
+
paint();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ---------- scene ----------
|
|
1076
|
+
function buildScene(first) {
|
|
1077
|
+
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
1078
|
+
sim = {}; edgeLines = [];
|
|
1079
|
+
world = el("g", {});
|
|
1080
|
+
svg.appendChild(world);
|
|
1081
|
+
model.nodes.forEach(function (node, idx) {
|
|
1082
|
+
var angle = idx * 2.399963;
|
|
1083
|
+
sim[node.id] = { node: node, x: W / 2 + Math.cos(angle) * (24 + idx * 4), y: H / 2 + Math.sin(angle) * (24 + idx * 4),
|
|
1084
|
+
vx: 0, vy: 0, visible: true, fixed: false, g: null, selRing: null };
|
|
1085
|
+
});
|
|
1086
|
+
var edgeLayer = el("g", {}), nodeLayer = el("g", {});
|
|
1087
|
+
world.appendChild(edgeLayer); world.appendChild(nodeLayer);
|
|
1088
|
+
model.edges.forEach(function (e) {
|
|
1089
|
+
if (!sim[e.source] || !sim[e.target]) return;
|
|
1090
|
+
var line = el("line", { stroke: EDGE_COLORS[e.kind] || "#2e3a55", "stroke-width": "1.2" });
|
|
1091
|
+
if (EDGE_DASH[e.kind]) line.setAttribute("stroke-dasharray", EDGE_DASH[e.kind]);
|
|
1092
|
+
edgeLayer.appendChild(line);
|
|
1093
|
+
edgeLines.push({ e: e, line: line });
|
|
1094
|
+
});
|
|
1095
|
+
model.nodes.forEach(function (node) {
|
|
1096
|
+
var n = sim[node.id];
|
|
1097
|
+
var prov = node.provenance;
|
|
1098
|
+
var root = node.kind === "vault" || node.kind === "repository";
|
|
1099
|
+
var r = radiusFor(node, model.edges);
|
|
1100
|
+
var g = el("g", { "class": "node", tabindex: "0", role: "button" });
|
|
1101
|
+
if (prov) {
|
|
1102
|
+
var ring = el("circle", { r: r + 2.5, fill: "none", stroke: PROV_COLOR[prov], "stroke-width": "1.4" });
|
|
1103
|
+
if (prov === "agent") ring.setAttribute("stroke-dasharray", "3 2");
|
|
1104
|
+
if (prov === "generated") ring.setAttribute("stroke-dasharray", "1 2");
|
|
1105
|
+
g.appendChild(ring);
|
|
1106
|
+
}
|
|
1107
|
+
var halo = el("circle", { r: r + 6, fill: "none", stroke: "var(--accent)", "stroke-width": "1", "class": "halo", opacity: "0" });
|
|
1108
|
+
g.appendChild(halo);
|
|
1109
|
+
var shape = shapeEl(node.kind, r);
|
|
1110
|
+
shape.setAttribute("fill", COLORS[node.kind] || "#6b7280");
|
|
1111
|
+
shape.setAttribute("stroke", "#0b1020");
|
|
1112
|
+
shape.setAttribute("stroke-width", "1.2");
|
|
1113
|
+
if (node.id === "repository" && model.staleness && model.staleness.state === "stale") {
|
|
1114
|
+
shape.setAttribute("stroke", "#f59e0b"); shape.setAttribute("stroke-width", "3");
|
|
1115
|
+
}
|
|
1116
|
+
g.appendChild(shape);
|
|
1117
|
+
if (prov) {
|
|
1118
|
+
var glyph = el("text", { "class": "glyph", x: 0, y: 2.5 });
|
|
1119
|
+
glyph.textContent = PROV_GLYPH[prov];
|
|
1120
|
+
g.appendChild(glyph);
|
|
1121
|
+
}
|
|
1122
|
+
var selRing = el("circle", { r: r + 4, fill: "none", stroke: "var(--accent)", "stroke-width": "2", "class": "sel-ring" });
|
|
1123
|
+
selRing.style.display = "none";
|
|
1124
|
+
g.appendChild(selRing);
|
|
1125
|
+
n.selRing = selRing;
|
|
1126
|
+
var hasKids = childrenOf(node.id).length > 0;
|
|
1127
|
+
var label = el("text", { "class": "label", x: root ? r + 5 : r + 4, y: 4 });
|
|
1128
|
+
label.textContent = capLabel(node.label) + (hasKids ? (collapsed[node.id] ? " ▸" : " ▾") : "");
|
|
1129
|
+
if (node.id === "repository" && model.staleness && model.staleness.state === "stale") {
|
|
1130
|
+
label.textContent = "⚠ " + label.textContent;
|
|
1131
|
+
}
|
|
1132
|
+
g.appendChild(label);
|
|
1133
|
+
var moved = false;
|
|
1134
|
+
shape.addEventListener("pointerdown", function (ev) {
|
|
1135
|
+
n.fixed = true; moved = false; ev.stopPropagation(); shape.setPointerCapture(ev.pointerId);
|
|
1136
|
+
});
|
|
1137
|
+
shape.addEventListener("pointermove", function (ev) {
|
|
1138
|
+
if (!n.fixed) return;
|
|
1139
|
+
var p = toWorld(ev);
|
|
1140
|
+
n.x = p.x; n.y = p.y; n.vx = 0; n.vy = 0; moved = true;
|
|
1141
|
+
alpha = Math.max(alpha, .08); paint();
|
|
1142
|
+
});
|
|
1143
|
+
shape.addEventListener("pointerup", function () {
|
|
1144
|
+
n.fixed = false;
|
|
1145
|
+
if (!moved) {
|
|
1146
|
+
if (hasKids) {
|
|
1147
|
+
if (collapsed[node.id]) delete collapsed[node.id]; else collapsed[node.id] = 1;
|
|
1148
|
+
alpha = Math.max(alpha, .35);
|
|
1149
|
+
paint();
|
|
1150
|
+
}
|
|
1151
|
+
selectById(node.id);
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
shape.addEventListener("dblclick", function () { focusOn(node.id); });
|
|
1155
|
+
shape.addEventListener("pointerenter", function () {
|
|
1156
|
+
g.classList.add("hovered");
|
|
1157
|
+
edgeLines.forEach(function (rec) {
|
|
1158
|
+
if (rec.e.source === node.id || rec.e.target === node.id) {
|
|
1159
|
+
rec.line.setAttribute("stroke-width", "1.8");
|
|
1160
|
+
rec.line.setAttribute("opacity", "1");
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
shape.addEventListener("pointerleave", function () {
|
|
1165
|
+
g.classList.remove("hovered");
|
|
1166
|
+
paint();
|
|
1167
|
+
});
|
|
1168
|
+
g.addEventListener("keydown", function (ev) {
|
|
1169
|
+
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); selectById(node.id); }
|
|
1170
|
+
});
|
|
1171
|
+
nodeLayer.appendChild(g);
|
|
1172
|
+
n.g = g;
|
|
1173
|
+
});
|
|
1174
|
+
camApply();
|
|
1175
|
+
paint();
|
|
1176
|
+
alpha = .6;
|
|
1177
|
+
if (first) {
|
|
1178
|
+
for (var i = 0; i < 140; i++) {
|
|
1179
|
+
tick();
|
|
1180
|
+
if (i % 20 === 19) paint();
|
|
1181
|
+
}
|
|
1182
|
+
alpha = .06;
|
|
1183
|
+
paint();
|
|
1184
|
+
} else {
|
|
1185
|
+
alpha = .22;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ---------- data ----------
|
|
1190
|
+
function fetchGraph(first) {
|
|
1191
|
+
var overlay = document.getElementById("overlay");
|
|
1192
|
+
if (first) overlay.className = "";
|
|
1193
|
+
return fetch("graph.json", { cache: "no-store" }).then(function (r) {
|
|
1194
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
1195
|
+
return r.text();
|
|
1196
|
+
}).then(function (text) {
|
|
1197
|
+
if (text === lastJson) { overlay.className = "hidden"; return; }
|
|
1198
|
+
lastJson = text;
|
|
1199
|
+
model = JSON.parse(text);
|
|
1200
|
+
renderStatus();
|
|
1201
|
+
buildScene(first);
|
|
1202
|
+
if (surface === "graph" && listOpen) renderList();
|
|
1203
|
+
if (surface === "health") renderHealth();
|
|
1204
|
+
overlay.className = "hidden";
|
|
1205
|
+
if (!model.nodes.length) {
|
|
1206
|
+
overlay.className = "";
|
|
1207
|
+
overlay.innerHTML = "<div class='card'><p>no notes yet — write one in pi</p></div>";
|
|
1208
|
+
}
|
|
1209
|
+
}).catch(function () {
|
|
1210
|
+
overlay.className = "";
|
|
1211
|
+
overlay.innerHTML = "<div class='card'><p>could not load the graph</p>" +
|
|
1212
|
+
"<button id='retry'>Retry</button></div>";
|
|
1213
|
+
document.getElementById("retry").addEventListener("click", function () { fetchGraph(true); });
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// ---------- search ----------
|
|
1218
|
+
searchEl.addEventListener("input", function () {
|
|
1219
|
+
query = searchEl.value.trim().toLowerCase();
|
|
1220
|
+
if (surface === "graph" && listOpen) renderList();
|
|
1221
|
+
paint();
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// ---------- theme ----------
|
|
1225
|
+
var theme = "dark";
|
|
1226
|
+
try {
|
|
1227
|
+
theme = localStorage.getItem("weave-theme") ||
|
|
1228
|
+
(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
|
1229
|
+
} catch (e) { /* storage may be unavailable */ }
|
|
1230
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
1231
|
+
document.getElementById("theme").addEventListener("click", function () {
|
|
1232
|
+
theme = theme === "dark" ? "light" : "dark";
|
|
1233
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
1234
|
+
try { localStorage.setItem("weave-theme", theme); } catch (e) { /* ignore */ }
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// ---------- help ----------
|
|
1238
|
+
function toggleHelp() {
|
|
1239
|
+
helpOpen = !helpOpen;
|
|
1240
|
+
document.getElementById("help").classList.toggle("hidden", !helpOpen);
|
|
1241
|
+
}
|
|
1242
|
+
document.getElementById("help-btn").addEventListener("click", toggleHelp);
|
|
1243
|
+
document.getElementById("help-close").addEventListener("click", function () { helpOpen = false; document.getElementById("help").classList.add("hidden"); });
|
|
1244
|
+
|
|
1245
|
+
// ---------- toast ----------
|
|
1246
|
+
var toastTimer = null;
|
|
1247
|
+
function toast(msg) {
|
|
1248
|
+
var t = document.getElementById("toast");
|
|
1249
|
+
t.textContent = msg;
|
|
1250
|
+
t.classList.add("show");
|
|
1251
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
1252
|
+
toastTimer = setTimeout(function () { t.classList.remove("show"); }, 1800);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// ---------- keyboard ----------
|
|
1256
|
+
document.addEventListener("keydown", function (ev) {
|
|
1257
|
+
var k = ev.key;
|
|
1258
|
+
if (k === "1") showSurface("graph");
|
|
1259
|
+
else if (k === "2") toggleListPanel();
|
|
1260
|
+
else if (k === "3") showSurface("health");
|
|
1261
|
+
else if (k === "f") { if (selectedId) focusOn(selectedId); }
|
|
1262
|
+
else if (k === "g") exitFocus();
|
|
1263
|
+
else if (k === "/") { ev.preventDefault(); searchEl.focus(); }
|
|
1264
|
+
else if (k === "?") toggleHelp();
|
|
1265
|
+
else if (k === "=" || k === "+") zoomAt(W / 2, H / 2, 1.25);
|
|
1266
|
+
else if (k === "-" || k === "_") zoomAt(W / 2, H / 2, 0.8);
|
|
1267
|
+
else if (k === "Enter" || k === " ") {
|
|
1268
|
+
var t = ev.target;
|
|
1269
|
+
if (t && t.getAttribute && t.getAttribute("data-id")) { ev.preventDefault(); selectById(t.getAttribute("data-id")); }
|
|
1270
|
+
}
|
|
1271
|
+
else if (k === "ArrowDown" || k === "ArrowUp") {
|
|
1272
|
+
if (surface === "graph" && listOpen) {
|
|
1273
|
+
ev.preventDefault();
|
|
1274
|
+
var rows = document.querySelectorAll("#list-rows .row");
|
|
1275
|
+
var idx = Array.prototype.indexOf.call(rows, document.activeElement);
|
|
1276
|
+
var next = k === "ArrowDown" ? idx + 1 : idx - 1;
|
|
1277
|
+
if (next >= 0 && next < rows.length) rows[next].focus();
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
else if (k === "Escape") {
|
|
1281
|
+
if (helpOpen) { helpOpen = false; document.getElementById("help").classList.add("hidden"); }
|
|
1282
|
+
else if (document.activeElement === searchEl) { searchEl.blur(); }
|
|
1283
|
+
else if (panel.className === "open") { closePanel(); }
|
|
1284
|
+
else if (focusId) { exitFocus(); }
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// ---------- legend (collapsible; hidden by default so it stays out of the face) ----------
|
|
1289
|
+
var legend = document.getElementById("legend");
|
|
1290
|
+
var L = [["vault", "vault root"], ["note", "vault note"], ["repository", "repository"],
|
|
1291
|
+
["module", "module"], ["file", "okf file"], ["package", "package"], ["entryPoint", "entry point"],
|
|
1292
|
+
["gitState", "git anchor"], ["external", "remote"]];
|
|
1293
|
+
var legendBody = L.map(function (p) {
|
|
1294
|
+
return "<div class='row'><span class='dot' style='background:" + COLORS[p[0]] + "'></span>" + p[1] + "</div>";
|
|
1295
|
+
}).join("") +
|
|
1296
|
+
"<div class='row'><span class='ring human'></span>human</div>" +
|
|
1297
|
+
"<div class='row'><span class='ring agent'></span>agent</div>" +
|
|
1298
|
+
"<div class='row'><span class='ring generated'></span>generated</div>" +
|
|
1299
|
+
"<div class='row'>scroll=zoom · drag=pan · dblclick=focus</div>";
|
|
1300
|
+
legend.innerHTML = "<button id='legend-toggle' class='legend-head' aria-expanded='false'><span class='caret'>▾</span>legend</button>" +
|
|
1301
|
+
"<div class='legend-body'>" + legendBody + "</div>";
|
|
1302
|
+
document.getElementById("legend-toggle").addEventListener("click", function () {
|
|
1303
|
+
var collapsed = legend.classList.toggle("collapsed");
|
|
1304
|
+
document.getElementById("legend-toggle").setAttribute("aria-expanded", String(!collapsed));
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// ---------- init ----------
|
|
1308
|
+
collapsed = { vault: 1, repository: 1 };
|
|
1309
|
+
fetchGraph(true);
|
|
1310
|
+
setInterval(function () { fetchGraph(false).catch(function () {}); }, 5000);
|
|
1311
|
+
loop();
|
|
1312
|
+
})();
|
|
1313
|
+
</script>
|
|
1314
|
+
</body>
|
|
1315
|
+
</html>
|
|
1316
|
+
`;
|