get-claudia 1.35.2 → 1.36.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/bin/index.js +6 -4
- package/package.json +1 -1
- package/visualizer/lib/clusters.js +159 -0
- package/visualizer/lib/graph.js +43 -1
- package/visualizer/lib/pollination.js +101 -0
- package/visualizer/server.js +25 -0
- package/visualizer-threejs/index.html +61 -24
- package/visualizer-threejs/public/styles.css +591 -207
- package/visualizer-threejs/src/config.js +68 -58
- package/visualizer-threejs/src/design-panel.js +21 -0
- package/visualizer-threejs/src/links.js +140 -11
- package/visualizer-threejs/src/main.js +54 -3
- package/visualizer-threejs/src/themes.js +342 -1
- package/visualizer-threejs/src/ui.js +341 -95
package/bin/index.js
CHANGED
|
@@ -412,13 +412,15 @@ async function main() {
|
|
|
412
412
|
runGatewaySetup((gatewayOk) => maybeRunRelay(memoryInstalled, visualizerInstalled, gatewayOk));
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
// Helper: auto-install visualizer after memory
|
|
415
|
+
// Helper: auto-install visualizer after memory, then chain to gateway
|
|
416
|
+
// On upgrades, always attempt visualizer install even if memory step had issues,
|
|
417
|
+
// since the database likely already exists from a prior install.
|
|
416
418
|
function maybeRunVisualizer(memoryInstalled) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
+
const memoryDbExists = existsSync(join(homedir(), '.claudia', 'memory'));
|
|
420
|
+
if (memoryInstalled || isUpgrade || memoryDbExists) {
|
|
419
421
|
runVisualizerSetup((vizOk) => maybeRunGateway(memoryInstalled, vizOk));
|
|
420
422
|
} else {
|
|
421
|
-
//
|
|
423
|
+
// Fresh install with no memory system -- skip visualizer
|
|
422
424
|
maybeRunGateway(memoryInstalled, false);
|
|
423
425
|
}
|
|
424
426
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DBSCAN clustering on 3D UMAP coordinates.
|
|
3
|
+
* Groups semantically similar nodes into clusters for convex hull visualization.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let cachedClusters = null;
|
|
7
|
+
let cacheTimestamp = 0;
|
|
8
|
+
const CACHE_TTL = 60000; // 1 minute (same as UMAP)
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute DBSCAN clusters from 3D positions.
|
|
12
|
+
* @param {Object} positions - Map of nodeId -> { x, y, z }
|
|
13
|
+
* @param {Array} nodes - Graph nodes array
|
|
14
|
+
* @returns {Array} clusters - [{ id, label, memberIds, centroid }]
|
|
15
|
+
*/
|
|
16
|
+
export function computeClusters(positions, nodes, { eps = 30, minPts = 3 } = {}) {
|
|
17
|
+
if (cachedClusters && Date.now() - cacheTimestamp < CACHE_TTL) {
|
|
18
|
+
return cachedClusters;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Only cluster nodes that have UMAP positions
|
|
22
|
+
const posEntries = [];
|
|
23
|
+
for (const node of nodes) {
|
|
24
|
+
const pos = positions[node.id];
|
|
25
|
+
if (pos) {
|
|
26
|
+
posEntries.push({ id: node.id, node, pos: [pos.x, pos.y, pos.z] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (posEntries.length < minPts) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// DBSCAN
|
|
35
|
+
const labels = new Int32Array(posEntries.length).fill(-1); // -1 = unvisited
|
|
36
|
+
const NOISE = -2;
|
|
37
|
+
let clusterId = 0;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < posEntries.length; i++) {
|
|
40
|
+
if (labels[i] !== -1) continue;
|
|
41
|
+
|
|
42
|
+
const neighbors = rangeQuery(posEntries, i, eps);
|
|
43
|
+
if (neighbors.length < minPts) {
|
|
44
|
+
labels[i] = NOISE;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
labels[i] = clusterId;
|
|
49
|
+
const seed = [...neighbors];
|
|
50
|
+
|
|
51
|
+
for (let j = 0; j < seed.length; j++) {
|
|
52
|
+
const q = seed[j];
|
|
53
|
+
if (labels[q] === NOISE) labels[q] = clusterId;
|
|
54
|
+
if (labels[q] !== -1) continue;
|
|
55
|
+
|
|
56
|
+
labels[q] = clusterId;
|
|
57
|
+
const qNeighbors = rangeQuery(posEntries, q, eps);
|
|
58
|
+
if (qNeighbors.length >= minPts) {
|
|
59
|
+
for (const n of qNeighbors) {
|
|
60
|
+
if (!seed.includes(n)) seed.push(n);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
clusterId++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build cluster objects
|
|
68
|
+
const clusterMap = new Map();
|
|
69
|
+
for (let i = 0; i < posEntries.length; i++) {
|
|
70
|
+
const cid = labels[i];
|
|
71
|
+
if (cid < 0) continue;
|
|
72
|
+
if (!clusterMap.has(cid)) clusterMap.set(cid, []);
|
|
73
|
+
clusterMap.get(cid).push(posEntries[i]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const clusters = [];
|
|
77
|
+
for (const [id, members] of clusterMap) {
|
|
78
|
+
if (members.length < minPts) continue;
|
|
79
|
+
|
|
80
|
+
const centroid = [0, 0, 0];
|
|
81
|
+
for (const m of members) {
|
|
82
|
+
centroid[0] += m.pos[0];
|
|
83
|
+
centroid[1] += m.pos[1];
|
|
84
|
+
centroid[2] += m.pos[2];
|
|
85
|
+
}
|
|
86
|
+
centroid[0] /= members.length;
|
|
87
|
+
centroid[1] /= members.length;
|
|
88
|
+
centroid[2] /= members.length;
|
|
89
|
+
|
|
90
|
+
clusters.push({
|
|
91
|
+
id,
|
|
92
|
+
label: generateClusterLabel(members.map(m => m.node)),
|
|
93
|
+
memberIds: members.map(m => m.id),
|
|
94
|
+
centroid
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
cachedClusters = clusters;
|
|
99
|
+
cacheTimestamp = Date.now();
|
|
100
|
+
return clusters;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Euclidean distance range query for DBSCAN */
|
|
104
|
+
function rangeQuery(entries, idx, eps) {
|
|
105
|
+
const neighbors = [];
|
|
106
|
+
const p = entries[idx].pos;
|
|
107
|
+
const eps2 = eps * eps;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < entries.length; i++) {
|
|
110
|
+
if (i === idx) continue;
|
|
111
|
+
const q = entries[i].pos;
|
|
112
|
+
const d2 = (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2 + (p[2] - q[2]) ** 2;
|
|
113
|
+
if (d2 <= eps2) neighbors.push(i);
|
|
114
|
+
}
|
|
115
|
+
return neighbors;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Generate a label from cluster member nodes using most common entity type/name */
|
|
119
|
+
function generateClusterLabel(nodes) {
|
|
120
|
+
// Count entity types
|
|
121
|
+
const typeCounts = {};
|
|
122
|
+
const entityNames = [];
|
|
123
|
+
for (const n of nodes) {
|
|
124
|
+
if (n.nodeType === 'entity') {
|
|
125
|
+
const t = n.entityType || 'unknown';
|
|
126
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
127
|
+
entityNames.push(n.name);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Most common entity type
|
|
132
|
+
let topType = null;
|
|
133
|
+
let topCount = 0;
|
|
134
|
+
for (const [type, count] of Object.entries(typeCounts)) {
|
|
135
|
+
if (count > topCount) { topType = type; topCount = count; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Use top entity names (by importance) for label
|
|
139
|
+
const importantNodes = nodes
|
|
140
|
+
.filter(n => n.nodeType === 'entity')
|
|
141
|
+
.sort((a, b) => (b.importance || 0) - (a.importance || 0))
|
|
142
|
+
.slice(0, 2);
|
|
143
|
+
|
|
144
|
+
if (importantNodes.length > 0) {
|
|
145
|
+
const names = importantNodes.map(n => n.name).join(', ');
|
|
146
|
+
return topType ? `${capitalize(topType)}: ${names}` : names;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return topType ? `${capitalize(topType)} cluster` : `Cluster`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function capitalize(s) {
|
|
153
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function invalidateClusterCache() {
|
|
157
|
+
cachedClusters = null;
|
|
158
|
+
cacheTimestamp = 0;
|
|
159
|
+
}
|
package/visualizer/lib/graph.js
CHANGED
|
@@ -207,7 +207,49 @@ export function buildGraph({ includeHistorical = false } = {}) {
|
|
|
207
207
|
timestamp: new Date().toISOString()
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
// ── Deduplicate bidirectional relationship links ─────────
|
|
211
|
+
// If A→B and B→A both exist, merge into one link with bidirectional: true
|
|
212
|
+
const linkMap = new Map(); // canonical key -> link
|
|
213
|
+
const deduped = [];
|
|
214
|
+
|
|
215
|
+
for (const l of links) {
|
|
216
|
+
if (l.linkType !== 'relationship') {
|
|
217
|
+
deduped.push(l);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const a = l.source < l.target ? l.source : l.target;
|
|
221
|
+
const b = l.source < l.target ? l.target : l.source;
|
|
222
|
+
const key = `${a}_${b}`;
|
|
223
|
+
|
|
224
|
+
if (linkMap.has(key)) {
|
|
225
|
+
// Merge: keep stronger strength, combine labels, mark bidirectional
|
|
226
|
+
const existing = linkMap.get(key);
|
|
227
|
+
existing.bidirectional = true;
|
|
228
|
+
if (l.strength > existing.strength) {
|
|
229
|
+
existing.strength = l.strength;
|
|
230
|
+
existing.width = l.width;
|
|
231
|
+
}
|
|
232
|
+
// Combine labels if different
|
|
233
|
+
if (l.label && l.label !== existing.label) {
|
|
234
|
+
existing.label = `${existing.label} / ${l.label}`;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
l.bidirectional = false;
|
|
238
|
+
linkMap.set(key, l);
|
|
239
|
+
deduped.push(l);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Build existing link pairs set for pollination exclusion
|
|
244
|
+
const existingPairs = new Set();
|
|
245
|
+
for (const l of deduped) {
|
|
246
|
+
if (l.linkType === 'relationship') {
|
|
247
|
+
existingPairs.add(`${l.source}_${l.target}`);
|
|
248
|
+
existingPairs.add(`${l.target}_${l.source}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { nodes, links: deduped, meta, existingPairs };
|
|
211
253
|
}
|
|
212
254
|
|
|
213
255
|
// ── Helpers ─────────────────────────────────────────────────
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pollination — discover weak-but-plausible connections between distant nodes.
|
|
3
|
+
* Computes pairwise cosine similarity on entity embeddings, filters to
|
|
4
|
+
* the "interesting" range [0.3, 0.5], and excludes already-linked pairs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getDb } from './database.js';
|
|
8
|
+
|
|
9
|
+
let cachedLinks = null;
|
|
10
|
+
let cacheTimestamp = 0;
|
|
11
|
+
const CACHE_TTL = 60000; // 1 minute
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compute pollination links from entity embeddings.
|
|
15
|
+
* @param {Set} existingPairs - Set of "entityId-entityId" strings for already-linked pairs
|
|
16
|
+
* @returns {Array} [{ source, target, similarity }]
|
|
17
|
+
*/
|
|
18
|
+
export function computePollinationLinks(existingPairs) {
|
|
19
|
+
if (cachedLinks && Date.now() - cacheTimestamp < CACHE_TTL) {
|
|
20
|
+
return cachedLinks;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const db = getDb();
|
|
24
|
+
let embeddings;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const rows = db.prepare('SELECT entity_id as id, embedding FROM entity_embeddings').all();
|
|
28
|
+
embeddings = rows.map(row => ({
|
|
29
|
+
id: row.id,
|
|
30
|
+
vec: blobToFloat32(row.embedding)
|
|
31
|
+
})).filter(r => r.vec !== null);
|
|
32
|
+
} catch {
|
|
33
|
+
return []; // Table might not exist
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (embeddings.length < 2) return [];
|
|
37
|
+
|
|
38
|
+
// Precompute norms
|
|
39
|
+
for (const e of embeddings) {
|
|
40
|
+
let sum = 0;
|
|
41
|
+
for (let i = 0; i < e.vec.length; i++) sum += e.vec[i] * e.vec[i];
|
|
42
|
+
e.norm = Math.sqrt(sum);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const links = [];
|
|
46
|
+
const SIM_LOW = 0.3;
|
|
47
|
+
const SIM_HIGH = 0.5;
|
|
48
|
+
const MAX_LINKS = 50;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
51
|
+
for (let j = i + 1; j < embeddings.length; j++) {
|
|
52
|
+
const a = embeddings[i];
|
|
53
|
+
const b = embeddings[j];
|
|
54
|
+
|
|
55
|
+
// Skip already-linked pairs
|
|
56
|
+
const pairKey = `entity-${a.id}_entity-${b.id}`;
|
|
57
|
+
const pairKeyR = `entity-${b.id}_entity-${a.id}`;
|
|
58
|
+
if (existingPairs.has(pairKey) || existingPairs.has(pairKeyR)) continue;
|
|
59
|
+
|
|
60
|
+
// Cosine similarity
|
|
61
|
+
let dot = 0;
|
|
62
|
+
for (let k = 0; k < a.vec.length; k++) dot += a.vec[k] * b.vec[k];
|
|
63
|
+
const sim = a.norm > 0 && b.norm > 0 ? dot / (a.norm * b.norm) : 0;
|
|
64
|
+
|
|
65
|
+
if (sim >= SIM_LOW && sim <= SIM_HIGH) {
|
|
66
|
+
links.push({
|
|
67
|
+
source: `entity-${a.id}`,
|
|
68
|
+
target: `entity-${b.id}`,
|
|
69
|
+
similarity: Math.round(sim * 1000) / 1000
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sort by similarity descending, take top N
|
|
76
|
+
links.sort((a, b) => b.similarity - a.similarity);
|
|
77
|
+
const result = links.slice(0, MAX_LINKS);
|
|
78
|
+
|
|
79
|
+
cachedLinks = result;
|
|
80
|
+
cacheTimestamp = Date.now();
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function blobToFloat32(blob) {
|
|
85
|
+
if (!blob) return null;
|
|
86
|
+
try {
|
|
87
|
+
if (Buffer.isBuffer(blob)) {
|
|
88
|
+
const floats = new Float32Array(blob.buffer, blob.byteOffset, blob.length / 4);
|
|
89
|
+
return Array.from(floats);
|
|
90
|
+
}
|
|
91
|
+
if (typeof blob === 'string') return JSON.parse(blob);
|
|
92
|
+
return null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function invalidatePollinationCache() {
|
|
99
|
+
cachedLinks = null;
|
|
100
|
+
cacheTimestamp = 0;
|
|
101
|
+
}
|
package/visualizer/server.js
CHANGED
|
@@ -15,6 +15,8 @@ import { dirname, join } from 'path';
|
|
|
15
15
|
import { openDatabase, findDbPath, closeDatabase, getDb, listDatabases, getProjectHash } from './lib/database.js';
|
|
16
16
|
import { buildGraph } from './lib/graph.js';
|
|
17
17
|
import { getProjectedPositions } from './lib/projection.js';
|
|
18
|
+
import { computeClusters } from './lib/clusters.js';
|
|
19
|
+
import { computePollinationLinks } from './lib/pollination.js';
|
|
18
20
|
import { createEventStream, stopPolling } from './lib/events.js';
|
|
19
21
|
|
|
20
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -105,6 +107,29 @@ app.get('/api/graph', async (req, res) => {
|
|
|
105
107
|
graph.meta.umapEnabled = false;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// Compute clusters from UMAP positions
|
|
111
|
+
try {
|
|
112
|
+
const positions = {};
|
|
113
|
+
for (const node of graph.nodes) {
|
|
114
|
+
if (node.fx !== undefined) {
|
|
115
|
+
positions[node.id] = { x: node.fx, y: node.fy, z: node.fz };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
graph.clusters = computeClusters(positions, graph.nodes);
|
|
119
|
+
} catch {
|
|
120
|
+
graph.clusters = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Compute pollination links from entity embeddings
|
|
124
|
+
try {
|
|
125
|
+
graph.pollinationLinks = computePollinationLinks(graph.existingPairs || new Set());
|
|
126
|
+
} catch {
|
|
127
|
+
graph.pollinationLinks = [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Remove internal-only field before sending to client
|
|
131
|
+
delete graph.existingPairs;
|
|
132
|
+
|
|
108
133
|
res.json(graph);
|
|
109
134
|
} catch (err) {
|
|
110
135
|
console.error('Graph error:', err);
|
|
@@ -10,69 +10,99 @@
|
|
|
10
10
|
<!-- Three.js container -->
|
|
11
11
|
<div id="graph-container"></div>
|
|
12
12
|
|
|
13
|
+
<!-- Hover tooltip (follows cursor) -->
|
|
14
|
+
<div id="tooltip" class="hidden">
|
|
15
|
+
<div id="tooltip-name"></div>
|
|
16
|
+
<div id="tooltip-type"></div>
|
|
17
|
+
<div id="tooltip-meta"></div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
13
20
|
<!-- Top HUD bar -->
|
|
14
21
|
<div id="hud-bar">
|
|
15
|
-
<div id="hud-
|
|
22
|
+
<div id="hud-left">
|
|
16
23
|
<img src="/assets/claudia-logo.png" alt="Claudia" class="hud-logo" />
|
|
17
|
-
<span>Claudia Brain</span>
|
|
24
|
+
<span class="hud-brand">Claudia Brain</span>
|
|
18
25
|
<span id="activity-pulse" class="pulse"></span>
|
|
19
26
|
</div>
|
|
20
27
|
<div id="hud-stats">
|
|
21
|
-
<div class="stat"><span id="stat-entities">0</span>
|
|
22
|
-
<div class="stat"><span id="stat-memories">0</span>
|
|
23
|
-
<div class="stat"><span id="stat-patterns">0</span>
|
|
24
|
-
<div class="stat"><span id="stat-relationships">0</span>
|
|
28
|
+
<div class="stat"><span id="stat-entities">0</span><label>entities</label></div>
|
|
29
|
+
<div class="stat"><span id="stat-memories">0</span><label>memories</label></div>
|
|
30
|
+
<div class="stat"><span id="stat-patterns">0</span><label>patterns</label></div>
|
|
31
|
+
<div class="stat"><span id="stat-relationships">0</span><label>relationships</label></div>
|
|
25
32
|
</div>
|
|
26
33
|
<div id="hud-controls">
|
|
27
|
-
<a href="https://github.com/kbanc85/claudia" target="_blank" rel="noopener"
|
|
28
|
-
class="hud-github" title="View on GitHub">
|
|
29
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
30
|
-
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
31
|
-
</svg>
|
|
32
|
-
</a>
|
|
33
34
|
<div id="db-selector-container">
|
|
34
35
|
<select id="db-selector" title="Select Database">
|
|
35
36
|
<option value="">Loading...</option>
|
|
36
37
|
</select>
|
|
37
38
|
</div>
|
|
38
|
-
<span id="fps-counter">-- FPS</span>
|
|
39
|
-
<span id="engine-info">Three.js</span>
|
|
40
39
|
<div id="settings-container">
|
|
41
40
|
<button id="settings-btn" title="Quality Settings">⚙</button>
|
|
42
41
|
<div id="settings-panel" class="hidden">
|
|
43
42
|
<div class="settings-title">Quality</div>
|
|
44
|
-
<label class="settings-option"><input type="radio" name="quality" value="low"><span>Low</span><span class="settings-desc">No bloom
|
|
45
|
-
<label class="settings-option"><input type="radio" name="quality" value="medium"><span>Medium</span><span class="settings-desc">Bloom
|
|
43
|
+
<label class="settings-option"><input type="radio" name="quality" value="low"><span>Low</span><span class="settings-desc">No bloom</span></label>
|
|
44
|
+
<label class="settings-option"><input type="radio" name="quality" value="medium"><span>Medium</span><span class="settings-desc">Bloom + particles</span></label>
|
|
46
45
|
<label class="settings-option"><input type="radio" name="quality" value="high" checked><span>High</span><span class="settings-desc">Full effects</span></label>
|
|
47
|
-
<label class="settings-option"><input type="radio" name="quality" value="ultra"><span>Ultra</span><span class="settings-desc">
|
|
46
|
+
<label class="settings-option"><input type="radio" name="quality" value="ultra"><span>Ultra</span><span class="settings-desc">Max glow</span></label>
|
|
47
|
+
<div class="settings-divider"></div>
|
|
48
|
+
<div class="settings-title">Resolution</div>
|
|
49
|
+
<label class="settings-option"><input type="radio" name="resolution" value="0"><span>Auto</span><span class="settings-desc">Device default</span></label>
|
|
50
|
+
<label class="settings-option"><input type="radio" name="resolution" value="0.5"><span>0.5x</span><span class="settings-desc">Performance</span></label>
|
|
51
|
+
<label class="settings-option"><input type="radio" name="resolution" value="1"><span>1x</span><span class="settings-desc">Balanced</span></label>
|
|
52
|
+
<label class="settings-option"><input type="radio" name="resolution" value="1.5"><span>1.5x</span><span class="settings-desc">Crisp</span></label>
|
|
53
|
+
<label class="settings-option"><input type="radio" name="resolution" value="2" ><span>2x</span><span class="settings-desc">Retina</span></label>
|
|
54
|
+
<div class="settings-divider"></div>
|
|
55
|
+
<div class="settings-meta">
|
|
56
|
+
<span id="fps-counter">-- FPS</span>
|
|
57
|
+
<span id="engine-info">Three.js</span>
|
|
58
|
+
<a href="https://github.com/kbanc85/claudia" target="_blank" rel="noopener" class="hud-github" title="View on GitHub">
|
|
59
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
60
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
61
|
+
</svg>
|
|
62
|
+
</a>
|
|
63
|
+
</div>
|
|
48
64
|
</div>
|
|
49
65
|
</div>
|
|
50
66
|
</div>
|
|
51
67
|
</div>
|
|
52
68
|
|
|
53
|
-
<!--
|
|
54
|
-
<div id="sidebar">
|
|
69
|
+
<!-- Sidebar (collapsible overlay) -->
|
|
70
|
+
<div id="sidebar" class="collapsed">
|
|
71
|
+
<div id="sidebar-header">
|
|
72
|
+
<span class="sidebar-title">Filters</span>
|
|
73
|
+
<button id="sidebar-close" title="Close (S)">×</button>
|
|
74
|
+
</div>
|
|
55
75
|
<div id="search-container">
|
|
56
|
-
<input type="text" id="search-input" placeholder="Search
|
|
76
|
+
<input type="text" id="search-input" placeholder="Search... ( / )" autocomplete="off">
|
|
57
77
|
<div id="search-results"></div>
|
|
58
78
|
</div>
|
|
59
79
|
<div id="filters">
|
|
60
|
-
<h3>
|
|
80
|
+
<h3>Entity Types</h3>
|
|
61
81
|
<div id="type-filters"></div>
|
|
62
82
|
<h3>Memory Types</h3>
|
|
63
83
|
<div id="memory-filters"></div>
|
|
64
84
|
</div>
|
|
65
|
-
<div id="legend"
|
|
85
|
+
<div id="legend">
|
|
86
|
+
<h3>Shapes</h3>
|
|
87
|
+
</div>
|
|
66
88
|
</div>
|
|
67
89
|
|
|
90
|
+
<!-- Sidebar toggle button (always visible) -->
|
|
91
|
+
<button id="sidebar-toggle" title="Filters (S)">
|
|
92
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
93
|
+
<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
|
|
68
97
|
<!-- Right detail panel (slides in on node click) -->
|
|
69
98
|
<div id="detail-panel" class="hidden">
|
|
70
|
-
<button id="detail-close">×</button>
|
|
99
|
+
<button id="detail-close" title="Close (Esc)">×</button>
|
|
71
100
|
<div id="detail-content"></div>
|
|
72
101
|
</div>
|
|
73
102
|
|
|
74
103
|
<!-- Bottom timeline -->
|
|
75
104
|
<div id="timeline-container">
|
|
105
|
+
<div id="timeline-density"></div>
|
|
76
106
|
<div id="timeline-bar">
|
|
77
107
|
<input type="range" id="timeline-slider" min="0" max="100" value="100">
|
|
78
108
|
<div id="timeline-labels">
|
|
@@ -85,7 +115,14 @@
|
|
|
85
115
|
<button id="timeline-play" title="Play/Pause">▶</button>
|
|
86
116
|
<button id="timeline-speed" title="Speed">1x</button>
|
|
87
117
|
</div>
|
|
88
|
-
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Keyboard hints (shown briefly on first load) -->
|
|
121
|
+
<div id="keyboard-hints" class="hidden">
|
|
122
|
+
<div class="hint"><kbd>S</kbd> Filters</div>
|
|
123
|
+
<div class="hint"><kbd>/</kbd> Search</div>
|
|
124
|
+
<div class="hint"><kbd>H</kbd> Design</div>
|
|
125
|
+
<div class="hint"><kbd>Esc</kbd> Clear</div>
|
|
89
126
|
</div>
|
|
90
127
|
|
|
91
128
|
<script type="module" src="/src/main.js"></script>
|