@jungjaehoon/mama-server 1.4.3 → 1.4.5
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/README.md +20 -0
- package/package.json +1 -1
- package/src/viewer/graph-api.js +73 -0
- package/src/viewer/viewer.css +236 -2
- package/src/viewer/viewer.html +12 -0
- package/src/viewer/viewer.js +202 -0
package/README.md
CHANGED
|
@@ -179,6 +179,26 @@ curl -X POST http://127.0.0.1:3847/embed/batch \
|
|
|
179
179
|
- **Automatic**: Starts with MCP server, no extra configuration needed
|
|
180
180
|
- **Secure**: localhost only (127.0.0.1), no external access
|
|
181
181
|
|
|
182
|
+
## Graph Viewer
|
|
183
|
+
|
|
184
|
+
Interactive visualization of your reasoning graph.
|
|
185
|
+
|
|
186
|
+
**Access:** `http://localhost:3847/viewer`
|
|
187
|
+
|
|
188
|
+
**Features:**
|
|
189
|
+
|
|
190
|
+
- Network graph with physics simulation
|
|
191
|
+
- Checkpoint timeline sidebar
|
|
192
|
+
- Draggable detail panel
|
|
193
|
+
- Topic filtering and search
|
|
194
|
+
|
|
195
|
+
## Environment Variables
|
|
196
|
+
|
|
197
|
+
| Variable | Default | Description |
|
|
198
|
+
| ---------------- | -------------------------- | -------------------------- |
|
|
199
|
+
| `MAMA_DB_PATH` | `~/.claude/mama-memory.db` | SQLite database location |
|
|
200
|
+
| `MAMA_HTTP_PORT` | `3847` | HTTP embedding server port |
|
|
201
|
+
|
|
182
202
|
## Technical Details
|
|
183
203
|
|
|
184
204
|
- **Database:** SQLite + sqlite-vec extension
|
package/package.json
CHANGED
package/src/viewer/graph-api.js
CHANGED
|
@@ -85,6 +85,39 @@ async function getAllEdges() {
|
|
|
85
85
|
}));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Get all checkpoints
|
|
90
|
+
*
|
|
91
|
+
* @returns {Promise<Array>} Array of checkpoint objects
|
|
92
|
+
*/
|
|
93
|
+
async function getAllCheckpoints() {
|
|
94
|
+
const adapter = getAdapter();
|
|
95
|
+
|
|
96
|
+
const stmt = adapter.prepare(`
|
|
97
|
+
SELECT
|
|
98
|
+
id,
|
|
99
|
+
timestamp,
|
|
100
|
+
summary,
|
|
101
|
+
open_files,
|
|
102
|
+
next_steps,
|
|
103
|
+
status
|
|
104
|
+
FROM checkpoints
|
|
105
|
+
ORDER BY timestamp DESC
|
|
106
|
+
LIMIT 50
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
const rows = stmt.all();
|
|
110
|
+
|
|
111
|
+
return rows.map((row) => ({
|
|
112
|
+
id: row.id,
|
|
113
|
+
timestamp: row.timestamp,
|
|
114
|
+
summary: row.summary,
|
|
115
|
+
open_files: row.open_files ? JSON.parse(row.open_files) : [],
|
|
116
|
+
next_steps: row.next_steps,
|
|
117
|
+
status: row.status,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
88
121
|
/**
|
|
89
122
|
* Get unique topics from nodes
|
|
90
123
|
*
|
|
@@ -455,6 +488,39 @@ async function handleSimilarRequest(req, res, params) {
|
|
|
455
488
|
}
|
|
456
489
|
}
|
|
457
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Handle GET /checkpoints request - list all checkpoints
|
|
493
|
+
*
|
|
494
|
+
* @param {Object} req - HTTP request
|
|
495
|
+
* @param {Object} res - HTTP response
|
|
496
|
+
*/
|
|
497
|
+
async function handleCheckpointsRequest(req, res) {
|
|
498
|
+
try {
|
|
499
|
+
// Ensure DB is initialized
|
|
500
|
+
await initDB();
|
|
501
|
+
|
|
502
|
+
const checkpoints = await getAllCheckpoints();
|
|
503
|
+
|
|
504
|
+
res.writeHead(200);
|
|
505
|
+
res.end(
|
|
506
|
+
JSON.stringify({
|
|
507
|
+
checkpoints,
|
|
508
|
+
count: checkpoints.length,
|
|
509
|
+
})
|
|
510
|
+
);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error(`[GraphAPI] Checkpoints error: ${error.message}`);
|
|
513
|
+
res.writeHead(500);
|
|
514
|
+
res.end(
|
|
515
|
+
JSON.stringify({
|
|
516
|
+
error: true,
|
|
517
|
+
code: 'CHECKPOINTS_FAILED',
|
|
518
|
+
message: error.message,
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
458
524
|
/**
|
|
459
525
|
* Create route handler for graph API
|
|
460
526
|
*
|
|
@@ -506,6 +572,12 @@ function createGraphHandler() {
|
|
|
506
572
|
return true; // Request handled
|
|
507
573
|
}
|
|
508
574
|
|
|
575
|
+
// Route: GET /checkpoints - list all checkpoints
|
|
576
|
+
if (pathname === '/checkpoints' && req.method === 'GET') {
|
|
577
|
+
await handleCheckpointsRequest(req, res);
|
|
578
|
+
return true; // Request handled
|
|
579
|
+
}
|
|
580
|
+
|
|
509
581
|
return false; // Request not handled
|
|
510
582
|
};
|
|
511
583
|
}
|
|
@@ -515,6 +587,7 @@ module.exports = {
|
|
|
515
587
|
// Exported for testing
|
|
516
588
|
getAllNodes,
|
|
517
589
|
getAllEdges,
|
|
590
|
+
getAllCheckpoints,
|
|
518
591
|
getUniqueTopics,
|
|
519
592
|
filterNodesByTopic,
|
|
520
593
|
filterEdgesByNodes,
|
package/src/viewer/viewer.css
CHANGED
|
@@ -83,11 +83,11 @@ header h1 {
|
|
|
83
83
|
position: relative;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/* Detail Panel */
|
|
86
|
+
/* Detail Panel - draggable */
|
|
87
87
|
#detail-panel {
|
|
88
88
|
display: none;
|
|
89
89
|
position: fixed;
|
|
90
|
-
right:
|
|
90
|
+
right: 340px;
|
|
91
91
|
top: 80px;
|
|
92
92
|
width: 350px;
|
|
93
93
|
max-height: calc(100vh - 100px);
|
|
@@ -97,6 +97,7 @@ header h1 {
|
|
|
97
97
|
padding: 16px;
|
|
98
98
|
overflow-y: auto;
|
|
99
99
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
100
|
+
z-index: 300;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
#detail-panel.visible {
|
|
@@ -107,6 +108,8 @@ header h1 {
|
|
|
107
108
|
color: #a0a0ff;
|
|
108
109
|
margin-bottom: 12px;
|
|
109
110
|
font-size: 14px;
|
|
111
|
+
cursor: move;
|
|
112
|
+
user-select: none;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
#detail-panel .field {
|
|
@@ -426,3 +429,234 @@ header h1 {
|
|
|
426
429
|
#legend-panel.collapsed + .legend-toggle {
|
|
427
430
|
display: block;
|
|
428
431
|
}
|
|
432
|
+
|
|
433
|
+
/* Checkpoint Sidebar Panel */
|
|
434
|
+
#checkpoint-panel {
|
|
435
|
+
position: fixed;
|
|
436
|
+
right: 0;
|
|
437
|
+
top: 50px;
|
|
438
|
+
width: 320px;
|
|
439
|
+
height: calc(100vh - 50px);
|
|
440
|
+
background: #16213e;
|
|
441
|
+
border-left: 1px solid #4a4a6a;
|
|
442
|
+
display: flex;
|
|
443
|
+
flex-direction: column;
|
|
444
|
+
z-index: 200;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#checkpoint-panel.hidden {
|
|
448
|
+
display: none;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.checkpoint-header {
|
|
452
|
+
display: flex;
|
|
453
|
+
justify-content: space-between;
|
|
454
|
+
align-items: center;
|
|
455
|
+
padding: 12px 16px;
|
|
456
|
+
border-bottom: 1px solid #4a4a6a;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.checkpoint-header h4 {
|
|
460
|
+
color: #a0a0ff;
|
|
461
|
+
font-size: 14px;
|
|
462
|
+
font-weight: 600;
|
|
463
|
+
margin: 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.checkpoint-list {
|
|
467
|
+
flex: 1;
|
|
468
|
+
overflow-y: auto;
|
|
469
|
+
padding: 8px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* Custom scrollbar for dark theme */
|
|
473
|
+
.checkpoint-list::-webkit-scrollbar,
|
|
474
|
+
.checkpoint-section-content::-webkit-scrollbar,
|
|
475
|
+
#detail-panel::-webkit-scrollbar,
|
|
476
|
+
.reasoning-content::-webkit-scrollbar,
|
|
477
|
+
.similar-list::-webkit-scrollbar {
|
|
478
|
+
width: 6px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.checkpoint-list::-webkit-scrollbar-track,
|
|
482
|
+
.checkpoint-section-content::-webkit-scrollbar-track,
|
|
483
|
+
#detail-panel::-webkit-scrollbar-track,
|
|
484
|
+
.reasoning-content::-webkit-scrollbar-track,
|
|
485
|
+
.similar-list::-webkit-scrollbar-track {
|
|
486
|
+
background: #1a1a2e;
|
|
487
|
+
border-radius: 3px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.checkpoint-list::-webkit-scrollbar-thumb,
|
|
491
|
+
.checkpoint-section-content::-webkit-scrollbar-thumb,
|
|
492
|
+
#detail-panel::-webkit-scrollbar-thumb,
|
|
493
|
+
.reasoning-content::-webkit-scrollbar-thumb,
|
|
494
|
+
.similar-list::-webkit-scrollbar-thumb {
|
|
495
|
+
background: #4a4a6a;
|
|
496
|
+
border-radius: 3px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.checkpoint-list::-webkit-scrollbar-thumb:hover,
|
|
500
|
+
.checkpoint-section-content::-webkit-scrollbar-thumb:hover,
|
|
501
|
+
#detail-panel::-webkit-scrollbar-thumb:hover,
|
|
502
|
+
.reasoning-content::-webkit-scrollbar-thumb:hover,
|
|
503
|
+
.similar-list::-webkit-scrollbar-thumb:hover {
|
|
504
|
+
background: #5a5a7a;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* Horizontal scrollbar (for graph container) */
|
|
508
|
+
#graph-container::-webkit-scrollbar {
|
|
509
|
+
height: 6px;
|
|
510
|
+
width: 6px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#graph-container::-webkit-scrollbar-track {
|
|
514
|
+
background: #1a1a2e;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#graph-container::-webkit-scrollbar-thumb {
|
|
518
|
+
background: #4a4a6a;
|
|
519
|
+
border-radius: 3px;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#graph-container::-webkit-scrollbar-thumb:hover {
|
|
523
|
+
background: #5a5a7a;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* Firefox scrollbar support */
|
|
527
|
+
* {
|
|
528
|
+
scrollbar-width: thin;
|
|
529
|
+
scrollbar-color: #4a4a6a #1a1a2e;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.loading-checkpoints {
|
|
533
|
+
color: #666;
|
|
534
|
+
font-style: italic;
|
|
535
|
+
text-align: center;
|
|
536
|
+
padding: 20px;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.checkpoint-item {
|
|
540
|
+
background: #1a1a2e;
|
|
541
|
+
border: 1px solid #3a3a5a;
|
|
542
|
+
border-radius: 6px;
|
|
543
|
+
padding: 12px;
|
|
544
|
+
margin-bottom: 8px;
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
transition: all 0.2s;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.checkpoint-item:hover {
|
|
550
|
+
border-color: #a0a0ff;
|
|
551
|
+
background: #1e1e3e;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.checkpoint-item.expanded {
|
|
555
|
+
border-color: #a0a0ff;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.checkpoint-time {
|
|
559
|
+
font-size: 11px;
|
|
560
|
+
color: #888;
|
|
561
|
+
margin-bottom: 6px;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.checkpoint-summary {
|
|
565
|
+
font-size: 12px;
|
|
566
|
+
color: #e0e0e0;
|
|
567
|
+
line-height: 1.4;
|
|
568
|
+
max-height: 60px;
|
|
569
|
+
overflow: hidden;
|
|
570
|
+
text-overflow: ellipsis;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.checkpoint-item.expanded .checkpoint-summary {
|
|
574
|
+
max-height: none;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.checkpoint-details {
|
|
578
|
+
display: none;
|
|
579
|
+
margin-top: 10px;
|
|
580
|
+
padding-top: 10px;
|
|
581
|
+
border-top: 1px solid #3a3a5a;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.checkpoint-item.expanded .checkpoint-details {
|
|
585
|
+
display: block;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.checkpoint-section {
|
|
589
|
+
margin-bottom: 8px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.checkpoint-section-title {
|
|
593
|
+
font-size: 10px;
|
|
594
|
+
color: #888;
|
|
595
|
+
text-transform: uppercase;
|
|
596
|
+
margin-bottom: 4px;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.checkpoint-section-content {
|
|
600
|
+
font-size: 11px;
|
|
601
|
+
color: #aaa;
|
|
602
|
+
white-space: pre-wrap;
|
|
603
|
+
max-height: 100px;
|
|
604
|
+
overflow-y: auto;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.checkpoint-files {
|
|
608
|
+
display: flex;
|
|
609
|
+
flex-wrap: wrap;
|
|
610
|
+
gap: 4px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.checkpoint-file {
|
|
614
|
+
font-size: 10px;
|
|
615
|
+
color: #6366f1;
|
|
616
|
+
background: #2a2a4e;
|
|
617
|
+
padding: 2px 6px;
|
|
618
|
+
border-radius: 3px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.checkpoint-related {
|
|
622
|
+
display: flex;
|
|
623
|
+
flex-wrap: wrap;
|
|
624
|
+
gap: 4px;
|
|
625
|
+
margin-top: 4px;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.checkpoint-related-link {
|
|
629
|
+
font-size: 10px;
|
|
630
|
+
color: #22c55e;
|
|
631
|
+
background: #1a3a2e;
|
|
632
|
+
padding: 2px 6px;
|
|
633
|
+
border-radius: 3px;
|
|
634
|
+
cursor: pointer;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.checkpoint-related-link:hover {
|
|
638
|
+
background: #2a4a3e;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.checkpoint-toggle {
|
|
642
|
+
position: fixed;
|
|
643
|
+
right: 20px;
|
|
644
|
+
top: 60px;
|
|
645
|
+
background: #16213e;
|
|
646
|
+
border: 1px solid #4a4a6a;
|
|
647
|
+
border-radius: 4px;
|
|
648
|
+
padding: 8px 12px;
|
|
649
|
+
color: #a0a0ff;
|
|
650
|
+
cursor: pointer;
|
|
651
|
+
font-size: 12px;
|
|
652
|
+
z-index: 99;
|
|
653
|
+
display: none;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.checkpoint-toggle:hover {
|
|
657
|
+
background: #1a2a4e;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
#checkpoint-panel.hidden ~ .checkpoint-toggle {
|
|
661
|
+
display: block;
|
|
662
|
+
}
|
package/src/viewer/viewer.html
CHANGED
|
@@ -63,6 +63,18 @@
|
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
|
+
<!-- Checkpoint Sidebar Panel -->
|
|
67
|
+
<div id="checkpoint-panel">
|
|
68
|
+
<div class="checkpoint-header">
|
|
69
|
+
<h4>📋 Checkpoints</h4>
|
|
70
|
+
<button class="close-btn" onclick="toggleCheckpoints()">×</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="checkpoint-list" id="checkpoint-list">
|
|
73
|
+
<div class="loading-checkpoints">Loading...</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<button class="checkpoint-toggle" id="checkpoint-toggle" onclick="toggleCheckpoints()">📋 Checkpoints</button>
|
|
77
|
+
|
|
66
78
|
<!-- Legend Panel -->
|
|
67
79
|
<div id="legend-panel">
|
|
68
80
|
<button class="close-btn" onclick="toggleLegend()" style="position:absolute;top:8px;right:8px;">×</button>
|
package/src/viewer/viewer.js
CHANGED
|
@@ -822,8 +822,210 @@ document.addEventListener('keydown', (e) => {
|
|
|
822
822
|
}
|
|
823
823
|
});
|
|
824
824
|
|
|
825
|
+
// Checkpoint Panel Functions
|
|
826
|
+
let checkpointsData = [];
|
|
827
|
+
|
|
828
|
+
// Toggle checkpoint panel visibility (called from HTML onclick)
|
|
829
|
+
// eslint-disable-next-line no-unused-vars
|
|
830
|
+
function toggleCheckpoints() {
|
|
831
|
+
const panel = document.getElementById('checkpoint-panel');
|
|
832
|
+
panel.classList.toggle('hidden');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Fetch checkpoints from API
|
|
836
|
+
async function fetchCheckpoints() {
|
|
837
|
+
try {
|
|
838
|
+
const response = await fetch('/checkpoints');
|
|
839
|
+
if (!response.ok) {
|
|
840
|
+
throw new Error(`HTTP ${response.status}`);
|
|
841
|
+
}
|
|
842
|
+
const data = await response.json();
|
|
843
|
+
checkpointsData = data.checkpoints || [];
|
|
844
|
+
renderCheckpoints();
|
|
845
|
+
} catch (error) {
|
|
846
|
+
console.error('[MAMA] Failed to fetch checkpoints:', error);
|
|
847
|
+
document.getElementById('checkpoint-list').innerHTML =
|
|
848
|
+
`<div class="loading-checkpoints" style="color:#f66">Failed to load: ${error.message}</div>`;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Render checkpoints list
|
|
853
|
+
function renderCheckpoints() {
|
|
854
|
+
const container = document.getElementById('checkpoint-list');
|
|
855
|
+
|
|
856
|
+
if (checkpointsData.length === 0) {
|
|
857
|
+
container.innerHTML = '<div class="loading-checkpoints">No checkpoints found</div>';
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const html = checkpointsData
|
|
862
|
+
.map(
|
|
863
|
+
(cp, idx) => `
|
|
864
|
+
<div class="checkpoint-item" onclick="expandCheckpoint(${idx})">
|
|
865
|
+
<div class="checkpoint-time">${formatCheckpointTime(cp.timestamp)}</div>
|
|
866
|
+
<div class="checkpoint-summary">${escapeHtml(extractFirstLine(cp.summary))}</div>
|
|
867
|
+
<div class="checkpoint-details">
|
|
868
|
+
${
|
|
869
|
+
cp.summary
|
|
870
|
+
? `
|
|
871
|
+
<div class="checkpoint-section">
|
|
872
|
+
<div class="checkpoint-section-title">Summary</div>
|
|
873
|
+
<div class="checkpoint-section-content">${escapeHtml(cp.summary)}</div>
|
|
874
|
+
</div>
|
|
875
|
+
`
|
|
876
|
+
: ''
|
|
877
|
+
}
|
|
878
|
+
${
|
|
879
|
+
cp.next_steps
|
|
880
|
+
? `
|
|
881
|
+
<div class="checkpoint-section">
|
|
882
|
+
<div class="checkpoint-section-title">Next Steps</div>
|
|
883
|
+
<div class="checkpoint-section-content">${escapeHtml(cp.next_steps)}</div>
|
|
884
|
+
</div>
|
|
885
|
+
`
|
|
886
|
+
: ''
|
|
887
|
+
}
|
|
888
|
+
${
|
|
889
|
+
cp.open_files && cp.open_files.length > 0
|
|
890
|
+
? `
|
|
891
|
+
<div class="checkpoint-section">
|
|
892
|
+
<div class="checkpoint-section-title">Open Files</div>
|
|
893
|
+
<div class="checkpoint-files">
|
|
894
|
+
${cp.open_files.map((f) => `<span class="checkpoint-file">${escapeHtml(f.split('/').pop())}</span>`).join('')}
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
`
|
|
898
|
+
: ''
|
|
899
|
+
}
|
|
900
|
+
${renderRelatedDecisions(cp.summary)}
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
`
|
|
904
|
+
)
|
|
905
|
+
.join('');
|
|
906
|
+
|
|
907
|
+
container.innerHTML = html;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Format checkpoint timestamp
|
|
911
|
+
function formatCheckpointTime(timestamp) {
|
|
912
|
+
const date = new Date(timestamp);
|
|
913
|
+
const now = new Date();
|
|
914
|
+
const diff = now - date;
|
|
915
|
+
|
|
916
|
+
if (diff < 3600000) {
|
|
917
|
+
const mins = Math.floor(diff / 60000);
|
|
918
|
+
return `${mins}m ago`;
|
|
919
|
+
}
|
|
920
|
+
if (diff < 86400000) {
|
|
921
|
+
const hours = Math.floor(diff / 3600000);
|
|
922
|
+
return `${hours}h ago`;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return (
|
|
926
|
+
date.toLocaleDateString() +
|
|
927
|
+
' ' +
|
|
928
|
+
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Extract first meaningful line from summary
|
|
933
|
+
function extractFirstLine(summary) {
|
|
934
|
+
if (!summary) {
|
|
935
|
+
return 'No summary';
|
|
936
|
+
}
|
|
937
|
+
const lines = summary.split('\n').filter((l) => l.trim() && !l.startsWith('**'));
|
|
938
|
+
return lines[0] || summary.substring(0, 100);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Extract and render related decisions from summary
|
|
942
|
+
function renderRelatedDecisions(summary) {
|
|
943
|
+
if (!summary) {
|
|
944
|
+
return '';
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Match patterns like "decision_xxx" or "Related decisions: xxx, yyy"
|
|
948
|
+
const decisionPattern = /decision_[a-z0-9_]+/gi;
|
|
949
|
+
const matches = summary.match(decisionPattern);
|
|
950
|
+
|
|
951
|
+
if (!matches || matches.length === 0) {
|
|
952
|
+
return '';
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const uniqueDecisions = [...new Set(matches)];
|
|
956
|
+
|
|
957
|
+
return `
|
|
958
|
+
<div class="checkpoint-section">
|
|
959
|
+
<div class="checkpoint-section-title">Related Decisions</div>
|
|
960
|
+
<div class="checkpoint-related">
|
|
961
|
+
${uniqueDecisions.map((d) => `<span class="checkpoint-related-link" onclick="event.stopPropagation(); navigateToDecision('${d}')">${d.substring(9, 30)}...</span>`).join('')}
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Expand/collapse checkpoint item
|
|
968
|
+
// eslint-disable-next-line no-unused-vars
|
|
969
|
+
function expandCheckpoint(idx) {
|
|
970
|
+
const items = document.querySelectorAll('.checkpoint-item');
|
|
971
|
+
items.forEach((item, i) => {
|
|
972
|
+
if (i === idx) {
|
|
973
|
+
item.classList.toggle('expanded');
|
|
974
|
+
} else {
|
|
975
|
+
item.classList.remove('expanded');
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Navigate to a decision in the graph (from checkpoint related link)
|
|
981
|
+
// eslint-disable-next-line no-unused-vars
|
|
982
|
+
function navigateToDecision(decisionId) {
|
|
983
|
+
// Close checkpoint panel
|
|
984
|
+
document.getElementById('checkpoint-panel').classList.remove('visible');
|
|
985
|
+
|
|
986
|
+
// Use existing navigateToNode function
|
|
987
|
+
navigateToNode(decisionId);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Make detail panel draggable
|
|
991
|
+
function initDraggablePanel() {
|
|
992
|
+
const panel = document.getElementById('detail-panel');
|
|
993
|
+
const header = panel.querySelector('h3');
|
|
994
|
+
let isDragging = false;
|
|
995
|
+
let offsetX, offsetY;
|
|
996
|
+
|
|
997
|
+
header.addEventListener('mousedown', (e) => {
|
|
998
|
+
isDragging = true;
|
|
999
|
+
offsetX = e.clientX - panel.offsetLeft;
|
|
1000
|
+
offsetY = e.clientY - panel.offsetTop;
|
|
1001
|
+
panel.style.transition = 'none';
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
document.addEventListener('mousemove', (e) => {
|
|
1005
|
+
if (!isDragging) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const x = Math.max(0, Math.min(e.clientX - offsetX, window.innerWidth - panel.offsetWidth));
|
|
1009
|
+
const y = Math.max(50, Math.min(e.clientY - offsetY, window.innerHeight - panel.offsetHeight));
|
|
1010
|
+
panel.style.left = x + 'px';
|
|
1011
|
+
panel.style.top = y + 'px';
|
|
1012
|
+
panel.style.right = 'auto';
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
document.addEventListener('mouseup', () => {
|
|
1016
|
+
isDragging = false;
|
|
1017
|
+
panel.style.transition = '';
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
825
1021
|
// Initialize on page load
|
|
826
1022
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
1023
|
+
// Initialize draggable panel
|
|
1024
|
+
initDraggablePanel();
|
|
1025
|
+
|
|
1026
|
+
// Load checkpoints (panel is visible by default)
|
|
1027
|
+
fetchCheckpoints();
|
|
1028
|
+
|
|
827
1029
|
try {
|
|
828
1030
|
const data = await fetchGraphData();
|
|
829
1031
|
if (data.nodes.length === 0) {
|