wikimem 0.8.0 → 0.8.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/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +97 -8
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/connectors.d.ts +1 -1
- package/dist/core/connectors.d.ts.map +1 -1
- package/dist/core/git.d.ts +1 -1
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js.map +1 -1
- package/dist/core/ingest.d.ts.map +1 -1
- package/dist/core/ingest.js +74 -3
- package/dist/core/ingest.js.map +1 -1
- package/dist/core/lint.d.ts.map +1 -1
- package/dist/core/lint.js +23 -4
- package/dist/core/lint.js.map +1 -1
- package/dist/core/oauth-defaults.d.ts +31 -0
- package/dist/core/oauth-defaults.d.ts.map +1 -0
- package/dist/core/oauth-defaults.js +77 -0
- package/dist/core/oauth-defaults.js.map +1 -0
- package/dist/core/observer.d.ts +24 -1
- package/dist/core/observer.d.ts.map +1 -1
- package/dist/core/observer.js +146 -4
- package/dist/core/observer.js.map +1 -1
- package/dist/core/sync/gdrive.d.ts +14 -0
- package/dist/core/sync/gdrive.d.ts.map +1 -0
- package/dist/core/sync/gdrive.js +205 -0
- package/dist/core/sync/gdrive.js.map +1 -0
- package/dist/core/sync/github.d.ts +20 -0
- package/dist/core/sync/github.d.ts.map +1 -0
- package/dist/core/sync/github.js +206 -0
- package/dist/core/sync/github.js.map +1 -0
- package/dist/core/sync/gmail.d.ts +15 -0
- package/dist/core/sync/gmail.d.ts.map +1 -0
- package/dist/core/sync/gmail.js +159 -0
- package/dist/core/sync/gmail.js.map +1 -0
- package/dist/core/sync/index.d.ts +47 -0
- package/dist/core/sync/index.d.ts.map +1 -0
- package/dist/core/sync/index.js +100 -0
- package/dist/core/sync/index.js.map +1 -0
- package/dist/core/sync/jira.d.ts +15 -0
- package/dist/core/sync/jira.d.ts.map +1 -0
- package/dist/core/sync/jira.js +176 -0
- package/dist/core/sync/jira.js.map +1 -0
- package/dist/core/sync/linear.d.ts +15 -0
- package/dist/core/sync/linear.d.ts.map +1 -0
- package/dist/core/sync/linear.js +111 -0
- package/dist/core/sync/linear.js.map +1 -0
- package/dist/core/sync/notion.d.ts +14 -0
- package/dist/core/sync/notion.d.ts.map +1 -0
- package/dist/core/sync/notion.js +168 -0
- package/dist/core/sync/notion.js.map +1 -0
- package/dist/core/sync/rss.d.ts +20 -0
- package/dist/core/sync/rss.d.ts.map +1 -0
- package/dist/core/sync/rss.js +165 -0
- package/dist/core/sync/rss.js.map +1 -0
- package/dist/core/sync/scheduler.d.ts +31 -0
- package/dist/core/sync/scheduler.d.ts.map +1 -0
- package/dist/core/sync/scheduler.js +129 -0
- package/dist/core/sync/scheduler.js.map +1 -0
- package/dist/core/sync/slack.d.ts +16 -0
- package/dist/core/sync/slack.d.ts.map +1 -0
- package/dist/core/sync/slack.js +173 -0
- package/dist/core/sync/slack.js.map +1 -0
- package/dist/core/vault.d.ts +22 -0
- package/dist/core/vault.d.ts.map +1 -1
- package/dist/core/vault.js +65 -0
- package/dist/core/vault.js.map +1 -1
- package/dist/core/webhooks.d.ts +13 -0
- package/dist/core/webhooks.d.ts.map +1 -0
- package/dist/core/webhooks.js +206 -0
- package/dist/core/webhooks.js.map +1 -0
- package/dist/mcp-server.d.ts +11 -6
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +99 -6
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools-extended.d.ts +15 -0
- package/dist/mcp-tools-extended.d.ts.map +1 -0
- package/dist/mcp-tools-extended.js +277 -0
- package/dist/mcp-tools-extended.js.map +1 -0
- package/dist/processors/csv.d.ts +18 -0
- package/dist/processors/csv.d.ts.map +1 -0
- package/dist/processors/csv.js +230 -0
- package/dist/processors/csv.js.map +1 -0
- package/dist/processors/image.d.ts.map +1 -1
- package/dist/processors/image.js +55 -27
- package/dist/processors/image.js.map +1 -1
- package/dist/processors/pdf.d.ts.map +1 -1
- package/dist/processors/pdf.js +5 -1
- package/dist/processors/pdf.js.map +1 -1
- package/dist/processors/pptx.d.ts +3 -1
- package/dist/processors/pptx.d.ts.map +1 -1
- package/dist/processors/pptx.js +236 -95
- package/dist/processors/pptx.js.map +1 -1
- package/dist/processors/xlsx.d.ts +2 -0
- package/dist/processors/xlsx.d.ts.map +1 -1
- package/dist/processors/xlsx.js +182 -46
- package/dist/processors/xlsx.js.map +1 -1
- package/dist/templates/source-types.d.ts +33 -0
- package/dist/templates/source-types.d.ts.map +1 -0
- package/dist/templates/source-types.js +178 -0
- package/dist/templates/source-types.js.map +1 -0
- package/dist/web/public/index.html +1785 -103
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +746 -38
- package/dist/web/server.js.map +1 -1
- package/package.json +4 -1
- package/src/web/public/index.html +1785 -103
- package/templates/source-types/article.md +21 -0
- package/templates/source-types/book.md +21 -0
- package/templates/source-types/paper.md +23 -0
- package/templates/source-types/podcast.md +21 -0
- package/templates/source-types/raw-notes.md +17 -0
- package/templates/source-types/tweet-thread.md +19 -0
- package/templates/source-types/video.md +21 -0
|
@@ -368,13 +368,17 @@
|
|
|
368
368
|
.tree-chevron.open { transform: rotate(90deg); }
|
|
369
369
|
|
|
370
370
|
.tree-icon {
|
|
371
|
-
width:
|
|
372
|
-
min-width:
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
width: 16px;
|
|
372
|
+
min-width: 16px;
|
|
373
|
+
height: 16px;
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: center;
|
|
375
377
|
color: var(--text-muted);
|
|
376
|
-
margin-right:
|
|
378
|
+
margin-right: 5px;
|
|
379
|
+
flex-shrink: 0;
|
|
377
380
|
}
|
|
381
|
+
.tree-icon svg { width: 14px; height: 14px; }
|
|
378
382
|
.tree-item.active .tree-icon { color: var(--accent); }
|
|
379
383
|
|
|
380
384
|
.tree-label {
|
|
@@ -548,6 +552,9 @@
|
|
|
548
552
|
}
|
|
549
553
|
.tab:hover .tab-close, .tab.active .tab-close { opacity: 1; }
|
|
550
554
|
.tab-close:hover { background: var(--bg-hover); color: var(--text); }
|
|
555
|
+
.tab-drag-over { background: var(--accent-dim); border-left: 2px solid var(--accent); }
|
|
556
|
+
.tab[draggable] { cursor: grab; }
|
|
557
|
+
.tab[draggable]:active { cursor: grabbing; }
|
|
551
558
|
|
|
552
559
|
#tab-new {
|
|
553
560
|
width: 28px; height: 28px;
|
|
@@ -685,6 +692,62 @@
|
|
|
685
692
|
transition: border-color 0.2s;
|
|
686
693
|
}
|
|
687
694
|
.stat-card:hover { border-color: var(--border); }
|
|
695
|
+
|
|
696
|
+
/* Health badges */
|
|
697
|
+
.health-badge {
|
|
698
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
699
|
+
padding: 6px 12px; border-radius: var(--radius-md);
|
|
700
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
701
|
+
font-size: 12px; color: var(--text-secondary); transition: border-color 0.15s;
|
|
702
|
+
}
|
|
703
|
+
.health-badge:hover { border-color: var(--accent); }
|
|
704
|
+
.health-badge-icon { font-size: 14px; }
|
|
705
|
+
.health-badge-count { font-weight: 600; color: var(--text-bright); }
|
|
706
|
+
.health-badge-label { color: var(--text-dim); }
|
|
707
|
+
.health-badge.warning { border-color: rgba(215,186,125,0.3); background: rgba(215,186,125,0.06); }
|
|
708
|
+
.health-badge.danger { border-color: rgba(241,76,76,0.3); background: rgba(241,76,76,0.06); }
|
|
709
|
+
|
|
710
|
+
/* Conflict card */
|
|
711
|
+
.conflict-card {
|
|
712
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
713
|
+
border-radius: var(--radius-md); padding: 14px; margin-bottom: 10px;
|
|
714
|
+
}
|
|
715
|
+
.conflict-card-header {
|
|
716
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
717
|
+
margin-bottom: 10px;
|
|
718
|
+
}
|
|
719
|
+
.conflict-reason {
|
|
720
|
+
font-size: 11px; color: var(--amber); font-style: italic; margin-bottom: 10px;
|
|
721
|
+
}
|
|
722
|
+
.conflict-sides {
|
|
723
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;
|
|
724
|
+
}
|
|
725
|
+
.conflict-side {
|
|
726
|
+
background: var(--bg-surface); border: 1px solid var(--border-subtle);
|
|
727
|
+
border-radius: var(--radius-sm); padding: 10px; font-size: 12px;
|
|
728
|
+
}
|
|
729
|
+
.conflict-side-title {
|
|
730
|
+
font-weight: 600; color: var(--text-bright); margin-bottom: 6px;
|
|
731
|
+
cursor: pointer;
|
|
732
|
+
}
|
|
733
|
+
.conflict-side-title:hover { color: var(--accent); }
|
|
734
|
+
.conflict-side-summary { color: var(--text-secondary); line-height: 1.5; }
|
|
735
|
+
.conflict-side-snippet {
|
|
736
|
+
margin-top: 6px; font-size: 11px; color: var(--text-dim);
|
|
737
|
+
border-top: 1px solid var(--border-subtle); padding-top: 6px;
|
|
738
|
+
max-height: 80px; overflow: hidden;
|
|
739
|
+
}
|
|
740
|
+
.conflict-actions {
|
|
741
|
+
display: flex; gap: 6px; flex-wrap: wrap;
|
|
742
|
+
}
|
|
743
|
+
.conflict-btn {
|
|
744
|
+
padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border);
|
|
745
|
+
background: var(--bg-surface); color: var(--text-secondary);
|
|
746
|
+
font-size: 11px; cursor: pointer; transition: all 0.15s;
|
|
747
|
+
}
|
|
748
|
+
.conflict-btn:hover { border-color: var(--accent); color: var(--text-bright); }
|
|
749
|
+
.conflict-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
750
|
+
.conflict-btn.primary:hover { opacity: 0.9; }
|
|
688
751
|
.stat-value {
|
|
689
752
|
font-size: 22px;
|
|
690
753
|
font-weight: 600;
|
|
@@ -769,6 +832,29 @@
|
|
|
769
832
|
text-wrap: balance;
|
|
770
833
|
}
|
|
771
834
|
|
|
835
|
+
/* ── Fact Timeline (COMP-MP-002 temporal reasoning) ── */
|
|
836
|
+
#page-fact-timeline { margin: 12px 0 18px; }
|
|
837
|
+
.fact-timeline-toggle { display:flex; align-items:center; gap:6px; cursor:pointer; color:var(--text-muted); font-size:12px; user-select:none; padding:4px 0; }
|
|
838
|
+
.fact-timeline-toggle:hover { color:var(--text-primary); }
|
|
839
|
+
.fact-timeline-toggle .ft-chevron { transition:transform 0.15s; font-size:10px; }
|
|
840
|
+
.fact-timeline-toggle .ft-chevron.open { transform:rotate(90deg); }
|
|
841
|
+
.fact-timeline-body { overflow:hidden; max-height:0; transition:max-height 0.25s ease; }
|
|
842
|
+
.fact-timeline-body.open { max-height:600px; overflow-y:auto; }
|
|
843
|
+
.ft-entries { position:relative; padding-left:20px; margin:8px 0 0; }
|
|
844
|
+
.ft-entries::before { content:''; position:absolute; left:6px; top:0; bottom:0; width:2px; background:var(--border-subtle); border-radius:1px; }
|
|
845
|
+
.ft-entry { position:relative; padding:6px 0 6px 12px; font-size:12px; line-height:1.5; }
|
|
846
|
+
.ft-entry::before { content:''; position:absolute; left:-17px; top:12px; width:8px; height:8px; border-radius:50%; border:2px solid var(--accent); background:var(--bg-surface); }
|
|
847
|
+
.ft-entry.ft-current::before { background:var(--accent); }
|
|
848
|
+
.ft-entry-header { display:flex; align-items:center; gap:8px; }
|
|
849
|
+
.ft-version-badge { background:var(--accent); color:#fff; font-size:10px; padding:1px 6px; border-radius:8px; font-weight:600; }
|
|
850
|
+
.ft-version-badge.ft-old { background:var(--border-subtle); color:var(--text-muted); }
|
|
851
|
+
.ft-timestamp { color:var(--text-muted); font-size:11px; }
|
|
852
|
+
.ft-actor { color:var(--text-muted); font-size:11px; font-style:italic; }
|
|
853
|
+
.ft-source { color:var(--text-muted); font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:300px; }
|
|
854
|
+
.ft-diff-btn { font-size:11px; color:var(--accent); background:none; border:1px solid var(--border-subtle); border-radius:4px; padding:1px 6px; cursor:pointer; margin-left:auto; }
|
|
855
|
+
.ft-diff-btn:hover { border-color:var(--accent); }
|
|
856
|
+
.ft-diff-preview { background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:6px; padding:8px 10px; margin:4px 0; font-size:11px; color:var(--text-muted); max-height:120px; overflow-y:auto; white-space:pre-wrap; word-break:break-word; }
|
|
857
|
+
|
|
772
858
|
/* ── Metadata Properties Panel (Obsidian-style) ── */
|
|
773
859
|
#page-meta {
|
|
774
860
|
border-bottom: 1px solid var(--border-subtle);
|
|
@@ -1067,6 +1153,21 @@
|
|
|
1067
1153
|
.st-btn code { font-size: 11px; font-family: inherit; }
|
|
1068
1154
|
.st-divider { width: 1px; height: 18px; background: var(--border-subtle, var(--border)); margin: 0 2px; }
|
|
1069
1155
|
|
|
1156
|
+
/* ── @Mention Autocomplete (ENT-002) ── */
|
|
1157
|
+
#mention-popup {
|
|
1158
|
+
position: fixed; z-index: 9998; display: none;
|
|
1159
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
1160
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.35); max-height: 200px; overflow-y: auto;
|
|
1161
|
+
min-width: 200px; max-width: 320px; padding: 4px;
|
|
1162
|
+
}
|
|
1163
|
+
#mention-popup.visible { display: block; }
|
|
1164
|
+
.mention-item {
|
|
1165
|
+
padding: 6px 12px; font-size: 12px; color: var(--text-secondary); cursor: pointer;
|
|
1166
|
+
border-radius: 5px; transition: background 0.08s; display: flex; align-items: center; gap: 8px;
|
|
1167
|
+
}
|
|
1168
|
+
.mention-item:hover, .mention-item.active { background: var(--bg-hover); color: var(--text-bright); }
|
|
1169
|
+
.mention-item-type { font-size: 9px; padding: 1px 6px; border-radius: 3px; background: var(--accent-dim); color: var(--accent); font-weight: 600; text-transform: uppercase; }
|
|
1170
|
+
|
|
1070
1171
|
/* ── Entity / concept profile (UX-031) ── */
|
|
1071
1172
|
#page-entity-profile:empty { display: none; }
|
|
1072
1173
|
#page-entity-profile:not(:empty) { margin: 0 0 8px; }
|
|
@@ -1294,6 +1395,23 @@
|
|
|
1294
1395
|
.wiki-source-link:hover { text-decoration: underline; text-underline-offset: 2px; color: var(--wiki-link-hover); }
|
|
1295
1396
|
.wiki-see-also-list { margin: 0; padding-left: 1.25em; list-style: disc; color: var(--text-secondary); font-size: 14px; }
|
|
1296
1397
|
.wiki-see-also-list li { margin: 5px 0; }
|
|
1398
|
+
/* Similar Pages section */
|
|
1399
|
+
.similar-pages-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
1400
|
+
.similar-page-card { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius); cursor: pointer; font-size: 13px; color: var(--text-secondary); transition: border-color 0.15s, background 0.15s; max-width: 280px; }
|
|
1401
|
+
.similar-page-card:hover { border-color: var(--accent); background: var(--accent-dim); color: var(--text-bright); }
|
|
1402
|
+
.similar-page-card .sim-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
|
|
1403
|
+
.similar-page-card .sim-score { font-size: 11px; color: var(--text-muted); flex-shrink: 0; font-variant-numeric: tabular-nums; }
|
|
1404
|
+
.similar-page-card .sim-tags { display: flex; gap: 3px; flex-shrink: 0; }
|
|
1405
|
+
.similar-page-card .sim-tag { font-size: 10px; padding: 1px 5px; border-radius: 3px; background: var(--accent-dim); color: var(--accent); }
|
|
1406
|
+
/* Search filters */
|
|
1407
|
+
.search-filters { display: flex; gap: 6px; padding: 4px 12px 6px; flex-wrap: wrap; align-items: center; border-top: 1px solid var(--border-subtle); }
|
|
1408
|
+
.search-filter-chip { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--bg-card); border: 1px solid var(--border-subtle); color: var(--text-secondary); cursor: pointer; transition: all 0.15s; white-space: nowrap; }
|
|
1409
|
+
.search-filter-chip:hover { border-color: var(--accent); color: var(--text-bright); }
|
|
1410
|
+
.search-filter-chip.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }
|
|
1411
|
+
.search-filter-select { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--bg-card); border: 1px solid var(--border-subtle); color: var(--text-secondary); outline: none; }
|
|
1412
|
+
.search-filter-select:focus { border-color: var(--accent); }
|
|
1413
|
+
/* Search snippet highlight */
|
|
1414
|
+
#search-results mark, .search-result mark { background: rgba(79,158,255,0.2); color: var(--text-bright); border-radius: 2px; padding: 0 1px; }
|
|
1297
1415
|
.page-article-footer {
|
|
1298
1416
|
margin-top: 32px;
|
|
1299
1417
|
padding-top: 14px;
|
|
@@ -1338,6 +1456,36 @@
|
|
|
1338
1456
|
#page-title-row #page-title { flex: 1; min-width: 0; }
|
|
1339
1457
|
#page-edit-btn { display: none !important; }
|
|
1340
1458
|
|
|
1459
|
+
/* ── Confidence Badge ── */
|
|
1460
|
+
.confidence-badge {
|
|
1461
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1462
|
+
font-size: 11px; font-weight: 500; padding: 2px 8px;
|
|
1463
|
+
border-radius: 10px; white-space: nowrap; flex-shrink: 0;
|
|
1464
|
+
margin-top: 8px; line-height: 1;
|
|
1465
|
+
}
|
|
1466
|
+
.confidence-badge.high { background: var(--green-dim); color: var(--green); }
|
|
1467
|
+
.confidence-badge.medium { background: rgba(215,186,125,0.12); color: var(--amber); }
|
|
1468
|
+
.confidence-badge.low { background: var(--red-dim); color: var(--red); }
|
|
1469
|
+
.confidence-badge svg { width: 10px; height: 10px; }
|
|
1470
|
+
|
|
1471
|
+
/* ── Validation Controls ── */
|
|
1472
|
+
.validation-bar {
|
|
1473
|
+
display: flex; align-items: center; gap: 6px; margin-top: 4px; flex-wrap: wrap;
|
|
1474
|
+
}
|
|
1475
|
+
.validation-btn {
|
|
1476
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1477
|
+
font-size: 11px; padding: 3px 10px; border-radius: 4px;
|
|
1478
|
+
border: 1px solid var(--border-subtle); background: transparent;
|
|
1479
|
+
color: var(--text-dim); cursor: pointer; transition: all 0.12s;
|
|
1480
|
+
}
|
|
1481
|
+
.validation-btn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--border); }
|
|
1482
|
+
.validation-btn.active-verified { background: var(--green-dim); color: var(--green); border-color: var(--green); }
|
|
1483
|
+
.validation-btn.active-outdated { background: rgba(215,186,125,0.12); color: var(--amber); border-color: var(--amber); }
|
|
1484
|
+
.validation-btn.active-wrong { background: var(--red-dim); color: var(--red); border-color: var(--red); }
|
|
1485
|
+
.validation-status-label {
|
|
1486
|
+
font-size: 10px; color: var(--text-muted); margin-left: 4px;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1341
1489
|
/* ── WYSIWYG Inline Editor (Obsidian-style) ── */
|
|
1342
1490
|
#page-editor { display: none; } /* kept for compatibility but not used */
|
|
1343
1491
|
#page-body[contenteditable="true"] {
|
|
@@ -2132,6 +2280,63 @@
|
|
|
2132
2280
|
.conn-modal-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; }
|
|
2133
2281
|
.btn-danger:hover { background:var(--red-dim)!important; color:var(--red)!important; }
|
|
2134
2282
|
|
|
2283
|
+
/* ── OAuth Connector Cards (v2 — clean one-click) ── */
|
|
2284
|
+
.oauth-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
|
2285
|
+
@media (max-width:600px) { .oauth-grid { grid-template-columns:1fr; } }
|
|
2286
|
+
.oauth-card { background:var(--bg-card); border:1px solid var(--border); border-radius:10px; padding:16px; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s, box-shadow 0.2s; }
|
|
2287
|
+
.oauth-card:hover { border-color:rgba(79,158,255,0.3); }
|
|
2288
|
+
.oauth-card.oauth-connected { border-color:rgba(78,201,176,0.25); }
|
|
2289
|
+
.oauth-card-top { display:flex; align-items:center; gap:10px; }
|
|
2290
|
+
.oauth-card-icon { width:36px; height:36px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
|
2291
|
+
.oauth-card-icon.github { background:rgba(255,255,255,0.08); color:#e0e0e0; }
|
|
2292
|
+
.oauth-card-icon.slack { background:rgba(74,21,75,0.15); color:#e0e0e0; }
|
|
2293
|
+
.oauth-card-icon.google { background:rgba(66,133,244,0.1); }
|
|
2294
|
+
.oauth-card-icon.linear { background:rgba(99,91,255,0.1); color:#635BFF; }
|
|
2295
|
+
.oauth-card-icon.jira { background:rgba(0,82,204,0.1); color:#0052CC; }
|
|
2296
|
+
.oauth-card-info { flex:1; min-width:0; }
|
|
2297
|
+
.oauth-card-name { font-size:13px; font-weight:600; color:var(--text-bright); }
|
|
2298
|
+
.oauth-card-desc { font-size:11px; color:var(--text-muted); margin-top:1px; line-height:1.3; }
|
|
2299
|
+
.oauth-card-badge { font-size:10px; padding:2px 8px; border-radius:10px; font-weight:600; flex-shrink:0; letter-spacing:0.2px; }
|
|
2300
|
+
.oauth-card-badge.connected { background:rgba(78,201,176,0.12); color:#4ec9b0; }
|
|
2301
|
+
.oauth-card-badge.ready { background:rgba(79,158,255,0.12); color:#4f9eff; }
|
|
2302
|
+
.oauth-card-actions { display:flex; gap:8px; align-items:center; }
|
|
2303
|
+
.oauth-connect-btn { flex:1; padding:8px 16px; border:none; border-radius:var(--radius-md); font-family:var(--font); font-size:12px; font-weight:600; cursor:pointer; transition:opacity 0.15s, transform 0.1s; text-align:center; }
|
|
2304
|
+
.oauth-connect-btn:hover { opacity:0.9; }
|
|
2305
|
+
.oauth-connect-btn:active { transform:scale(0.98); }
|
|
2306
|
+
.oauth-connect-btn.primary { background:var(--accent); color:var(--bg-deep); }
|
|
2307
|
+
.oauth-connect-btn.secondary { background:var(--bg-surface); color:var(--text); border:1px solid var(--border); }
|
|
2308
|
+
.oauth-connect-btn.danger { background:transparent; color:var(--red); border:1px solid rgba(241,76,76,0.25); font-weight:500; flex:0; padding:8px 12px; }
|
|
2309
|
+
.oauth-connect-btn.danger:hover { background:rgba(241,76,76,0.08); }
|
|
2310
|
+
.oauth-connect-btn:disabled { opacity:0.5; cursor:not-allowed; transform:none; }
|
|
2311
|
+
.oauth-connected-meta { font-size:11px; color:var(--text-muted); }
|
|
2312
|
+
.oauth-info-tip { font-size:11px; color:var(--text-muted); line-height:1.4; padding:8px 10px; background:var(--bg-surface); border-radius:var(--radius); }
|
|
2313
|
+
.oauth-info-tip a { color:var(--accent); text-decoration:none; }
|
|
2314
|
+
.oauth-info-tip a:hover { text-decoration:underline; }
|
|
2315
|
+
/* Device flow UI */
|
|
2316
|
+
.device-flow-box { background:var(--bg-surface); border:1px solid var(--border); border-radius:8px; padding:14px; text-align:center; }
|
|
2317
|
+
.device-flow-code { font-family:var(--font-mono); font-size:22px; font-weight:700; color:var(--text-bright); letter-spacing:3px; padding:8px 0; user-select:all; }
|
|
2318
|
+
.device-flow-steps { font-size:12px; color:var(--text-secondary); line-height:1.6; }
|
|
2319
|
+
.device-flow-steps a { color:var(--accent); text-decoration:none; font-weight:500; }
|
|
2320
|
+
.device-flow-steps a:hover { text-decoration:underline; }
|
|
2321
|
+
.device-flow-status { font-size:11px; color:var(--text-muted); margin-top:8px; display:flex; align-items:center; justify-content:center; gap:6px; }
|
|
2322
|
+
.device-flow-spinner { width:12px; height:12px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:df-spin 0.8s linear infinite; display:inline-block; }
|
|
2323
|
+
@keyframes df-spin { to { transform:rotate(360deg); } }
|
|
2324
|
+
/* Advanced setup collapsible */
|
|
2325
|
+
.oauth-advanced-toggle { display:flex; align-items:center; gap:6px; padding:10px 0 6px; cursor:pointer; font-size:12px; color:var(--text-muted); font-weight:500; border:none; background:none; font-family:var(--font); }
|
|
2326
|
+
.oauth-advanced-toggle:hover { color:var(--text-secondary); }
|
|
2327
|
+
.oauth-advanced-toggle svg { transition:transform 0.2s; }
|
|
2328
|
+
.oauth-advanced-toggle.open svg { transform:rotate(90deg); }
|
|
2329
|
+
.oauth-advanced-body { display:none; padding:12px 0; }
|
|
2330
|
+
.oauth-advanced-body.open { display:block; }
|
|
2331
|
+
.oauth-setup-steps { margin:8px 0; padding:0; list-style:none; counter-reset:step; }
|
|
2332
|
+
.oauth-setup-steps li { position:relative; padding:6px 0 6px 28px; font-size:11px; color:var(--text-secondary); line-height:1.5; }
|
|
2333
|
+
.oauth-setup-steps li::before { counter-increment:step; content:counter(step); position:absolute; left:0; top:6px; width:20px; height:20px; border-radius:50%; background:var(--bg); border:1px solid var(--border); display:flex; align-items:center; justify-content:center; font-size:9px; font-weight:600; color:var(--text-muted); }
|
|
2334
|
+
.oauth-setup-steps code { background:var(--bg); border:1px solid var(--border); border-radius:3px; padding:1px 4px; font-size:10px; color:var(--accent); }
|
|
2335
|
+
.oauth-input-row { display:flex; gap:8px; margin-top:8px; }
|
|
2336
|
+
.oauth-input-row .settings-input { flex:1; font-size:12px; }
|
|
2337
|
+
.oauth-actions { display:flex; gap:8px; margin-top:10px; align-items:center; }
|
|
2338
|
+
.oauth-actions .settings-status { font-size:11px; }
|
|
2339
|
+
|
|
2135
2340
|
.pipe-history h3 { font-size:13px; font-weight:600; color:var(--text-secondary); margin-bottom:12px; text-transform:uppercase; letter-spacing:0.5px; }
|
|
2136
2341
|
.pipe-run-item { background:var(--bg-card); border:1px solid var(--border); border-radius:10px; padding:12px 16px; margin-bottom:8px; cursor:pointer; transition:border-color .15s, background .15s; }
|
|
2137
2342
|
.pipe-run-item:hover { border-color:var(--accent); background:var(--bg-hover); }
|
|
@@ -2337,6 +2542,21 @@
|
|
|
2337
2542
|
.btn-secondary { background:var(--bg-card); border:1px solid var(--border); color:var(--text); padding:7px 16px; border-radius:var(--radius-md); font-size:13px; cursor:pointer; }
|
|
2338
2543
|
.btn-secondary:hover { background:var(--bg-hover); border-color:var(--accent); }
|
|
2339
2544
|
|
|
2545
|
+
/* ── Contradiction compare (Observer / SUP-004) ── */
|
|
2546
|
+
.cx-modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.65); z-index:210; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(2px); }
|
|
2547
|
+
.cx-modal { background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-lg); width:min(960px,96vw); max-height:88vh; display:flex; flex-direction:column; box-shadow:0 16px 48px rgba(0,0,0,0.55); }
|
|
2548
|
+
.cx-modal-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:14px 18px; border-bottom:1px solid var(--border); }
|
|
2549
|
+
.cx-modal-header h3 { font-size:14px; font-weight:600; margin:0; line-height:1.35; }
|
|
2550
|
+
.cx-modal-reason { font-size:12px; color:var(--text-secondary); margin-top:6px; line-height:1.45; }
|
|
2551
|
+
.cx-modal-body { display:flex; flex:1; min-height:0; gap:0; }
|
|
2552
|
+
.cx-col { flex:1; min-width:0; display:flex; flex-direction:column; border-right:1px solid var(--border); }
|
|
2553
|
+
.cx-col:last-child { border-right:none; }
|
|
2554
|
+
.cx-col-head { font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-dim); padding:8px 12px; background:var(--bg-surface); border-bottom:1px solid var(--border); }
|
|
2555
|
+
.cx-col-body { padding:12px; overflow:auto; flex:1; font-family:var(--font-mono); font-size:11px; line-height:1.5; color:var(--text-secondary); white-space:pre-wrap; word-break:break-word; }
|
|
2556
|
+
.cx-row { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; padding:8px 0; border-bottom:1px solid var(--border-subtle); font-size:12px; }
|
|
2557
|
+
.cx-row:last-child { border-bottom:none; }
|
|
2558
|
+
.cx-row-reason { color:var(--text-muted); flex:1; line-height:1.4; }
|
|
2559
|
+
|
|
2340
2560
|
/* ── History toolbar additions ── */
|
|
2341
2561
|
.history-toolbar-right { display:flex; gap:8px; margin-left:auto; }
|
|
2342
2562
|
.btn-accent-outline { background:transparent; border:1px solid var(--accent); color:var(--accent); padding:5px 14px; border-radius:var(--radius-md); cursor:pointer; font-size:12px; font-weight:500; }
|
|
@@ -2492,6 +2712,9 @@
|
|
|
2492
2712
|
<button class="sidebar-action-btn" id="sa-new-note" title="New Note">
|
|
2493
2713
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
|
2494
2714
|
</button>
|
|
2715
|
+
<button class="sidebar-action-btn" id="sa-new-folder" title="New Folder">
|
|
2716
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>
|
|
2717
|
+
</button>
|
|
2495
2718
|
<button class="sidebar-action-btn" id="sa-collapse" title="Collapse All">
|
|
2496
2719
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
|
2497
2720
|
</button>
|
|
@@ -2613,7 +2836,21 @@
|
|
|
2613
2836
|
</div>
|
|
2614
2837
|
<div id="url-result" style="margin-top:8px;font-size:12px;color:var(--text-dim);display:none"></div>
|
|
2615
2838
|
</div>
|
|
2839
|
+
<!-- Connect Sources (prominent on home page per Chairman Prompt #76) -->
|
|
2840
|
+
<div id="home-connectors" style="margin-bottom:16px">
|
|
2841
|
+
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:8px">Connect Sources</div>
|
|
2842
|
+
<div id="home-oauth-cards" style="display:flex;gap:8px;flex-wrap:wrap"></div>
|
|
2843
|
+
</div>
|
|
2844
|
+
|
|
2616
2845
|
<div id="home-god-nodes" class="recent-list" style="display:none;margin-bottom:16px"></div>
|
|
2846
|
+
|
|
2847
|
+
<!-- Wiki Health -->
|
|
2848
|
+
<div id="home-health" style="display:none;margin-bottom:16px">
|
|
2849
|
+
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:8px">Wiki Health</div>
|
|
2850
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap" id="health-badges"></div>
|
|
2851
|
+
<div id="conflicts-panel" style="display:none;margin-top:12px"></div>
|
|
2852
|
+
</div>
|
|
2853
|
+
|
|
2617
2854
|
<div class="home-section-title">Recent pages</div>
|
|
2618
2855
|
<div class="recent-list" id="home-recent"></div>
|
|
2619
2856
|
</div><!-- /home-dashboard -->
|
|
@@ -2624,13 +2861,16 @@
|
|
|
2624
2861
|
<div id="page-breadcrumbs"></div>
|
|
2625
2862
|
<div id="page-title-row">
|
|
2626
2863
|
<h1 id="page-title"></h1>
|
|
2864
|
+
<span id="page-confidence-badge" class="confidence-badge" style="display:none"></span>
|
|
2627
2865
|
<button id="page-edit-btn" onclick="toggleEditMode()" title="Edit page (Ctrl+E)">
|
|
2628
2866
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
2629
2867
|
</button>
|
|
2630
2868
|
</div>
|
|
2869
|
+
<div id="page-validation-bar" class="validation-bar" style="display:none"></div>
|
|
2631
2870
|
<div id="page-meta"></div>
|
|
2632
2871
|
<div id="page-entity-profile" class="page-entity-profile"></div>
|
|
2633
2872
|
<div id="page-toc"></div>
|
|
2873
|
+
<div id="page-fact-timeline" style="display:none"></div>
|
|
2634
2874
|
<div id="page-body" class="md"></div>
|
|
2635
2875
|
<div id="page-encyclopedia-chrome"></div>
|
|
2636
2876
|
<div id="wysiwyg-bar">
|
|
@@ -2697,8 +2937,8 @@
|
|
|
2697
2937
|
<div class="graph-settings-section">
|
|
2698
2938
|
<span class="graph-settings-label">Labels</span>
|
|
2699
2939
|
<div class="graph-settings-radios">
|
|
2700
|
-
<label><input type="radio" name="graph-labels" value="hubs"
|
|
2701
|
-
<label><input type="radio" name="graph-labels" value="all" /> All</label>
|
|
2940
|
+
<label><input type="radio" name="graph-labels" value="hubs" /> Hubs only</label>
|
|
2941
|
+
<label><input type="radio" name="graph-labels" value="all" checked /> All</label>
|
|
2702
2942
|
<label><input type="radio" name="graph-labels" value="none" /> None</label>
|
|
2703
2943
|
</div>
|
|
2704
2944
|
</div>
|
|
@@ -2769,7 +3009,7 @@
|
|
|
2769
3009
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 01-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
|
|
2770
3010
|
</button>
|
|
2771
3011
|
</div>
|
|
2772
|
-
<div id="voice-recording-status" style="display:none;margin-top:6px;font-size:11px;color:var(--red);
|
|
3012
|
+
<div id="voice-recording-status" style="display:none;margin-top:6px;font-size:11px;color:var(--red);align-items:center;gap:6px">
|
|
2773
3013
|
<span style="width:6px;height:6px;background:var(--red);border-radius:50%;display:inline-block;animation:voicePulse 1.2s infinite"></span>
|
|
2774
3014
|
Recording… <span id="voice-timer">0:00</span>
|
|
2775
3015
|
</div>
|
|
@@ -3009,6 +3249,29 @@
|
|
|
3009
3249
|
</div>
|
|
3010
3250
|
</div>
|
|
3011
3251
|
|
|
3252
|
+
<!-- Contradiction compare (Observer heuristic pairs) -->
|
|
3253
|
+
<div id="cx-modal-overlay" class="cx-modal-overlay" style="display:none">
|
|
3254
|
+
<div class="cx-modal" role="dialog" aria-modal="true" aria-labelledby="cx-modal-title">
|
|
3255
|
+
<div class="cx-modal-header">
|
|
3256
|
+
<div>
|
|
3257
|
+
<h3 id="cx-modal-title">Compare pages</h3>
|
|
3258
|
+
<div id="cx-modal-reason" class="cx-modal-reason"></div>
|
|
3259
|
+
</div>
|
|
3260
|
+
<button type="button" class="diff-modal-close" id="cx-modal-close" title="Close">×</button>
|
|
3261
|
+
</div>
|
|
3262
|
+
<div class="cx-modal-body">
|
|
3263
|
+
<div class="cx-col">
|
|
3264
|
+
<div class="cx-col-head" id="cx-head-a">Page A</div>
|
|
3265
|
+
<div class="cx-col-body" id="cx-body-a"></div>
|
|
3266
|
+
</div>
|
|
3267
|
+
<div class="cx-col">
|
|
3268
|
+
<div class="cx-col-head" id="cx-head-b">Page B</div>
|
|
3269
|
+
<div class="cx-col-body" id="cx-body-b"></div>
|
|
3270
|
+
</div>
|
|
3271
|
+
</div>
|
|
3272
|
+
</div>
|
|
3273
|
+
</div>
|
|
3274
|
+
|
|
3012
3275
|
<!-- Command Palette (Cmd+P) -->
|
|
3013
3276
|
<div id="palette-overlay">
|
|
3014
3277
|
<div id="palette-box">
|
|
@@ -3036,6 +3299,9 @@
|
|
|
3036
3299
|
|
|
3037
3300
|
<input type="file" id="file-input" multiple accept=".md,.txt,.pdf,.docx,.xlsx,.pptx,.csv,.json,.yaml,.yml,.html,.htm" />
|
|
3038
3301
|
|
|
3302
|
+
<!-- @Mention Autocomplete Popup -->
|
|
3303
|
+
<div id="mention-popup"></div>
|
|
3304
|
+
|
|
3039
3305
|
<!-- Floating Selection Toolbar (Notion-style) -->
|
|
3040
3306
|
<div id="selection-toolbar" class="selection-toolbar">
|
|
3041
3307
|
<button class="st-btn" data-cmd="bold" title="Bold (⌘B)"><b>B</b></button>
|
|
@@ -3067,9 +3333,9 @@
|
|
|
3067
3333
|
editTabType: null,
|
|
3068
3334
|
};
|
|
3069
3335
|
|
|
3070
|
-
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
3336
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
3071
3337
|
function formatBytes(b) { if(b<1024) return b+' B'; if(b<1048576) return (b/1024).toFixed(1)+' KB'; return (b/1048576).toFixed(1)+' MB'; }
|
|
3072
|
-
async function api(path) { const r=await fetch(path); return r.json(); }
|
|
3338
|
+
async function api(path) { const r=await fetch(path); if(!r.ok) throw new Error(`HTTP ${r.status} — ${path}`); return r.json(); }
|
|
3073
3339
|
|
|
3074
3340
|
const BOOKMARKS_STORAGE_KEY = 'wikimem-bookmarks';
|
|
3075
3341
|
function getBookmarks() {
|
|
@@ -3120,16 +3386,35 @@
|
|
|
3120
3386
|
return 'cat-page';
|
|
3121
3387
|
}
|
|
3122
3388
|
|
|
3389
|
+
const SVG_ICONS = {
|
|
3390
|
+
folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
|
|
3391
|
+
markdown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
|
|
3392
|
+
pdf: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M10 12h1.5a1.5 1.5 0 0 1 0 3H10v-3z"/><path d="M7 18v-6h1a2 2 0 0 1 0 4H7"/></svg>',
|
|
3393
|
+
image: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
|
|
3394
|
+
video: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><path d="M10 8l6 4-6 4V8z"/></svg>',
|
|
3395
|
+
audio: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
|
|
3396
|
+
spreadsheet: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="16" y2="17"/><line x1="12" y1="9" x2="12" y2="21"/></svg>',
|
|
3397
|
+
code: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
3398
|
+
text: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
|
|
3399
|
+
wiki: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',
|
|
3400
|
+
file: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
|
|
3401
|
+
};
|
|
3123
3402
|
function fileIcon(type, name) {
|
|
3124
|
-
if(type==='dir') return
|
|
3403
|
+
if(type==='dir') return SVG_ICONS.folder;
|
|
3125
3404
|
if(type==='raw') {
|
|
3126
3405
|
const ext = (name||'').split('.').pop().toLowerCase();
|
|
3127
|
-
if(['pdf'].includes(ext)) return
|
|
3128
|
-
if(['jpg','jpeg','png','gif','webp','svg'].includes(ext)) return
|
|
3129
|
-
if(['
|
|
3130
|
-
return
|
|
3406
|
+
if(['pdf'].includes(ext)) return SVG_ICONS.pdf;
|
|
3407
|
+
if(['jpg','jpeg','png','gif','webp','svg','ico'].includes(ext)) return SVG_ICONS.image;
|
|
3408
|
+
if(['mp4','webm','mov','avi','mkv'].includes(ext)) return SVG_ICONS.video;
|
|
3409
|
+
if(['mp3','wav','m4a','ogg','flac','aac','webm'].includes(ext)) return SVG_ICONS.audio;
|
|
3410
|
+
if(['csv','xlsx','xls','tsv'].includes(ext)) return SVG_ICONS.spreadsheet;
|
|
3411
|
+
if(['js','ts','jsx','tsx','py','go','rs','c','cpp','java','rb','sh','bash','zsh'].includes(ext)) return SVG_ICONS.code;
|
|
3412
|
+
if(['md','markdown'].includes(ext)) return SVG_ICONS.markdown;
|
|
3413
|
+
if(['txt','log','ini','cfg','conf'].includes(ext)) return SVG_ICONS.text;
|
|
3414
|
+
if(['json','yaml','yml','xml','toml','html','htm','css'].includes(ext)) return SVG_ICONS.code;
|
|
3415
|
+
return SVG_ICONS.file;
|
|
3131
3416
|
}
|
|
3132
|
-
return
|
|
3417
|
+
return SVG_ICONS.wiki;
|
|
3133
3418
|
}
|
|
3134
3419
|
|
|
3135
3420
|
const chevronSvg = '<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
@@ -3185,16 +3470,44 @@
|
|
|
3185
3470
|
});
|
|
3186
3471
|
}
|
|
3187
3472
|
|
|
3473
|
+
/** Add Wikipedia-style [1][2] citation markers linking to sources (TREND-001) */
|
|
3474
|
+
function addSourceCitations(container, page) {
|
|
3475
|
+
const fm = page?.frontmatter ?? {};
|
|
3476
|
+
const sources = Array.isArray(fm.sources) ? fm.sources : [];
|
|
3477
|
+
if (sources.length === 0) return;
|
|
3478
|
+
|
|
3479
|
+
// Build references section
|
|
3480
|
+
const refsHtml = sources.map((src, i) => {
|
|
3481
|
+
const label = String(src).replace(/^raw\//, '').replace(/^.*\//, '');
|
|
3482
|
+
return `<li id="cite-${i+1}"><a href="#" class="wiki-source-link" onclick="event.preventDefault();openRawFile('${esc(String(src))}')">${esc(label)}</a></li>`;
|
|
3483
|
+
}).join('');
|
|
3484
|
+
|
|
3485
|
+
// Add citation superscripts after the first paragraph
|
|
3486
|
+
const firstP = container.querySelector('p');
|
|
3487
|
+
if (firstP && sources.length > 0) {
|
|
3488
|
+
const citeLinks = sources.map((_, i) =>
|
|
3489
|
+
`<a href="#cite-${i+1}" class="citation-ref" title="Source ${i+1}"><sup>[${i+1}]</sup></a>`
|
|
3490
|
+
).join('');
|
|
3491
|
+
firstP.insertAdjacentHTML('beforeend', ' ' + citeLinks);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
// Append references section after content
|
|
3495
|
+
const refsSection = document.createElement('section');
|
|
3496
|
+
refsSection.className = 'page-references-section';
|
|
3497
|
+
refsSection.innerHTML = `<h2 class="page-encyclopedia-heading">References</h2><ol class="page-references-list">${refsHtml}</ol>`;
|
|
3498
|
+
container.appendChild(refsSection);
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3188
3501
|
// ── Views ──
|
|
3189
3502
|
function showView(mode) {
|
|
3190
3503
|
state.viewMode = mode;
|
|
3191
|
-
document.getElementById('home-view')
|
|
3192
|
-
document.getElementById('page-view')
|
|
3193
|
-
document.getElementById('graph-view')
|
|
3194
|
-
document.getElementById('settings-view')
|
|
3195
|
-
document.getElementById('history-view')
|
|
3196
|
-
document.getElementById('pipeline-view')
|
|
3197
|
-
document.getElementById('timelapse-view')
|
|
3504
|
+
document.getElementById('home-view')?.classList.toggle('active', mode==='home');
|
|
3505
|
+
document.getElementById('page-view')?.classList.toggle('active', mode==='page');
|
|
3506
|
+
document.getElementById('graph-view')?.classList.toggle('active', mode==='graph');
|
|
3507
|
+
document.getElementById('settings-view')?.classList.toggle('active', mode==='settings');
|
|
3508
|
+
document.getElementById('history-view')?.classList.toggle('active', mode==='history');
|
|
3509
|
+
document.getElementById('pipeline-view')?.classList.toggle('active', mode==='pipeline');
|
|
3510
|
+
document.getElementById('timelapse-view')?.classList.toggle('active', mode==='timelapse');
|
|
3198
3511
|
|
|
3199
3512
|
document.getElementById('rail-files').classList.toggle('active', mode==='home'||mode==='page');
|
|
3200
3513
|
document.getElementById('rail-graph').classList.toggle('active', mode==='graph');
|
|
@@ -3400,11 +3713,17 @@
|
|
|
3400
3713
|
|
|
3401
3714
|
function renderTabs() {
|
|
3402
3715
|
const bar = document.getElementById('tab-bar');
|
|
3403
|
-
const tabsHtml = state.tabs.map(t => {
|
|
3716
|
+
const tabsHtml = state.tabs.map((t, idx) => {
|
|
3404
3717
|
const active = t.id === state.activeTabId ? ' active' : '';
|
|
3405
3718
|
const icon = tabIcon(t);
|
|
3406
3719
|
const dirtyDot = t.dirty ? '<span class="tab-dirty-dot"></span>' : '';
|
|
3407
|
-
return `<div class="tab${active}" onclick="activateTab('${esc(t.id)}')"
|
|
3720
|
+
return `<div class="tab${active}" data-tab-idx="${idx}" draggable="true" onclick="activateTab('${esc(t.id)}')"
|
|
3721
|
+
ondragstart="event.dataTransfer.setData('tab-idx','${idx}');event.dataTransfer.effectAllowed='move';this.style.opacity='0.4'"
|
|
3722
|
+
ondragend="this.style.opacity='1'"
|
|
3723
|
+
ondragover="event.preventDefault();this.classList.add('tab-drag-over')"
|
|
3724
|
+
ondragleave="this.classList.remove('tab-drag-over')"
|
|
3725
|
+
ondrop="event.preventDefault();this.classList.remove('tab-drag-over');reorderTab(+event.dataTransfer.getData('tab-idx'),${idx})"
|
|
3726
|
+
oncontextmenu="event.preventDefault();showTabContextMenu(event,'${esc(t.id)}',${idx})">
|
|
3408
3727
|
<span class="tab-icon">${icon}</span>
|
|
3409
3728
|
<span class="tab-label">${esc(t.title)}</span>${dirtyDot}
|
|
3410
3729
|
<button class="tab-close" onclick="event.stopPropagation();closeTab('${esc(t.id)}')">×</button>
|
|
@@ -3417,6 +3736,44 @@
|
|
|
3417
3736
|
`<div id="tab-bar-right"><div class="status-dot"></div><span id="status-text">${document.getElementById('status-text')?.textContent || 'Loading...'}</span></div>`;
|
|
3418
3737
|
}
|
|
3419
3738
|
|
|
3739
|
+
function reorderTab(fromIdx, toIdx) {
|
|
3740
|
+
if (fromIdx === toIdx) return;
|
|
3741
|
+
const [moved] = state.tabs.splice(fromIdx, 1);
|
|
3742
|
+
state.tabs.splice(toIdx, 0, moved);
|
|
3743
|
+
renderTabs();
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
function showTabContextMenu(e, tabId, idx) {
|
|
3747
|
+
let menu = document.getElementById('tab-ctx-menu');
|
|
3748
|
+
if (!menu) {
|
|
3749
|
+
menu = document.createElement('div');
|
|
3750
|
+
menu.id = 'tab-ctx-menu';
|
|
3751
|
+
menu.className = 'ctx-menu';
|
|
3752
|
+
document.body.appendChild(menu);
|
|
3753
|
+
}
|
|
3754
|
+
menu.innerHTML = '';
|
|
3755
|
+
const items = [
|
|
3756
|
+
{ label: 'Close Tab', action: () => closeTab(tabId) },
|
|
3757
|
+
{ label: 'Close Other Tabs', action: () => { state.tabs = state.tabs.filter(t => t.id === tabId); state.activeTabId = tabId; renderTabs(); } },
|
|
3758
|
+
{ label: 'Close Tabs to the Right', action: () => { state.tabs = state.tabs.slice(0, idx + 1); if (!state.tabs.find(t => t.id === state.activeTabId)) { activateTab(tabId); } renderTabs(); } },
|
|
3759
|
+
{ label: 'Duplicate Tab', action: () => { const t = state.tabs[idx]; openTab(t.id + '-dup-' + Date.now(), t.title, t.type); } },
|
|
3760
|
+
];
|
|
3761
|
+
items.forEach(item => {
|
|
3762
|
+
const el = document.createElement('div');
|
|
3763
|
+
el.className = 'ctx-menu-item';
|
|
3764
|
+
el.textContent = item.label;
|
|
3765
|
+
el.addEventListener('click', () => { menu.style.display = 'none'; item.action(); });
|
|
3766
|
+
menu.appendChild(el);
|
|
3767
|
+
});
|
|
3768
|
+
menu.style.display = 'block';
|
|
3769
|
+
menu.style.left = e.clientX + 'px';
|
|
3770
|
+
menu.style.top = e.clientY + 'px';
|
|
3771
|
+
setTimeout(() => {
|
|
3772
|
+
const close = (ev) => { if (!menu.contains(ev.target)) { menu.style.display = 'none'; document.removeEventListener('click', close); } };
|
|
3773
|
+
document.addEventListener('click', close);
|
|
3774
|
+
}, 0);
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3420
3777
|
function estimateReadingMinutes(wordCount) {
|
|
3421
3778
|
const n = Number(wordCount) || 0;
|
|
3422
3779
|
return Math.max(1, Math.round(n / 200));
|
|
@@ -3453,6 +3810,9 @@
|
|
|
3453
3810
|
parts.push(`<section class="page-encyclopedia-section page-see-also-block" aria-labelledby="wiki-see-also-heading"><h2 id="wiki-see-also-heading" class="page-encyclopedia-heading">See also</h2><ul class="wiki-see-also-list">${items}</ul></section>`);
|
|
3454
3811
|
}
|
|
3455
3812
|
|
|
3813
|
+
// Similar Pages placeholder (loaded async)
|
|
3814
|
+
parts.push(`<section class="page-encyclopedia-section page-similar-block" id="similar-pages-section" style="display:none" aria-labelledby="wiki-similar-heading"><h2 id="wiki-similar-heading" class="page-encyclopedia-heading">Similar pages</h2><div id="similar-pages-list" class="similar-pages-grid"></div></section>`);
|
|
3815
|
+
|
|
3456
3816
|
const updated = fm.updated || fm.created;
|
|
3457
3817
|
if (updated) {
|
|
3458
3818
|
parts.push(`<footer class="page-article-footer"><div class="page-article-footer-inner"><span class="page-last-updated">Last updated: ${esc(String(updated))}</span></div></footer>`);
|
|
@@ -3592,6 +3952,11 @@
|
|
|
3592
3952
|
document.getElementById('page-breadcrumbs').innerHTML = breadcrumbs;
|
|
3593
3953
|
|
|
3594
3954
|
const fm = page.frontmatter || {};
|
|
3955
|
+
|
|
3956
|
+
// Confidence badge + validation bar
|
|
3957
|
+
renderConfidenceBadge(fm);
|
|
3958
|
+
renderValidationBar(page.title, fm);
|
|
3959
|
+
|
|
3595
3960
|
const tags = (fm.tags || []).map(t =>
|
|
3596
3961
|
`<span class="meta-tag-chip">${esc(t)}<button class="chip-remove" onclick="event.stopPropagation();removePageTag('${esc(page.title)}','${esc(t)}')" title="Remove tag">×</button></span>`
|
|
3597
3962
|
).join('');
|
|
@@ -3665,10 +4030,14 @@
|
|
|
3665
4030
|
body.innerHTML = rendered;
|
|
3666
4031
|
body.classList.add('fade-in');
|
|
3667
4032
|
addCopyButtons(body);
|
|
4033
|
+
addSourceCitations(body, page);
|
|
3668
4034
|
setTimeout(() => body.classList.remove('fade-in'), 250);
|
|
3669
4035
|
|
|
3670
4036
|
buildToc(body);
|
|
3671
4037
|
|
|
4038
|
+
// Fetch and render fact timeline (COMP-MP-002)
|
|
4039
|
+
renderFactTimeline(page.title, fm);
|
|
4040
|
+
|
|
3672
4041
|
renderEncyclopediaChrome(page);
|
|
3673
4042
|
|
|
3674
4043
|
const blContainer = document.getElementById('page-backlinks');
|
|
@@ -3684,6 +4053,9 @@
|
|
|
3684
4053
|
highlightActiveTreeItem(title);
|
|
3685
4054
|
document.getElementById('sb-path').textContent = relPath || title;
|
|
3686
4055
|
document.getElementById('content').scrollTop = 0;
|
|
4056
|
+
|
|
4057
|
+
// Async: load similar pages
|
|
4058
|
+
loadSimilarPages(page.title);
|
|
3687
4059
|
} catch(err) {
|
|
3688
4060
|
console.error('Failed to open page:', err);
|
|
3689
4061
|
}
|
|
@@ -3721,6 +4093,8 @@
|
|
|
3721
4093
|
showView('page');
|
|
3722
4094
|
|
|
3723
4095
|
document.getElementById('page-edit-btn').style.display = 'none';
|
|
4096
|
+
document.getElementById('page-confidence-badge').style.display = 'none';
|
|
4097
|
+
document.getElementById('page-validation-bar').style.display = 'none';
|
|
3724
4098
|
document.getElementById('page-title').textContent = filename;
|
|
3725
4099
|
const rawParts = filepath.split('/').filter(Boolean);
|
|
3726
4100
|
let rawBc = `<span onclick="showView('home')" style="cursor:pointer">home</span><span class="breadcrumb-sep">›</span><span>raw</span>`;
|
|
@@ -3882,8 +4256,8 @@
|
|
|
3882
4256
|
});
|
|
3883
4257
|
}
|
|
3884
4258
|
|
|
3885
|
-
//
|
|
3886
|
-
document.getElementById('page-body').addEventListener('
|
|
4259
|
+
// Double-click on page body → activate WYSIWYG edit (single-click reserved for link navigation)
|
|
4260
|
+
document.getElementById('page-body').addEventListener('dblclick', (e) => {
|
|
3887
4261
|
if (state.editing || state.editTabType !== 'wiki') return;
|
|
3888
4262
|
if (e.target.closest('a[href]')) return; // let normal links work
|
|
3889
4263
|
activateWysiwyg();
|
|
@@ -3897,10 +4271,16 @@
|
|
|
3897
4271
|
let rawMd = '';
|
|
3898
4272
|
let pageSlug = title;
|
|
3899
4273
|
try {
|
|
3900
|
-
const
|
|
4274
|
+
const r = await fetch('/api/pages/' + encodeURIComponent(title) + '/raw');
|
|
4275
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
4276
|
+
const data = await r.json();
|
|
3901
4277
|
rawMd = data.raw || '';
|
|
3902
4278
|
pageSlug = data.slug || title;
|
|
3903
|
-
} catch {
|
|
4279
|
+
} catch (err) {
|
|
4280
|
+
console.warn('Could not load page source for editing:', err);
|
|
4281
|
+
if (typeof showToast === 'function') showToast('Could not load source — edit cancelled');
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
3904
4284
|
|
|
3905
4285
|
state.editing = true;
|
|
3906
4286
|
state.editOriginal = rawMd;
|
|
@@ -4149,8 +4529,12 @@
|
|
|
4149
4529
|
|
|
4150
4530
|
for (const item of items) {
|
|
4151
4531
|
if (item.sep) { menu.innerHTML += '<div class="ctx-menu-sep"></div>'; continue; }
|
|
4152
|
-
const
|
|
4153
|
-
|
|
4532
|
+
const el = document.createElement('div');
|
|
4533
|
+
el.className = 'ctx-menu-item';
|
|
4534
|
+
if (item.danger) el.style.color = 'var(--red)';
|
|
4535
|
+
el.innerHTML = `${item.icon || ''} ${item.label}`;
|
|
4536
|
+
el.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); item.action(); });
|
|
4537
|
+
menu.appendChild(el);
|
|
4154
4538
|
}
|
|
4155
4539
|
|
|
4156
4540
|
document.body.appendChild(menu);
|
|
@@ -4163,6 +4547,9 @@
|
|
|
4163
4547
|
document.getElementById('tree-ctx-menu')?.remove();
|
|
4164
4548
|
}
|
|
4165
4549
|
document.addEventListener('click', closeTreeContextMenu);
|
|
4550
|
+
document.addEventListener('contextmenu', (e) => {
|
|
4551
|
+
if (e.target.closest?.('.tree-item')) { e.preventDefault(); }
|
|
4552
|
+
});
|
|
4166
4553
|
|
|
4167
4554
|
async function renameFile(title, path, type) {
|
|
4168
4555
|
const newName = prompt('New name:', title);
|
|
@@ -4399,6 +4786,62 @@
|
|
|
4399
4786
|
} catch(e) { el.textContent = originalTitle; showToast('Failed to rename'); }
|
|
4400
4787
|
}
|
|
4401
4788
|
|
|
4789
|
+
// ── Confidence & Validation System ──
|
|
4790
|
+
function renderConfidenceBadge(fm) {
|
|
4791
|
+
const el = document.getElementById('page-confidence-badge');
|
|
4792
|
+
if (!el) return;
|
|
4793
|
+
const val = typeof fm.confidence === 'number' ? fm.confidence : null;
|
|
4794
|
+
if (val === null) { el.style.display = 'none'; return; }
|
|
4795
|
+
const tier = val > 80 ? 'high' : val >= 50 ? 'medium' : 'low';
|
|
4796
|
+
const icons = {
|
|
4797
|
+
high: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
4798
|
+
medium: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>',
|
|
4799
|
+
low: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
4800
|
+
};
|
|
4801
|
+
el.className = `confidence-badge ${tier}`;
|
|
4802
|
+
el.innerHTML = `${icons[tier]} ${val}%`;
|
|
4803
|
+
el.title = `Confidence: ${val}% (${tier})`;
|
|
4804
|
+
el.style.display = '';
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
function renderValidationBar(title, fm) {
|
|
4808
|
+
const el = document.getElementById('page-validation-bar');
|
|
4809
|
+
if (!el) return;
|
|
4810
|
+
const vs = fm.validation_status || 'unreviewed';
|
|
4811
|
+
const validatedAt = fm.validated_at ? ` · ${fm.validated_at}` : '';
|
|
4812
|
+
el.innerHTML = `
|
|
4813
|
+
<button class="validation-btn${vs === 'verified' ? ' active-verified' : ''}" onclick="setPageValidation('${esc(title)}','verified')">
|
|
4814
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> Verified
|
|
4815
|
+
</button>
|
|
4816
|
+
<button class="validation-btn${vs === 'outdated' ? ' active-outdated' : ''}" onclick="setPageValidation('${esc(title)}','outdated')">
|
|
4817
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Outdated
|
|
4818
|
+
</button>
|
|
4819
|
+
<button class="validation-btn${vs === 'wrong' ? ' active-wrong' : ''}" onclick="setPageValidation('${esc(title)}','wrong')">
|
|
4820
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> Wrong
|
|
4821
|
+
</button>
|
|
4822
|
+
${vs !== 'unreviewed' ? `<span class="validation-status-label">${vs}${validatedAt}</span>` : ''}`;
|
|
4823
|
+
el.style.display = 'flex';
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
async function setPageValidation(title, status) {
|
|
4827
|
+
try {
|
|
4828
|
+
const res = await fetch('/api/pages/' + encodeURIComponent(title) + '/validate', {
|
|
4829
|
+
method: 'PATCH',
|
|
4830
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4831
|
+
body: JSON.stringify({ validation_status: status }),
|
|
4832
|
+
});
|
|
4833
|
+
const data = await res.json();
|
|
4834
|
+
if (data.error) { showToast('Validation failed: ' + data.error); return; }
|
|
4835
|
+
showToast(`✓ Marked as ${status}`);
|
|
4836
|
+
// Re-render with updated frontmatter
|
|
4837
|
+
const fm = data.page?.frontmatter || {};
|
|
4838
|
+
renderConfidenceBadge(fm);
|
|
4839
|
+
renderValidationBar(title, fm);
|
|
4840
|
+
} catch (err) {
|
|
4841
|
+
showToast('Validation failed');
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4402
4845
|
async function removePageTag(title, tag) {
|
|
4403
4846
|
try {
|
|
4404
4847
|
const { raw, slug } = await fetchPageRaw(title);
|
|
@@ -4771,11 +5214,25 @@
|
|
|
4771
5214
|
let auditActorFilter = 'all';
|
|
4772
5215
|
|
|
4773
5216
|
function inferActor(entry) {
|
|
4774
|
-
const msg = (entry.message || '')
|
|
5217
|
+
const msg = (entry.message || '');
|
|
5218
|
+
const msgLower = msg.toLowerCase();
|
|
4775
5219
|
const author = (entry.author || '').toLowerCase();
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
if(
|
|
5220
|
+
|
|
5221
|
+
// Check conventional commit format: wiki: feat(observe), wiki: feat(ingest), wiki: feat(scrape)
|
|
5222
|
+
if(/^wiki:\s*feat\(observe\)/i.test(msg)) return 'observer';
|
|
5223
|
+
if(/^wiki:\s*feat\(ingest\)/i.test(msg) || /^wiki:\s*feat\(scrape\)/i.test(msg)) return 'agent';
|
|
5224
|
+
|
|
5225
|
+
// Check explicit prefixes and tags
|
|
5226
|
+
if(msgLower.startsWith('observer:') || msgLower.includes('[observer]') || author.includes('observer')) return 'observer';
|
|
5227
|
+
if(msgLower.startsWith('webhook:') || msgLower.includes('[webhook]') || author.includes('webhook')) return 'webhook';
|
|
5228
|
+
if(msgLower.startsWith('agent:') || msgLower.startsWith('auto:') || msgLower.includes('[agent]') || author.includes('bot') || author.includes('agent')) return 'agent';
|
|
5229
|
+
|
|
5230
|
+
// wiki: feat(manual) or plain wiki: commits from the CLI are human
|
|
5231
|
+
if(/^wiki:\s*feat\(manual\)/i.test(msg)) return 'human';
|
|
5232
|
+
|
|
5233
|
+
// Any other wiki: commit without a known scope — check author for hints
|
|
5234
|
+
if(/^wiki:/i.test(msg) && (author.includes('wikimem') || author === 'wikimem-cli')) return 'agent';
|
|
5235
|
+
|
|
4779
5236
|
return 'human';
|
|
4780
5237
|
}
|
|
4781
5238
|
|
|
@@ -4818,8 +5275,18 @@
|
|
|
4818
5275
|
|
|
4819
5276
|
checkMigrationBanner();
|
|
4820
5277
|
|
|
5278
|
+
// Show loading state immediately
|
|
5279
|
+
timeline.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)"><div class="spinner" style="margin:0 auto 8px"></div>Loading audit trail…</div>';
|
|
5280
|
+
|
|
4821
5281
|
try {
|
|
4822
|
-
const
|
|
5282
|
+
const wikiOnly = wikiOnlyEl?.checked ?? true;
|
|
5283
|
+
const search = searchEl?.value?.trim() || '';
|
|
5284
|
+
let url = `/api/git/log?limit=50&wikiOnly=${wikiOnly}`;
|
|
5285
|
+
if(search) url += `&search=${encodeURIComponent(search)}`;
|
|
5286
|
+
|
|
5287
|
+
// Fetch status and log in parallel
|
|
5288
|
+
const [status, log] = await Promise.all([api('/api/git/status'), api(url)]);
|
|
5289
|
+
|
|
4823
5290
|
if(!status.initialized) {
|
|
4824
5291
|
branchInfo.textContent = 'Not a git repository';
|
|
4825
5292
|
initBtn.style.display = 'inline-block';
|
|
@@ -4830,12 +5297,6 @@
|
|
|
4830
5297
|
initBtn.style.display = 'none';
|
|
4831
5298
|
branchInfo.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> ${esc(status.branch)}`;
|
|
4832
5299
|
|
|
4833
|
-
const wikiOnly = wikiOnlyEl?.checked ?? true;
|
|
4834
|
-
const search = searchEl?.value?.trim() || '';
|
|
4835
|
-
let url = `/api/git/log?limit=50&wikiOnly=${wikiOnly}`;
|
|
4836
|
-
if(search) url += `&search=${encodeURIComponent(search)}`;
|
|
4837
|
-
const log = await api(url);
|
|
4838
|
-
|
|
4839
5300
|
if(!log.length) {
|
|
4840
5301
|
const msg = wikiOnly
|
|
4841
5302
|
? 'No wiki commits yet. Ingest content to create wiki commits.'
|
|
@@ -5258,19 +5719,21 @@
|
|
|
5258
5719
|
return community;
|
|
5259
5720
|
}
|
|
5260
5721
|
|
|
5261
|
-
// ── Compute in-degree for
|
|
5722
|
+
// ── Compute in-degree for hub nodes ─────────────────────────────────
|
|
5262
5723
|
const inDegree = new Map(data.nodes.map(n => [n.id, 0]));
|
|
5263
5724
|
for(const l of data.links) {
|
|
5264
5725
|
const t = typeof l.target==='object' ? l.target.id : l.target;
|
|
5265
5726
|
inDegree.set(t, (inDegree.get(t)||0)+1);
|
|
5266
5727
|
}
|
|
5267
|
-
const
|
|
5268
|
-
const isGodNode = id => (inDegree.get(id)||0) >= Math.max(
|
|
5728
|
+
const totalNodes = data.nodes.length;
|
|
5729
|
+
const isGodNode = id => (inDegree.get(id)||0) >= Math.max(5, Math.ceil(totalNodes * 0.06));
|
|
5730
|
+
// Node radius: matches timelapse formula (based on inbound link count)
|
|
5731
|
+
const nR = id => { const li = inDegree.get(id)||0; return Math.max(4, Math.min(18, 4 + Math.sqrt(li) * 3)); };
|
|
5269
5732
|
|
|
5270
5733
|
const communityMap = detectCommunities(data.nodes, data.links);
|
|
5271
5734
|
const communityIds = [...new Set(communityMap.values())];
|
|
5272
|
-
const
|
|
5273
|
-
const communityColor = id =>
|
|
5735
|
+
const GRAPH_COLORS = ['#4f9eff','#4ec9b0','#d7ba7d','#c586c0','#9cdcfe','#dcdcaa','#ce9178','#608b4e','#b5cea8'];
|
|
5736
|
+
const communityColor = id => GRAPH_COLORS[communityIds.indexOf(communityMap.get(id)||id) % GRAPH_COLORS.length];
|
|
5274
5737
|
|
|
5275
5738
|
const svg = d3.select('#graph-svg');
|
|
5276
5739
|
const tooltip = document.getElementById('tooltip');
|
|
@@ -5308,17 +5771,17 @@
|
|
|
5308
5771
|
const link = g.append('g').selectAll('line').data(data.links).join('line')
|
|
5309
5772
|
.attr('stroke',cs('--border-subtle')).attr('stroke-width',0.8).attr('stroke-opacity',0.4);
|
|
5310
5773
|
|
|
5311
|
-
//
|
|
5774
|
+
// Hub node outer ring (gold, matching timelapse)
|
|
5312
5775
|
g.append('g').selectAll('circle.god-ring').data(data.nodes.filter(d=>isGodNode(d.id))).join('circle')
|
|
5313
5776
|
.attr('class','god-ring')
|
|
5314
|
-
.attr('r', d =>
|
|
5315
|
-
.attr('fill','none').attr('stroke',
|
|
5316
|
-
.attr('opacity',0.
|
|
5777
|
+
.attr('r', d => nR(d.id)+5)
|
|
5778
|
+
.attr('fill','none').attr('stroke','#d4a843').attr('stroke-width',1.5)
|
|
5779
|
+
.attr('opacity',0.5);
|
|
5317
5780
|
|
|
5318
5781
|
const node = g.append('g').selectAll('circle').data(data.nodes).join('circle')
|
|
5319
|
-
.attr('r', d =>
|
|
5782
|
+
.attr('r', d => nR(d.id))
|
|
5320
5783
|
.attr('fill', d => communityColor(d.id))
|
|
5321
|
-
.attr('stroke', d => isGodNode(d.id) ?
|
|
5784
|
+
.attr('stroke', d => isGodNode(d.id) ? '#d4a843' : cs('--bg'))
|
|
5322
5785
|
.attr('stroke-width', d => isGodNode(d.id) ? 2 : 1.5)
|
|
5323
5786
|
.attr('cursor','pointer')
|
|
5324
5787
|
.call(d3.drag()
|
|
@@ -5391,6 +5854,70 @@
|
|
|
5391
5854
|
.attr('cy', d => { const n = data.nodes.find(n=>n.id===d.id); return n?.y||0; });
|
|
5392
5855
|
label.attr('x',d=>d.x).attr('y',d=>d.y);
|
|
5393
5856
|
});
|
|
5857
|
+
|
|
5858
|
+
// ── Graph settings panel toggle ──
|
|
5859
|
+
const panelToggle = document.getElementById('graph-panel-toggle');
|
|
5860
|
+
const settingsPanel = document.getElementById('graph-settings-panel');
|
|
5861
|
+
if(panelToggle && settingsPanel) {
|
|
5862
|
+
panelToggle.addEventListener('click', () => {
|
|
5863
|
+
const open = settingsPanel.classList.toggle('collapsed');
|
|
5864
|
+
panelToggle.setAttribute('aria-expanded', String(!settingsPanel.classList.contains('collapsed')));
|
|
5865
|
+
settingsPanel.setAttribute('aria-hidden', String(settingsPanel.classList.contains('collapsed')));
|
|
5866
|
+
});
|
|
5867
|
+
}
|
|
5868
|
+
|
|
5869
|
+
// ── Graph labels radio handler ──
|
|
5870
|
+
function applyLabelMode(mode) {
|
|
5871
|
+
if(mode === 'all') label.attr('display', 'block');
|
|
5872
|
+
else if(mode === 'none') label.attr('display', 'none');
|
|
5873
|
+
else label.attr('display', d => isGodNode(d.id) ? 'block' : 'none');
|
|
5874
|
+
}
|
|
5875
|
+
document.querySelectorAll('input[name="graph-labels"]').forEach(r => {
|
|
5876
|
+
r.addEventListener('change', e => applyLabelMode(e.target.value));
|
|
5877
|
+
});
|
|
5878
|
+
|
|
5879
|
+
// ── Graph force strength slider ──
|
|
5880
|
+
const forceRange = document.getElementById('graph-force-range');
|
|
5881
|
+
const forceValue = document.getElementById('graph-force-value');
|
|
5882
|
+
if(forceRange) {
|
|
5883
|
+
forceRange.addEventListener('input', e => {
|
|
5884
|
+
const v = parseInt(e.target.value, 10);
|
|
5885
|
+
sim.force('charge', d3.forceManyBody().strength(v));
|
|
5886
|
+
sim.alpha(0.3).restart();
|
|
5887
|
+
if(forceValue) forceValue.textContent = v < 0 ? `\u2212${Math.abs(v)}` : String(v);
|
|
5888
|
+
});
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
// ── Graph link distance slider ──
|
|
5892
|
+
const linkDistRange = document.getElementById('graph-link-dist-range');
|
|
5893
|
+
const linkDistValue = document.getElementById('graph-link-dist-value');
|
|
5894
|
+
if(linkDistRange) {
|
|
5895
|
+
linkDistRange.addEventListener('input', e => {
|
|
5896
|
+
const v = parseInt(e.target.value, 10);
|
|
5897
|
+
sim.force('link').distance(v);
|
|
5898
|
+
sim.alpha(0.3).restart();
|
|
5899
|
+
if(linkDistValue) linkDistValue.textContent = String(v);
|
|
5900
|
+
});
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
// ── Graph node size radio ──
|
|
5904
|
+
document.querySelectorAll('input[name="graph-node-size"]').forEach(r => {
|
|
5905
|
+
r.addEventListener('change', e => {
|
|
5906
|
+
const mode = e.target.value;
|
|
5907
|
+
if(mode === 'uniform') {
|
|
5908
|
+
node.attr('r', 8);
|
|
5909
|
+
} else if(mode === 'wordcount') {
|
|
5910
|
+
node.attr('r', d => Math.max(4, Math.min(22, Math.sqrt(d.wordCount / 50))));
|
|
5911
|
+
} else {
|
|
5912
|
+
node.attr('r', d => isGodNode(d.id) ? Math.max(7, Math.min(22, Math.sqrt(d.wordCount/50))) : Math.max(4, Math.min(16, Math.sqrt(d.wordCount/70))));
|
|
5913
|
+
}
|
|
5914
|
+
sim.force('collision', d3.forceCollide().radius(d => {
|
|
5915
|
+
const r = parseFloat(node.filter(n => n.id === d.id).attr('r')) || 8;
|
|
5916
|
+
return r + 4;
|
|
5917
|
+
}));
|
|
5918
|
+
sim.alpha(0.3).restart();
|
|
5919
|
+
});
|
|
5920
|
+
});
|
|
5394
5921
|
}
|
|
5395
5922
|
|
|
5396
5923
|
// ── Graph Export (EXPORT-001) ──
|
|
@@ -5553,21 +6080,80 @@
|
|
|
5553
6080
|
<div id="connectors-list">
|
|
5554
6081
|
<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:32px 0">Loading sources…</div>
|
|
5555
6082
|
</div>
|
|
6083
|
+
|
|
6084
|
+
<div style="margin-top:28px;padding-top:20px;border-top:1px solid var(--border-subtle)">
|
|
6085
|
+
<div class="settings-section-title" style="margin-bottom:4px">Platform Integrations</div>
|
|
6086
|
+
<div class="settings-section-desc">Connect platforms to auto-sync data into your wiki.</div>
|
|
6087
|
+
<div id="oauth-integrations">
|
|
6088
|
+
<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:24px 0">Loading integrations…</div>
|
|
6089
|
+
</div>
|
|
6090
|
+
</div>
|
|
6091
|
+
|
|
5556
6092
|
<div id="connector-modal" class="connector-modal" style="display:none"></div>
|
|
5557
6093
|
`;
|
|
5558
6094
|
loadConnectors();
|
|
6095
|
+
renderOAuthIntegrations();
|
|
5559
6096
|
} else if(section === 'appearance') {
|
|
6097
|
+
const prefs = loadAppearancePrefs();
|
|
5560
6098
|
el.innerHTML = `
|
|
5561
6099
|
<div class="settings-section-title">Appearance</div>
|
|
5562
|
-
<div class="settings-section-desc">Customize how wikimem looks.</div>
|
|
6100
|
+
<div class="settings-section-desc">Customize how wikimem looks and feels.</div>
|
|
6101
|
+
|
|
5563
6102
|
<div class="settings-group">
|
|
5564
6103
|
<label class="settings-label">Theme</label>
|
|
5565
|
-
<
|
|
6104
|
+
<div style="display:flex;gap:8px">
|
|
6105
|
+
<button class="settings-btn ${prefs.theme==='dark'?'settings-btn-primary':''}" onclick="setAppearance('theme','dark')">🌙 Dark</button>
|
|
6106
|
+
<button class="settings-btn ${prefs.theme==='light'?'settings-btn-primary':''}" onclick="setAppearance('theme','light')">☀️ Light</button>
|
|
6107
|
+
</div>
|
|
5566
6108
|
</div>
|
|
6109
|
+
|
|
5567
6110
|
<div class="settings-group">
|
|
5568
|
-
<label class="settings-label">Font Size</label>
|
|
5569
|
-
<
|
|
5570
|
-
|
|
6111
|
+
<label class="settings-label">Font Size <span style="color:var(--text-muted);font-weight:400">${prefs.fontSize}px</span></label>
|
|
6112
|
+
<input type="range" min="12" max="18" step="1" value="${prefs.fontSize}" class="tl-slider" style="max-width:240px;height:4px"
|
|
6113
|
+
oninput="document.querySelector('[data-fs-val]').textContent=this.value+'px';setAppearance('fontSize',+this.value)">
|
|
6114
|
+
<span data-fs-val style="display:none">${prefs.fontSize}px</span>
|
|
6115
|
+
</div>
|
|
6116
|
+
|
|
6117
|
+
<div class="settings-group">
|
|
6118
|
+
<label class="settings-label">Content Width</label>
|
|
6119
|
+
<select class="settings-select" onchange="setAppearance('contentWidth',this.value)">
|
|
6120
|
+
<option value="680" ${prefs.contentWidth===680?'selected':''}>Narrow (680px)</option>
|
|
6121
|
+
<option value="860" ${prefs.contentWidth===860?'selected':''}>Default (860px)</option>
|
|
6122
|
+
<option value="1100" ${prefs.contentWidth===1100?'selected':''}>Wide (1100px)</option>
|
|
6123
|
+
<option value="100%" ${prefs.contentWidth==='100%'?'selected':''}>Full width</option>
|
|
6124
|
+
</select>
|
|
6125
|
+
</div>
|
|
6126
|
+
|
|
6127
|
+
<div class="settings-group">
|
|
6128
|
+
<label class="settings-label">Interface Density</label>
|
|
6129
|
+
<select class="settings-select" onchange="setAppearance('density',this.value)">
|
|
6130
|
+
<option value="compact" ${prefs.density==='compact'?'selected':''}>Compact</option>
|
|
6131
|
+
<option value="comfortable" ${prefs.density==='comfortable'?'selected':''}>Comfortable (default)</option>
|
|
6132
|
+
<option value="spacious" ${prefs.density==='spacious'?'selected':''}>Spacious</option>
|
|
6133
|
+
</select>
|
|
6134
|
+
</div>
|
|
6135
|
+
|
|
6136
|
+
<div class="settings-group">
|
|
6137
|
+
<label class="settings-label">Accent Color</label>
|
|
6138
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
6139
|
+
${['#4f9eff','#8b5cf6','#10b981','#f59e0b','#ef4444','#ec4899','#06b6d4'].map(c =>
|
|
6140
|
+
`<button style="width:28px;height:28px;border-radius:50%;background:${c};border:2px solid ${c===prefs.accent?'var(--text-bright)':'transparent'};cursor:pointer;transition:border 0.1s" onclick="setAppearance('accent','${c}')"></button>`
|
|
6141
|
+
).join('')}
|
|
6142
|
+
</div>
|
|
6143
|
+
</div>
|
|
6144
|
+
|
|
6145
|
+
<div class="settings-group">
|
|
6146
|
+
<label class="settings-label">Code Block Line Numbers</label>
|
|
6147
|
+
<label class="auto-toggle" title="Show line numbers in code blocks">
|
|
6148
|
+
<input type="checkbox" ${prefs.codeLineNumbers?'checked':''} onchange="setAppearance('codeLineNumbers',this.checked)">
|
|
6149
|
+
<span class="auto-toggle-track"></span>
|
|
6150
|
+
</label>
|
|
6151
|
+
</div>
|
|
6152
|
+
|
|
6153
|
+
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border-subtle)">
|
|
6154
|
+
<button class="settings-btn" onclick="resetAppearance()">Reset to defaults</button>
|
|
6155
|
+
</div>
|
|
6156
|
+
`;
|
|
5571
6157
|
} else if(section === 'automations') {
|
|
5572
6158
|
el.innerHTML = `
|
|
5573
6159
|
<div class="settings-section-title">Automations</div>
|
|
@@ -5641,13 +6227,41 @@
|
|
|
5641
6227
|
<option value="manual">Manual only</option>
|
|
5642
6228
|
</select>
|
|
5643
6229
|
</div>
|
|
6230
|
+
<div class="settings-group" style="margin-bottom:14px">
|
|
6231
|
+
<label class="settings-label">Auto-Improve Weak Pages</label>
|
|
6232
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
6233
|
+
<label class="auto-toggle" title="Use LLM to fix weak pages">
|
|
6234
|
+
<input type="checkbox" id="auto-observer-improve" onchange="saveAutoSetting('observer','autoImprove',this.checked)">
|
|
6235
|
+
<span class="auto-toggle-track"></span>
|
|
6236
|
+
</label>
|
|
6237
|
+
<span style="font-size:11px;color:var(--text-muted)">LLM rewrites pages scoring below 50% (up to 3 per run)</span>
|
|
6238
|
+
</div>
|
|
6239
|
+
</div>
|
|
6240
|
+
<div class="settings-group" style="margin-bottom:14px">
|
|
6241
|
+
<label class="settings-label">Model for Observer</label>
|
|
6242
|
+
<select class="settings-select" id="auto-observer-model" onchange="saveAutoSetting('observer','model',this.value)">
|
|
6243
|
+
<option value="" selected>Use default (from Provider settings)</option>
|
|
6244
|
+
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
|
6245
|
+
<option value="claude-haiku-4-5-20251001">Claude Haiku 4.5</option>
|
|
6246
|
+
<option value="gpt-4o">GPT-4o</option>
|
|
6247
|
+
<option value="gpt-4o-mini">GPT-4o mini</option>
|
|
6248
|
+
<option value="ollama/llama3.1">Ollama — Llama 3.1</option>
|
|
6249
|
+
<option value="ollama/qwen2.5">Ollama — Qwen 2.5</option>
|
|
6250
|
+
</select>
|
|
6251
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">Choose which model scores and improves pages</div>
|
|
6252
|
+
</div>
|
|
5644
6253
|
<div class="settings-group" style="margin-bottom:0">
|
|
5645
|
-
<label class="settings-label">
|
|
5646
|
-
<div id="auto-observer-last-report" style="font-size:12px;color:var(--text-muted);padding:4px 0">
|
|
6254
|
+
<label class="settings-label">Quality report</label>
|
|
6255
|
+
<div id="auto-observer-last-report" style="font-size:12px;color:var(--text-muted);padding:4px 0">Loading…</div>
|
|
6256
|
+
<div id="observer-report-detail" style="margin-top:8px;display:none"></div>
|
|
6257
|
+
<div id="observer-cx-list" style="margin-top:10px;max-height:220px;overflow-y:auto"></div>
|
|
5647
6258
|
</div>
|
|
5648
6259
|
<div class="auto-card-footer">
|
|
5649
6260
|
<span class="auto-last-run" id="auto-observer-last">Last run: never</span>
|
|
5650
|
-
<
|
|
6261
|
+
<div style="display:flex;gap:6px">
|
|
6262
|
+
<button class="settings-btn auto-run-btn" onclick="runAutomation('observer')">Scan Only</button>
|
|
6263
|
+
<button class="settings-btn settings-btn-primary auto-run-btn" onclick="runObserverWithImprove()">Scan & Improve</button>
|
|
6264
|
+
</div>
|
|
5651
6265
|
</div>
|
|
5652
6266
|
</div>
|
|
5653
6267
|
</div>
|
|
@@ -5737,9 +6351,9 @@
|
|
|
5737
6351
|
<div class="settings-section-title">About</div>
|
|
5738
6352
|
<div class="settings-section-desc">wikimem — self-improving knowledge bases with LLMs</div>
|
|
5739
6353
|
<div style="color:var(--text-dim);font-size:13px;line-height:1.8">
|
|
5740
|
-
<p><strong style="color:var(--text)">Version:</strong> 0.
|
|
6354
|
+
<p><strong style="color:var(--text)">Version:</strong> 0.8.0</p>
|
|
5741
6355
|
<p><strong style="color:var(--text)">Author:</strong> Naman Parikh</p>
|
|
5742
|
-
<p><strong style="color:var(--text)">GitHub:</strong> <a href="https://github.com/naman10parikh/
|
|
6356
|
+
<p><strong style="color:var(--text)">GitHub:</strong> <a href="https://github.com/naman10parikh/wikimem" style="color:var(--accent)" target="_blank">naman10parikh/wikimem</a></p>
|
|
5743
6357
|
<p><strong style="color:var(--text)">npm:</strong> <a href="https://www.npmjs.com/package/wikimem" style="color:var(--accent)" target="_blank">wikimem</a></p>
|
|
5744
6358
|
<p><strong style="color:var(--text)">License:</strong> MIT</p>
|
|
5745
6359
|
</div>`;
|
|
@@ -5764,6 +6378,91 @@
|
|
|
5764
6378
|
|
|
5765
6379
|
// ── Automations Settings ──────────────────────────────────────────────
|
|
5766
6380
|
let autoSources = [];
|
|
6381
|
+
let _cxPairs = [];
|
|
6382
|
+
|
|
6383
|
+
function stripFrontmatter(md) {
|
|
6384
|
+
if (typeof md !== 'string') return '';
|
|
6385
|
+
const t = md.trim();
|
|
6386
|
+
if (t.startsWith('---')) {
|
|
6387
|
+
const end = t.indexOf('\n---', 3);
|
|
6388
|
+
if (end !== -1) return t.slice(end + 4).trim();
|
|
6389
|
+
}
|
|
6390
|
+
return t;
|
|
6391
|
+
}
|
|
6392
|
+
|
|
6393
|
+
function closeCxModal() {
|
|
6394
|
+
const o = document.getElementById('cx-modal-overlay');
|
|
6395
|
+
if (o) o.style.display = 'none';
|
|
6396
|
+
}
|
|
6397
|
+
|
|
6398
|
+
async function openContradictionCompare(idx) {
|
|
6399
|
+
const c = _cxPairs[idx];
|
|
6400
|
+
if (!c) return;
|
|
6401
|
+
const titleA = c.titleA || '';
|
|
6402
|
+
const titleB = c.titleB || '';
|
|
6403
|
+
document.getElementById('cx-modal-title').textContent = 'Potential contradiction';
|
|
6404
|
+
document.getElementById('cx-modal-reason').textContent = c.reason || '';
|
|
6405
|
+
document.getElementById('cx-head-a').textContent = titleA;
|
|
6406
|
+
document.getElementById('cx-head-b').textContent = titleB;
|
|
6407
|
+
document.getElementById('cx-body-a').textContent = 'Loading…';
|
|
6408
|
+
document.getElementById('cx-body-b').textContent = 'Loading…';
|
|
6409
|
+
const overlay = document.getElementById('cx-modal-overlay');
|
|
6410
|
+
if (overlay) overlay.style.display = 'flex';
|
|
6411
|
+
try {
|
|
6412
|
+
const [ra, rb] = await Promise.all([
|
|
6413
|
+
fetch('/api/pages/' + encodeURIComponent(titleA) + '/raw').then((r) => r.json()),
|
|
6414
|
+
fetch('/api/pages/' + encodeURIComponent(titleB) + '/raw').then((r) => r.json()),
|
|
6415
|
+
]);
|
|
6416
|
+
document.getElementById('cx-body-a').textContent = stripFrontmatter(ra.raw || '');
|
|
6417
|
+
document.getElementById('cx-body-b').textContent = stripFrontmatter(rb.raw || '');
|
|
6418
|
+
} catch (e) {
|
|
6419
|
+
document.getElementById('cx-body-a').textContent = 'Could not load page A.';
|
|
6420
|
+
document.getElementById('cx-body-b').textContent = 'Could not load page B.';
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
function renderContradictionRows(contradictions) {
|
|
6425
|
+
const container = document.getElementById('observer-cx-list');
|
|
6426
|
+
if (!container) return;
|
|
6427
|
+
if (!contradictions || contradictions.length === 0) {
|
|
6428
|
+
container.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No potential contradictions in the latest scan.</div>';
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
_cxPairs = contradictions;
|
|
6432
|
+
container.innerHTML = contradictions.slice(0, 14).map((c, i) => `
|
|
6433
|
+
<div class="cx-row">
|
|
6434
|
+
<div class="cx-row-reason">${esc(c.reason || '')}</div>
|
|
6435
|
+
<button type="button" class="settings-btn" style="font-size:11px;padding:3px 10px;flex-shrink:0" onclick="openContradictionCompare(${i})">Compare</button>
|
|
6436
|
+
</div>`).join('');
|
|
6437
|
+
}
|
|
6438
|
+
|
|
6439
|
+
async function refreshObserverReportPanel(partial) {
|
|
6440
|
+
const summaryEl = document.getElementById('auto-observer-last-report');
|
|
6441
|
+
if (!summaryEl) return;
|
|
6442
|
+
try {
|
|
6443
|
+
let report = partial;
|
|
6444
|
+
if (!report || !report.date) {
|
|
6445
|
+
const list = await api('/api/observer/reports');
|
|
6446
|
+
if (!list.reports || list.reports.length === 0) {
|
|
6447
|
+
summaryEl.innerHTML = 'No reports yet — run <strong>Scan Only</strong> or <strong>Scan & Improve</strong>.';
|
|
6448
|
+
renderContradictionRows([]);
|
|
6449
|
+
return;
|
|
6450
|
+
}
|
|
6451
|
+
const date = list.reports[0];
|
|
6452
|
+
report = await api('/api/observer/reports/' + encodeURIComponent(date));
|
|
6453
|
+
}
|
|
6454
|
+
const avg = report.averageScore != null ? report.averageScore : '—';
|
|
6455
|
+
const maxS = report.maxScore != null ? report.maxScore : '14';
|
|
6456
|
+
const orphans = report.orphanCount != null ? report.orphanCount : (report.orphans ? report.orphans.length : 0);
|
|
6457
|
+
const gaps = report.gapCount != null ? report.gapCount : (report.gaps ? report.gaps.length : 0);
|
|
6458
|
+
const cx = report.contradictionCount != null ? report.contradictionCount : (report.contradictions ? report.contradictions.length : 0);
|
|
6459
|
+
summaryEl.innerHTML = `Latest <span style="color:var(--text-secondary)">${esc(report.date || '')}</span> — avg score <strong>${avg}/${maxS}</strong>, ${orphans} orphans, ${gaps} gaps, <strong style="color:var(--amber)">${cx}</strong> contradiction flags`;
|
|
6460
|
+
renderContradictionRows(report.contradictions || []);
|
|
6461
|
+
} catch (e) {
|
|
6462
|
+
summaryEl.textContent = 'Could not load observer reports.';
|
|
6463
|
+
renderContradictionRows([]);
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
5767
6466
|
|
|
5768
6467
|
async function loadAutoSettings() {
|
|
5769
6468
|
try {
|
|
@@ -5790,7 +6489,7 @@
|
|
|
5790
6489
|
el('auto-observer-last').textContent = 'Last run: ' + formatTimeAgo(observer.lastRun);
|
|
5791
6490
|
|
|
5792
6491
|
if(el('auto-observer-last-report') && observer.lastReportPath) {
|
|
5793
|
-
el('auto-observer-last-report').innerHTML = `<
|
|
6492
|
+
el('auto-observer-last-report').innerHTML = `<span style="color:var(--text-muted)">Report file: </span><span style="font-family:var(--font-mono);font-size:11px">${esc(observer.lastReportPath)}</span>`;
|
|
5794
6493
|
}
|
|
5795
6494
|
|
|
5796
6495
|
if(el('auto-webhook-count') && webhook.count != null)
|
|
@@ -5802,6 +6501,8 @@
|
|
|
5802
6501
|
// Render sources list
|
|
5803
6502
|
autoSources = sourcing.sources || [];
|
|
5804
6503
|
renderAutoSources();
|
|
6504
|
+
|
|
6505
|
+
await refreshObserverReportPanel(null);
|
|
5805
6506
|
} catch(e) { /* settings endpoint may not exist yet */ }
|
|
5806
6507
|
}
|
|
5807
6508
|
|
|
@@ -5844,6 +6545,58 @@
|
|
|
5844
6545
|
navigator.clipboard.writeText(url).then(() => showToast('✓ Webhook URL copied'));
|
|
5845
6546
|
}
|
|
5846
6547
|
|
|
6548
|
+
// ── Appearance Preferences ──
|
|
6549
|
+
const APPEARANCE_DEFAULTS = { theme:'dark', fontSize:14, contentWidth:860, density:'comfortable', accent:'#4f9eff', codeLineNumbers:false };
|
|
6550
|
+
function loadAppearancePrefs() {
|
|
6551
|
+
try { return { ...APPEARANCE_DEFAULTS, ...JSON.parse(localStorage.getItem('wikimem-appearance')||'{}') }; }
|
|
6552
|
+
catch { return { ...APPEARANCE_DEFAULTS }; }
|
|
6553
|
+
}
|
|
6554
|
+
function saveAppearancePrefs(prefs) { localStorage.setItem('wikimem-appearance', JSON.stringify(prefs)); }
|
|
6555
|
+
function setAppearance(key, value) {
|
|
6556
|
+
const prefs = loadAppearancePrefs();
|
|
6557
|
+
prefs[key] = value;
|
|
6558
|
+
saveAppearancePrefs(prefs);
|
|
6559
|
+
applyAppearance(prefs);
|
|
6560
|
+
if(state.settingsSection === 'appearance') renderSettingsSection('appearance');
|
|
6561
|
+
}
|
|
6562
|
+
function resetAppearance() {
|
|
6563
|
+
localStorage.removeItem('wikimem-appearance');
|
|
6564
|
+
applyAppearance(APPEARANCE_DEFAULTS);
|
|
6565
|
+
renderSettingsSection('appearance');
|
|
6566
|
+
showToast('✓ Appearance reset');
|
|
6567
|
+
}
|
|
6568
|
+
function applyAppearance(prefs) {
|
|
6569
|
+
const r = document.documentElement;
|
|
6570
|
+
if(prefs.theme === 'light') {
|
|
6571
|
+
r.style.setProperty('--bg-deep','#f0f0f0'); r.style.setProperty('--bg-inset','#e8e8e8');
|
|
6572
|
+
r.style.setProperty('--bg','#ffffff'); r.style.setProperty('--bg-surface','#f5f5f5');
|
|
6573
|
+
r.style.setProperty('--bg-card','#eeeeee'); r.style.setProperty('--bg-hover','#e0e0e0');
|
|
6574
|
+
r.style.setProperty('--bg-active','#d5d5d5');
|
|
6575
|
+
r.style.setProperty('--text','#1e1e1e'); r.style.setProperty('--text-secondary','#555');
|
|
6576
|
+
r.style.setProperty('--text-bright','#000'); r.style.setProperty('--text-dim','#888');
|
|
6577
|
+
r.style.setProperty('--text-muted','#999');
|
|
6578
|
+
r.style.setProperty('--border','#d0d0d0'); r.style.setProperty('--border-subtle','#e0e0e0');
|
|
6579
|
+
} else {
|
|
6580
|
+
r.style.removeProperty('--bg-deep'); r.style.removeProperty('--bg-inset');
|
|
6581
|
+
r.style.removeProperty('--bg'); r.style.removeProperty('--bg-surface');
|
|
6582
|
+
r.style.removeProperty('--bg-card'); r.style.removeProperty('--bg-hover');
|
|
6583
|
+
r.style.removeProperty('--bg-active');
|
|
6584
|
+
r.style.removeProperty('--text'); r.style.removeProperty('--text-secondary');
|
|
6585
|
+
r.style.removeProperty('--text-bright'); r.style.removeProperty('--text-dim');
|
|
6586
|
+
r.style.removeProperty('--text-muted');
|
|
6587
|
+
r.style.removeProperty('--border'); r.style.removeProperty('--border-subtle');
|
|
6588
|
+
}
|
|
6589
|
+
r.style.setProperty('--font-size-base', prefs.fontSize + 'px');
|
|
6590
|
+
r.style.setProperty('--accent', prefs.accent);
|
|
6591
|
+
r.style.setProperty('--accent-dim', prefs.accent + '22');
|
|
6592
|
+
document.querySelectorAll('.page-layout').forEach(el => {
|
|
6593
|
+
el.style.maxWidth = typeof prefs.contentWidth === 'number' ? prefs.contentWidth + 'px' : prefs.contentWidth;
|
|
6594
|
+
});
|
|
6595
|
+
const densityPad = prefs.density === 'compact' ? '2px' : prefs.density === 'spacious' ? '6px' : '4px';
|
|
6596
|
+
r.style.setProperty('--density-pad', densityPad);
|
|
6597
|
+
}
|
|
6598
|
+
applyAppearance(loadAppearancePrefs());
|
|
6599
|
+
|
|
5847
6600
|
async function saveAutoSetting(automation, key, value) {
|
|
5848
6601
|
try {
|
|
5849
6602
|
await fetch('/api/automations/settings', {
|
|
@@ -5863,15 +6616,98 @@
|
|
|
5863
6616
|
const lastEl = document.getElementById(`auto-${type}-last`);
|
|
5864
6617
|
if(lastEl) { lastEl.textContent = '⟳ Running…'; }
|
|
5865
6618
|
try {
|
|
5866
|
-
await
|
|
6619
|
+
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
6620
|
+
const result = await res.json();
|
|
5867
6621
|
if(lastEl) { lastEl.textContent = 'Last run: just now'; }
|
|
5868
6622
|
const s = document.getElementById('automations-status');
|
|
5869
|
-
if(s) { s.className='settings-status ok'; s.textContent='✓
|
|
6623
|
+
if(s) { s.className='settings-status ok'; s.textContent='✓ Complete'; setTimeout(()=>s.textContent='', 3000); }
|
|
6624
|
+
if(type === 'observer') {
|
|
6625
|
+
const cx = result.contradictionCount != null ? result.contradictionCount : 0;
|
|
6626
|
+
showToast(`Observer: ${result.pagesReviewed} pages, avg ${result.averageScore}/${result.maxScore}, ${result.orphanCount} orphans, ${cx} contradiction flags`);
|
|
6627
|
+
refreshObserverReportPanel(result);
|
|
6628
|
+
renderObserverDetailPanel(result);
|
|
6629
|
+
}
|
|
5870
6630
|
} catch(e) {
|
|
5871
6631
|
if(lastEl) { lastEl.textContent = '✗ Failed to start'; }
|
|
5872
6632
|
}
|
|
5873
6633
|
}
|
|
5874
6634
|
|
|
6635
|
+
async function runObserverWithImprove() {
|
|
6636
|
+
const lastEl = document.getElementById('auto-observer-last');
|
|
6637
|
+
if(lastEl) { lastEl.textContent = '⟳ Scanning & improving…'; }
|
|
6638
|
+
try {
|
|
6639
|
+
const modelSel = document.getElementById('auto-observer-model');
|
|
6640
|
+
const model = modelSel ? modelSel.value : '';
|
|
6641
|
+
const body = { autoImprove: true, maxImprovements: 3 };
|
|
6642
|
+
if (model) body.model = model;
|
|
6643
|
+
const res = await fetch('/api/observer/run', {
|
|
6644
|
+
method: 'POST',
|
|
6645
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6646
|
+
body: JSON.stringify(body)
|
|
6647
|
+
});
|
|
6648
|
+
const result = await res.json();
|
|
6649
|
+
if(lastEl) { lastEl.textContent = 'Last run: just now'; }
|
|
6650
|
+
const improved = result.improvementCount || 0;
|
|
6651
|
+
showToast(`Observer: ${result.pagesReviewed} pages scanned, ${improved} pages improved, avg score ${result.averageScore}/${result.maxScore}`);
|
|
6652
|
+
refreshObserverReportPanel(result);
|
|
6653
|
+
renderObserverDetailPanel(result);
|
|
6654
|
+
if(improved > 0) { loadTree(); loadHome(); }
|
|
6655
|
+
} catch(e) {
|
|
6656
|
+
if(lastEl) { lastEl.textContent = '✗ Failed'; }
|
|
6657
|
+
showToast('Observer run failed: ' + (e.message || e));
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
|
|
6661
|
+
function renderObserverDetailPanel(result) {
|
|
6662
|
+
const panel = document.getElementById('observer-report-detail');
|
|
6663
|
+
if (!panel) return;
|
|
6664
|
+
let html = '';
|
|
6665
|
+
const pct = result.maxScore > 0 ? Math.round((result.averageScore / result.maxScore) * 100) : 0;
|
|
6666
|
+
const barColor = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--amber)' : 'var(--red)';
|
|
6667
|
+
html += '<div style="margin-bottom:12px">'
|
|
6668
|
+
+ '<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">'
|
|
6669
|
+
+ '<span style="color:var(--text-secondary)">Average Quality</span>'
|
|
6670
|
+
+ '<span style="color:' + barColor + ';font-weight:600">' + result.averageScore + '/' + result.maxScore + ' (' + pct + '%)</span></div>'
|
|
6671
|
+
+ '<div style="height:6px;background:var(--border);border-radius:3px;overflow:hidden">'
|
|
6672
|
+
+ '<div style="height:100%;width:' + pct + '%;background:' + barColor + ';border-radius:3px;transition:width 0.5s"></div></div></div>';
|
|
6673
|
+
html += '<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">'
|
|
6674
|
+
+ '<span style="font-size:11px;padding:3px 7px;background:var(--bg-surface);border-radius:var(--radius-sm);border:1px solid var(--border)">📊 ' + result.totalPages + ' pages</span>'
|
|
6675
|
+
+ '<span style="font-size:11px;padding:3px 7px;background:var(--bg-surface);border-radius:var(--radius-sm);border:1px solid var(--border)">🔗 ' + result.orphanCount + ' orphans</span>'
|
|
6676
|
+
+ '<span style="font-size:11px;padding:3px 7px;background:var(--bg-surface);border-radius:var(--radius-sm);border:1px solid var(--border)">🕳️ ' + result.gapCount + ' gaps</span>'
|
|
6677
|
+
+ '<span style="font-size:11px;padding:3px 7px;background:var(--bg-surface);border-radius:var(--radius-sm);border:1px solid var(--border)">⚡ ' + result.contradictionCount + ' contradictions</span></div>';
|
|
6678
|
+
if (result.improvements && result.improvements.length > 0) {
|
|
6679
|
+
html += '<div style="margin-bottom:12px"><div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;font-weight:500">Improvements</div>';
|
|
6680
|
+
for (const imp of result.improvements) {
|
|
6681
|
+
const icon = imp.improved ? '✅' : '❌';
|
|
6682
|
+
const sc = imp.newScore != null ? (imp.originalScore + ' → ' + imp.newScore) : ('was ' + imp.originalScore);
|
|
6683
|
+
const bg = imp.improved ? 'var(--green-dim)' : 'var(--red-dim)';
|
|
6684
|
+
html += '<div style="font-size:11px;padding:5px 8px;margin-bottom:3px;background:' + bg + ';border-radius:var(--radius-sm)">'
|
|
6685
|
+
+ icon + ' <strong>' + esc(imp.title) + '</strong> <span style="color:var(--text-muted)">(' + sc + '/' + result.maxScore + ')</span>'
|
|
6686
|
+
+ '<div style="color:var(--text-dim);margin-top:2px;font-size:10px">' + esc(imp.action) + '</div></div>';
|
|
6687
|
+
}
|
|
6688
|
+
html += '</div>';
|
|
6689
|
+
}
|
|
6690
|
+
if (result.weakestPages && result.weakestPages.length > 0) {
|
|
6691
|
+
html += '<div style="margin-bottom:8px"><div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;font-weight:500">Weakest Pages</div>';
|
|
6692
|
+
for (const p of result.weakestPages) {
|
|
6693
|
+
html += '<div style="font-size:11px;padding:3px 8px;margin-bottom:2px;display:flex;justify-content:space-between;background:var(--bg-surface);border-radius:var(--radius-sm);border:1px solid var(--border)">'
|
|
6694
|
+
+ '<span>' + esc(p.title) + '</span>'
|
|
6695
|
+
+ '<span style="color:var(--amber);font-weight:500">' + p.score + '/' + p.maxScore + '</span></div>';
|
|
6696
|
+
}
|
|
6697
|
+
html += '</div>';
|
|
6698
|
+
}
|
|
6699
|
+
if (result.topIssues && result.topIssues.length > 0) {
|
|
6700
|
+
html += '<div><div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;font-weight:500">Top Issues</div>';
|
|
6701
|
+
for (const iss of result.topIssues.slice(0, 5)) {
|
|
6702
|
+
html += '<div style="font-size:11px;padding:2px 0;color:var(--text-dim)">'
|
|
6703
|
+
+ '<span style="color:var(--amber);font-weight:500">' + iss.count + '×</span> ' + esc(iss.issue) + '</div>';
|
|
6704
|
+
}
|
|
6705
|
+
html += '</div>';
|
|
6706
|
+
}
|
|
6707
|
+
panel.innerHTML = html;
|
|
6708
|
+
panel.style.display = 'block';
|
|
6709
|
+
}
|
|
6710
|
+
|
|
5875
6711
|
// ── Pipeline Configuration ───────────────────────────────────────────
|
|
5876
6712
|
const CORE_STEPS = [
|
|
5877
6713
|
{ id: 'detect', icon: '🔍', label: 'Detect' },
|
|
@@ -6402,6 +7238,7 @@
|
|
|
6402
7238
|
}
|
|
6403
7239
|
onboardEl.style.display = 'none';
|
|
6404
7240
|
dashEl.style.display = '';
|
|
7241
|
+
renderHomeConnectors();
|
|
6405
7242
|
|
|
6406
7243
|
const statusText = `${stats.pageCount} pages · ${stats.wordCount.toLocaleString()} words`;
|
|
6407
7244
|
document.querySelectorAll('#status-text').forEach(el => el.textContent = statusText);
|
|
@@ -6517,6 +7354,12 @@
|
|
|
6517
7354
|
: `<div id="search-empty">Type to search across all pages, content, and metadata</div>`;
|
|
6518
7355
|
return;
|
|
6519
7356
|
}
|
|
7357
|
+
// Check if we can just update selection classes (avoids DOM destruction on hover)
|
|
7358
|
+
const existingItems = container.querySelectorAll('.search-result');
|
|
7359
|
+
if (existingItems.length === state.searchResults.length && existingItems.length > 0) {
|
|
7360
|
+
existingItems.forEach((el, i) => el.classList.toggle('selected', i === state.searchIndex));
|
|
7361
|
+
return;
|
|
7362
|
+
}
|
|
6520
7363
|
container.innerHTML = state.searchResults.map((r,i) => {
|
|
6521
7364
|
const snippet = r.snippet ? `<div style="font-size:11px;color:var(--text-muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px">${esc(r.snippet)}</div>` : '';
|
|
6522
7365
|
const matchIcon = r.matchType === 'content' ? '📝' : '◇';
|
|
@@ -6785,6 +7628,22 @@
|
|
|
6785
7628
|
if((e.metaKey||e.ctrlKey) && e.key==='g') { e.preventDefault(); railGraphClick(); }
|
|
6786
7629
|
if((e.metaKey||e.ctrlKey) && e.key===',') { e.preventDefault(); railSettingsClick(); }
|
|
6787
7630
|
if((e.metaKey||e.ctrlKey) && e.key==='w') { e.preventDefault(); if(state.activeTabId) closeTab(state.activeTabId); }
|
|
7631
|
+
// Cmd+N — new note
|
|
7632
|
+
if((e.metaKey||e.ctrlKey) && e.key==='n') {
|
|
7633
|
+
e.preventDefault();
|
|
7634
|
+
document.getElementById('sa-new-note')?.click();
|
|
7635
|
+
}
|
|
7636
|
+
// Cmd+Tab / Ctrl+Tab — next tab
|
|
7637
|
+
if((e.ctrlKey || e.metaKey) && e.key==='Tab') {
|
|
7638
|
+
e.preventDefault();
|
|
7639
|
+
if(state.tabs.length > 1) {
|
|
7640
|
+
const idx = state.tabs.findIndex(t => t.id === state.activeTabId);
|
|
7641
|
+
const next = e.shiftKey
|
|
7642
|
+
? (idx - 1 + state.tabs.length) % state.tabs.length
|
|
7643
|
+
: (idx + 1) % state.tabs.length;
|
|
7644
|
+
activateTab(state.tabs[next].id);
|
|
7645
|
+
}
|
|
7646
|
+
}
|
|
6788
7647
|
if(e.key==='Escape') { closeSearch(); closePalette(); }
|
|
6789
7648
|
});
|
|
6790
7649
|
|
|
@@ -7272,16 +8131,253 @@
|
|
|
7272
8131
|
loadTree(); loadHome();
|
|
7273
8132
|
}
|
|
7274
8133
|
|
|
8134
|
+
// ── Open Settings to a specific section ──
|
|
8135
|
+
function openSettings(section) {
|
|
8136
|
+
showView('settings');
|
|
8137
|
+
// showView triggers loadSettings() which renders the default section;
|
|
8138
|
+
// after config loads, switch to the requested section
|
|
8139
|
+
const waitForSettings = () => {
|
|
8140
|
+
if (document.getElementById('settings-content')) {
|
|
8141
|
+
renderSettingsSection(section || 'sources');
|
|
8142
|
+
// Highlight the matching nav item
|
|
8143
|
+
document.querySelectorAll('.settings-nav-item').forEach(el => {
|
|
8144
|
+
el.classList.toggle('active', el.dataset.section === (section || 'sources'));
|
|
8145
|
+
});
|
|
8146
|
+
} else {
|
|
8147
|
+
requestAnimationFrame(waitForSettings);
|
|
8148
|
+
}
|
|
8149
|
+
};
|
|
8150
|
+
requestAnimationFrame(waitForSettings);
|
|
8151
|
+
}
|
|
8152
|
+
|
|
7275
8153
|
// ── OAuth Connect ──
|
|
7276
8154
|
async function startOAuthConnect(provider) {
|
|
7277
8155
|
try {
|
|
7278
8156
|
const res = await fetch('/api/auth/start/' + encodeURIComponent(provider));
|
|
7279
8157
|
const data = await res.json();
|
|
7280
|
-
if (data.error
|
|
7281
|
-
|
|
8158
|
+
if (data.error === 'no_credentials') {
|
|
8159
|
+
// GitHub: try device flow (no client_secret needed — just needs client_id)
|
|
8160
|
+
if (provider === 'github') {
|
|
8161
|
+
showPipelineDeviceFlow();
|
|
8162
|
+
return;
|
|
8163
|
+
}
|
|
8164
|
+
// Show inline credential setup modal — don't redirect to Settings
|
|
8165
|
+
showCredentialSetupModal(provider);
|
|
8166
|
+
return;
|
|
8167
|
+
}
|
|
8168
|
+
if (data.error) { showToast('OAuth: ' + (data.message || data.error)); return; }
|
|
8169
|
+
if (data.url) {
|
|
8170
|
+
const popup = window.open(data.url, '_blank', 'width=600,height=700');
|
|
8171
|
+
// Poll for completion — when the popup closes, refresh status
|
|
8172
|
+
if (popup) {
|
|
8173
|
+
const poll = setInterval(() => {
|
|
8174
|
+
if (popup.closed) { clearInterval(poll); loadOAuthStatus(); renderOAuthIntegrations(); }
|
|
8175
|
+
}, 1000);
|
|
8176
|
+
}
|
|
8177
|
+
}
|
|
7282
8178
|
} catch (err) { showToast('OAuth failed: ' + (err.message || 'unknown')); }
|
|
7283
8179
|
}
|
|
7284
8180
|
|
|
8181
|
+
// Listen for OAuth completion postMessage from callback popup
|
|
8182
|
+
window.addEventListener('message', function(event) {
|
|
8183
|
+
if (event.data && event.data.type === 'wikimem-oauth-connected' && event.data.provider) {
|
|
8184
|
+
showToast(event.data.provider.charAt(0).toUpperCase() + event.data.provider.slice(1) + ' connected — syncing data...');
|
|
8185
|
+
loadOAuthStatus();
|
|
8186
|
+
renderOAuthIntegrations();
|
|
8187
|
+
renderHomeConnectors();
|
|
8188
|
+
loadSidebarConnectors();
|
|
8189
|
+
syncOAuthProvider(event.data.provider);
|
|
8190
|
+
}
|
|
8191
|
+
});
|
|
8192
|
+
|
|
8193
|
+
// Pipeline-page device flow modal for GitHub (no client_secret needed)
|
|
8194
|
+
async function showPipelineDeviceFlow() {
|
|
8195
|
+
// Create modal overlay
|
|
8196
|
+
let modal = document.getElementById('device-flow-modal');
|
|
8197
|
+
if (modal) modal.remove();
|
|
8198
|
+
modal = document.createElement('div');
|
|
8199
|
+
modal.id = 'device-flow-modal';
|
|
8200
|
+
modal.style.cssText = 'position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px)';
|
|
8201
|
+
modal.innerHTML = `<div style="background:var(--bg-primary);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:420px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.4)">
|
|
8202
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
|
8203
|
+
<h3 style="margin:0;font-size:15px;color:var(--text-bright)">Connect GitHub</h3>
|
|
8204
|
+
<button onclick="document.getElementById('device-flow-modal').remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px">×</button>
|
|
8205
|
+
</div>
|
|
8206
|
+
<p style="color:var(--text-secondary);font-size:12px;margin:0 0 16px">No app registration needed — uses GitHub Device Flow.</p>
|
|
8207
|
+
<div id="pipeline-device-flow-content" style="text-align:center;padding:8px 0">
|
|
8208
|
+
<div class="device-flow-spinner" style="margin:0 auto"></div>
|
|
8209
|
+
<div style="margin-top:8px;color:var(--text-muted);font-size:12px">Starting device flow...</div>
|
|
8210
|
+
</div>
|
|
8211
|
+
</div>`;
|
|
8212
|
+
document.body.appendChild(modal);
|
|
8213
|
+
modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
|
|
8214
|
+
|
|
8215
|
+
// Start device flow
|
|
8216
|
+
try {
|
|
8217
|
+
const res = await fetch('/api/auth/device-flow/start', { method: 'POST' });
|
|
8218
|
+
const data = await res.json();
|
|
8219
|
+
const content = document.getElementById('pipeline-device-flow-content');
|
|
8220
|
+
if (!content) return;
|
|
8221
|
+
|
|
8222
|
+
if (data.error === 'no_client_id') {
|
|
8223
|
+
content.innerHTML = `<div style="text-align:left">
|
|
8224
|
+
<p style="color:var(--text-secondary);font-size:12px;margin:0 0 12px">Device flow requires a GitHub OAuth App client ID. Set the env var or enter credentials:</p>
|
|
8225
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
8226
|
+
<input class="settings-input" id="pipeline-gh-id" type="text" placeholder="GitHub Client ID" style="width:100%;box-sizing:border-box" />
|
|
8227
|
+
<input class="settings-input" id="pipeline-gh-secret" type="password" placeholder="GitHub Client Secret" style="width:100%;box-sizing:border-box" />
|
|
8228
|
+
<button class="settings-btn settings-btn-primary" onclick="savePipelineGitHubCreds()" style="font-size:12px;padding:6px 16px;align-self:flex-end">Save & Connect</button>
|
|
8229
|
+
</div>
|
|
8230
|
+
</div>`;
|
|
8231
|
+
return;
|
|
8232
|
+
}
|
|
8233
|
+
|
|
8234
|
+
if (data.error) {
|
|
8235
|
+
content.innerHTML = '<div style="color:var(--red);font-size:12px">' + (data.message || data.error) + '</div>';
|
|
8236
|
+
return;
|
|
8237
|
+
}
|
|
8238
|
+
|
|
8239
|
+
// Show device code
|
|
8240
|
+
content.innerHTML = `<div style="text-align:left">
|
|
8241
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:12px">
|
|
8242
|
+
1. Go to <a href="${data.verification_uri}" target="_blank" style="color:var(--accent)">${data.verification_uri}</a><br/>
|
|
8243
|
+
2. Enter this code:
|
|
8244
|
+
</div>
|
|
8245
|
+
<div style="font-size:28px;font-weight:700;letter-spacing:4px;text-align:center;padding:12px;background:var(--bg-secondary);border-radius:8px;color:var(--text-bright);font-family:monospace;margin-bottom:8px">${data.user_code}</div>
|
|
8246
|
+
<div style="text-align:center;margin-bottom:12px">
|
|
8247
|
+
<button class="settings-btn" onclick="navigator.clipboard.writeText('${data.user_code}');this.textContent='Copied!';setTimeout(()=>this.textContent='Copy Code',1500)" style="font-size:11px;padding:4px 12px">Copy Code</button>
|
|
8248
|
+
</div>
|
|
8249
|
+
<div id="pipeline-device-status" style="text-align:center;font-size:12px;color:var(--text-muted)">
|
|
8250
|
+
<span class="device-flow-spinner" style="display:inline-block;width:12px;height:12px;border-width:2px;vertical-align:middle;margin-right:6px"></span>
|
|
8251
|
+
Waiting for authorization...
|
|
8252
|
+
</div>
|
|
8253
|
+
</div>`;
|
|
8254
|
+
|
|
8255
|
+
// Poll for completion
|
|
8256
|
+
pollPipelineDeviceFlow(data.device_code, data.interval || 5);
|
|
8257
|
+
} catch (err) {
|
|
8258
|
+
const content = document.getElementById('pipeline-device-flow-content');
|
|
8259
|
+
if (content) content.innerHTML = '<div style="color:var(--red);font-size:12px">Failed to start: ' + (err.message || 'unknown') + '</div>';
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8262
|
+
|
|
8263
|
+
function pollPipelineDeviceFlow(deviceCode, interval) {
|
|
8264
|
+
const pollMs = Math.max((interval || 5) * 1000, 5000);
|
|
8265
|
+
const timer = setInterval(async () => {
|
|
8266
|
+
try {
|
|
8267
|
+
const res = await fetch('/api/auth/device-flow/poll', {
|
|
8268
|
+
method: 'POST',
|
|
8269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8270
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
8271
|
+
});
|
|
8272
|
+
const data = await res.json();
|
|
8273
|
+
const statusEl = document.getElementById('pipeline-device-status');
|
|
8274
|
+
|
|
8275
|
+
if (data.status === 'complete') {
|
|
8276
|
+
clearInterval(timer);
|
|
8277
|
+
if (statusEl) statusEl.innerHTML = '<span style="color:var(--green);font-weight:600">Connected! Syncing your data...</span>';
|
|
8278
|
+
showToast('GitHub connected — syncing data...');
|
|
8279
|
+
loadOAuthStatus();
|
|
8280
|
+
syncOAuthProvider('github');
|
|
8281
|
+
// Close modal after brief delay
|
|
8282
|
+
setTimeout(() => { const m = document.getElementById('device-flow-modal'); if (m) m.remove(); }, 2500);
|
|
8283
|
+
} else if (data.status === 'expired') {
|
|
8284
|
+
clearInterval(timer);
|
|
8285
|
+
if (statusEl) statusEl.innerHTML = '<span style="color:var(--red)">Code expired. </span><a href="#" onclick="event.preventDefault();document.getElementById(\'device-flow-modal\').remove();showPipelineDeviceFlow()" style="color:var(--accent)">Try again</a>';
|
|
8286
|
+
}
|
|
8287
|
+
} catch { /* network error — keep trying */ }
|
|
8288
|
+
}, pollMs);
|
|
8289
|
+
}
|
|
8290
|
+
|
|
8291
|
+
async function savePipelineGitHubCreds() {
|
|
8292
|
+
const idInput = document.getElementById('pipeline-gh-id');
|
|
8293
|
+
const secretInput = document.getElementById('pipeline-gh-secret');
|
|
8294
|
+
if (!idInput || !secretInput) return;
|
|
8295
|
+
const clientId = idInput.value.trim();
|
|
8296
|
+
const clientSecret = secretInput.value.trim();
|
|
8297
|
+
if (!clientId || !clientSecret) { showToast('Enter both Client ID and Secret'); return; }
|
|
8298
|
+
try {
|
|
8299
|
+
const res = await fetch('/api/config', {
|
|
8300
|
+
method: 'PUT',
|
|
8301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8302
|
+
body: JSON.stringify({ github_client_id: clientId, github_client_secret: clientSecret }),
|
|
8303
|
+
});
|
|
8304
|
+
const data = await res.json();
|
|
8305
|
+
if (data.error) throw new Error(data.error);
|
|
8306
|
+
showToast('GitHub credentials saved — connecting...');
|
|
8307
|
+
const modal = document.getElementById('device-flow-modal');
|
|
8308
|
+
if (modal) modal.remove();
|
|
8309
|
+
startOAuthConnect('github');
|
|
8310
|
+
} catch (err) { showToast('Save failed: ' + (err.message || 'unknown')); }
|
|
8311
|
+
}
|
|
8312
|
+
|
|
8313
|
+
// ── Credential Setup Modal (shown when user clicks Connect but no credentials exist) ──
|
|
8314
|
+
function showCredentialSetupModal(provider) {
|
|
8315
|
+
const meta = OAUTH_PROVIDER_META[provider];
|
|
8316
|
+
if (!meta) return;
|
|
8317
|
+
const port = location.port || '3456';
|
|
8318
|
+
const callbackUrl = `http://localhost:${port}/api/auth/callback`;
|
|
8319
|
+
const stepsHtml = meta.steps.map(s =>
|
|
8320
|
+
`<li>${s.replace(/CALLBACK/g, '<code style="font-size:11px;color:var(--accent)">' + esc(callbackUrl) + '</code>').replace(/PORT/g, port)}</li>`
|
|
8321
|
+
).join('');
|
|
8322
|
+
|
|
8323
|
+
let modal = document.getElementById('cred-setup-modal');
|
|
8324
|
+
if (modal) modal.remove();
|
|
8325
|
+
modal = document.createElement('div');
|
|
8326
|
+
modal.id = 'cred-setup-modal';
|
|
8327
|
+
modal.style.cssText = 'position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px)';
|
|
8328
|
+
modal.innerHTML = `<div style="background:var(--bg-primary);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.4);max-height:80vh;overflow-y:auto">
|
|
8329
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
|
8330
|
+
<h3 style="margin:0;font-size:15px;color:var(--text-bright)">Connect ${meta.name}</h3>
|
|
8331
|
+
<button onclick="document.getElementById('cred-setup-modal').remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px">×</button>
|
|
8332
|
+
</div>
|
|
8333
|
+
<p style="color:var(--text-secondary);font-size:12px;margin:0 0 16px;line-height:1.5">
|
|
8334
|
+
Enter your ${meta.name} OAuth credentials to connect. You can set them as environment variables or enter them below.
|
|
8335
|
+
</p>
|
|
8336
|
+
<div style="background:var(--bg-surface);border-radius:8px;padding:12px;margin-bottom:16px">
|
|
8337
|
+
<div style="font-size:11px;color:var(--text-muted);margin-bottom:4px">Environment variables</div>
|
|
8338
|
+
<code style="font-size:11px;color:var(--accent);word-break:break-all">WIKIMEM_${provider.toUpperCase()}_CLIENT_ID</code><br/>
|
|
8339
|
+
<code style="font-size:11px;color:var(--accent);word-break:break-all">WIKIMEM_${provider.toUpperCase()}_CLIENT_SECRET</code>
|
|
8340
|
+
</div>
|
|
8341
|
+
<details style="margin-bottom:16px">
|
|
8342
|
+
<summary style="font-size:12px;color:var(--text-muted);cursor:pointer;font-weight:500">How to register a ${meta.name} OAuth app</summary>
|
|
8343
|
+
<ol class="oauth-setup-steps" style="margin-top:8px">${stepsHtml}</ol>
|
|
8344
|
+
</details>
|
|
8345
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
8346
|
+
<input class="settings-input" id="cred-modal-id" type="text" placeholder="Client ID" style="width:100%;box-sizing:border-box" />
|
|
8347
|
+
<input class="settings-input" id="cred-modal-secret" type="password" placeholder="Client Secret" style="width:100%;box-sizing:border-box" />
|
|
8348
|
+
<button class="settings-btn settings-btn-primary" onclick="saveCredModalAndConnect('${provider}')" style="font-size:12px;padding:8px 16px;align-self:flex-end">Save & Connect</button>
|
|
8349
|
+
</div>
|
|
8350
|
+
</div>`;
|
|
8351
|
+
document.body.appendChild(modal);
|
|
8352
|
+
modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
|
|
8353
|
+
}
|
|
8354
|
+
|
|
8355
|
+
async function saveCredModalAndConnect(provider) {
|
|
8356
|
+
const meta = OAUTH_PROVIDER_META[provider];
|
|
8357
|
+
const idInput = document.getElementById('cred-modal-id');
|
|
8358
|
+
const secretInput = document.getElementById('cred-modal-secret');
|
|
8359
|
+
if (!idInput || !secretInput) return;
|
|
8360
|
+
const clientId = idInput.value.trim();
|
|
8361
|
+
const clientSecret = secretInput.value.trim();
|
|
8362
|
+
if (!clientId || !clientSecret) { showToast('Enter both Client ID and Secret'); return; }
|
|
8363
|
+
try {
|
|
8364
|
+
const body = {};
|
|
8365
|
+
body[meta.idKey] = clientId;
|
|
8366
|
+
body[meta.secretKey] = clientSecret;
|
|
8367
|
+
const res = await fetch('/api/config', {
|
|
8368
|
+
method: 'PUT',
|
|
8369
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8370
|
+
body: JSON.stringify(body),
|
|
8371
|
+
});
|
|
8372
|
+
const data = await res.json();
|
|
8373
|
+
if (data.error) throw new Error(data.error);
|
|
8374
|
+
showToast(`${meta.name} credentials saved — connecting...`);
|
|
8375
|
+
const modal = document.getElementById('cred-setup-modal');
|
|
8376
|
+
if (modal) modal.remove();
|
|
8377
|
+
startOAuthConnect(provider);
|
|
8378
|
+
} catch (err) { showToast('Save failed: ' + (err.message || 'unknown')); }
|
|
8379
|
+
}
|
|
8380
|
+
|
|
7285
8381
|
async function loadOAuthStatus() {
|
|
7286
8382
|
try {
|
|
7287
8383
|
const data = await fetch('/api/auth/tokens').then(r => r.json());
|
|
@@ -7290,18 +8386,379 @@
|
|
|
7290
8386
|
if (!badge) continue;
|
|
7291
8387
|
if (info.connected) {
|
|
7292
8388
|
badge.innerHTML = '<span class="connected-badge">Connected</span>';
|
|
8389
|
+
} else if (info.hasCredentials || info.hasDeviceFlow) {
|
|
8390
|
+
badge.innerHTML = '<button class="connect-btn" onclick="event.stopPropagation();startOAuthConnect(\'' + provider + '\')">Connect</button>';
|
|
7293
8391
|
} else {
|
|
7294
8392
|
badge.innerHTML = '<button class="connect-btn" onclick="event.stopPropagation();startOAuthConnect(\'' + provider + '\')">Connect</button>';
|
|
7295
8393
|
}
|
|
7296
8394
|
}
|
|
7297
8395
|
// Gmail mirrors google token
|
|
7298
8396
|
const gmailBadge = document.getElementById('oauth-badge-gmail');
|
|
7299
|
-
if (gmailBadge && data.google
|
|
7300
|
-
|
|
8397
|
+
if (gmailBadge && data.google) {
|
|
8398
|
+
if (data.google.connected) {
|
|
8399
|
+
gmailBadge.innerHTML = '<span class="connected-badge">Connected</span>';
|
|
8400
|
+
} else {
|
|
8401
|
+
gmailBadge.innerHTML = '<button class="connect-btn" onclick="event.stopPropagation();startOAuthConnect(\'google\')">Connect</button>';
|
|
8402
|
+
}
|
|
7301
8403
|
}
|
|
7302
8404
|
} catch {}
|
|
7303
8405
|
}
|
|
7304
8406
|
|
|
8407
|
+
async function renderHomeConnectors() {
|
|
8408
|
+
const container = document.getElementById('home-oauth-cards');
|
|
8409
|
+
if (!container) return;
|
|
8410
|
+
const providers = ['github', 'slack', 'google', 'linear', 'jira'];
|
|
8411
|
+
let tokenData = {};
|
|
8412
|
+
try { tokenData = await fetch('/api/auth/tokens').then(r => r.json()); } catch {}
|
|
8413
|
+
container.innerHTML = providers.map(p => {
|
|
8414
|
+
const meta = OAUTH_PROVIDER_META[p];
|
|
8415
|
+
if (!meta) return '';
|
|
8416
|
+
const info = tokenData[p] || {};
|
|
8417
|
+
const connected = !!info.connected;
|
|
8418
|
+
const btnHtml = connected
|
|
8419
|
+
? '<span style="font-size:10px;color:#34d399;font-weight:600">Connected</span>'
|
|
8420
|
+
: `<button class="connect-btn" onclick="event.stopPropagation();startOAuthConnect('${p}')" style="font-size:10px;padding:2px 8px">Connect</button>`;
|
|
8421
|
+
return `<div style="display:flex;align-items:center;gap:8px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:8px 12px;min-width:140px;cursor:pointer" onclick="showView('settings');setTimeout(()=>{const t=document.querySelector('.settings-nav-item[data-section=sources]');if(t)t.click()},100)">
|
|
8422
|
+
<span style="color:var(--text-secondary);flex-shrink:0">${meta.icon}</span>
|
|
8423
|
+
<span style="font-size:12px;color:var(--text-bright);font-weight:500;flex:1">${meta.name}</span>
|
|
8424
|
+
${btnHtml}
|
|
8425
|
+
</div>`;
|
|
8426
|
+
}).join('');
|
|
8427
|
+
}
|
|
8428
|
+
|
|
8429
|
+
// ── OAuth Setup Wizard ──
|
|
8430
|
+
const OAUTH_PROVIDER_META = {
|
|
8431
|
+
github: {
|
|
8432
|
+
name: 'GitHub',
|
|
8433
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><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"/></svg>',
|
|
8434
|
+
desc: 'Repos, issues, PRs, and starred projects',
|
|
8435
|
+
idKey: 'github_client_id', secretKey: 'github_client_secret',
|
|
8436
|
+
steps: [
|
|
8437
|
+
'Go to <a href="https://github.com/settings/developers" target="_blank" style="color:var(--accent)">GitHub → Settings → Developer Settings → OAuth Apps</a>',
|
|
8438
|
+
'Click <strong>New OAuth App</strong>',
|
|
8439
|
+
'Set <strong>Homepage URL</strong> to <code>http://localhost:PORT</code>',
|
|
8440
|
+
'Set <strong>Authorization callback URL</strong> to <code>CALLBACK</code>',
|
|
8441
|
+
'Copy the <strong>Client ID</strong> and generate a <strong>Client Secret</strong>',
|
|
8442
|
+
'Paste both values below and click <strong>Save Credentials</strong>',
|
|
8443
|
+
],
|
|
8444
|
+
},
|
|
8445
|
+
slack: {
|
|
8446
|
+
name: 'Slack',
|
|
8447
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.164 0a2.528 2.528 0 0 1 2.521 2.522v6.312zM15.164 18.956a2.528 2.528 0 0 1 2.521 2.522A2.528 2.528 0 0 1 15.164 24a2.528 2.528 0 0 1-2.521-2.522v-2.522h2.521zm0-1.271a2.528 2.528 0 0 1-2.521-2.521 2.528 2.528 0 0 1 2.521-2.521h6.314A2.528 2.528 0 0 1 24 15.164a2.528 2.528 0 0 1-2.522 2.521h-6.314z"/></svg>',
|
|
8448
|
+
desc: 'Channels, messages, and threads',
|
|
8449
|
+
idKey: 'slack_client_id', secretKey: 'slack_client_secret',
|
|
8450
|
+
steps: [
|
|
8451
|
+
'Go to <a href="https://api.slack.com/apps" target="_blank" style="color:var(--accent)">api.slack.com/apps</a> and click <strong>Create New App → From scratch</strong>',
|
|
8452
|
+
'Name it (e.g. "WikiMem") and select your workspace',
|
|
8453
|
+
'Go to <strong>OAuth & Permissions</strong> → add <code>channels:history</code>, <code>channels:read</code>, <code>users:read</code> scopes',
|
|
8454
|
+
'Under <strong>Redirect URLs</strong>, add <code>CALLBACK</code>',
|
|
8455
|
+
'Go to <strong>Basic Information</strong> → copy <strong>Client ID</strong> and <strong>Client Secret</strong>',
|
|
8456
|
+
'Paste both values below and click <strong>Save Credentials</strong>',
|
|
8457
|
+
],
|
|
8458
|
+
},
|
|
8459
|
+
google: {
|
|
8460
|
+
name: 'Google',
|
|
8461
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>',
|
|
8462
|
+
desc: 'Gmail messages and Google Drive files',
|
|
8463
|
+
idKey: 'google_client_id', secretKey: 'google_client_secret',
|
|
8464
|
+
steps: [
|
|
8465
|
+
'Go to <a href="https://console.cloud.google.com/apis/credentials" target="_blank" style="color:var(--accent)">Google Cloud Console → Credentials</a>',
|
|
8466
|
+
'Click <strong>Create Credentials → OAuth client ID</strong> (create consent screen first if prompted)',
|
|
8467
|
+
'Application type: <strong>Web application</strong>',
|
|
8468
|
+
'Add <code>CALLBACK</code> under <strong>Authorized redirect URIs</strong>',
|
|
8469
|
+
'Copy the <strong>Client ID</strong> and <strong>Client Secret</strong>',
|
|
8470
|
+
'Enable <strong>Gmail API</strong> and <strong>Google Drive API</strong> in the API Library',
|
|
8471
|
+
'Paste both values below and click <strong>Save Credentials</strong>',
|
|
8472
|
+
],
|
|
8473
|
+
},
|
|
8474
|
+
linear: {
|
|
8475
|
+
name: 'Linear',
|
|
8476
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="#635BFF"><path d="M2.513 12.833l8.654 8.654a10.478 10.478 0 0 1-8.654-8.654zm-.362-2.584a10.478 10.478 0 0 1 3.388-5.725l10.937 10.937a10.478 10.478 0 0 1-5.725 3.388L2.151 10.249zm4.858-6.707a10.478 10.478 0 0 1 3.624-1.391l10.216 10.216a10.48 10.48 0 0 1-1.391 3.624L7.009 3.542zm5.088-1.735a10.478 10.478 0 0 1 4.37.117l6.571 6.571a10.478 10.478 0 0 1 .117 4.37L12.097 1.807zm5.845.904a10.444 10.444 0 0 1 3.37 2.924l-1.237-1.237.092-.092a10.478 10.478 0 0 0-2.924-2.294l.699.699z"/></svg>',
|
|
8477
|
+
desc: 'Issues, projects, and team activity',
|
|
8478
|
+
idKey: 'linear_client_id', secretKey: 'linear_client_secret',
|
|
8479
|
+
steps: [
|
|
8480
|
+
'Go to <a href="https://linear.app/settings/api" target="_blank" style="color:var(--accent)">Linear → Settings → API → OAuth Applications</a>',
|
|
8481
|
+
'Click <strong>Create new</strong>',
|
|
8482
|
+
'Set <strong>Callback URL</strong> to <code>CALLBACK</code>',
|
|
8483
|
+
'Copy the <strong>Client ID</strong> and <strong>Client Secret</strong>',
|
|
8484
|
+
'Paste both values below and click <strong>Save Credentials</strong>',
|
|
8485
|
+
],
|
|
8486
|
+
},
|
|
8487
|
+
jira: {
|
|
8488
|
+
name: 'Jira',
|
|
8489
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="#0052CC"><path d="M11.571 11.513H0a5.218 5.218 0 0 0 5.232 5.215h2.13v2.057A5.215 5.215 0 0 0 12.593 24V12.518a1.005 1.005 0 0 0-1.022-1.005zM6.348 6.294H17.87a5.218 5.218 0 0 1-5.231 5.214h-2.13V13.565A5.215 5.215 0 0 1 5.277 8.35V6.294h1.071zM12.798.042H1.276a5.218 5.218 0 0 0 5.232 5.214h2.13V7.313a5.215 5.215 0 0 0 5.231 5.215V1.047A1.005 1.005 0 0 0 12.798.042z"/></svg>',
|
|
8490
|
+
desc: 'Issues, boards, and project tracking',
|
|
8491
|
+
idKey: 'jira_client_id', secretKey: 'jira_client_secret',
|
|
8492
|
+
steps: [
|
|
8493
|
+
'Go to <a href="https://developer.atlassian.com/console/myapps/" target="_blank" style="color:var(--accent)">Atlassian Developer Console</a>',
|
|
8494
|
+
'Click <strong>Create → OAuth 2.0 integration</strong>',
|
|
8495
|
+
'Under <strong>Authorization</strong>, add callback URL: <code>CALLBACK</code>',
|
|
8496
|
+
'Add scopes: <code>read:jira-work</code>, <code>read:jira-user</code>, <code>offline_access</code>',
|
|
8497
|
+
'Copy the <strong>Client ID</strong> and <strong>Secret</strong>',
|
|
8498
|
+
'Paste both values below and click <strong>Save Credentials</strong>',
|
|
8499
|
+
],
|
|
8500
|
+
},
|
|
8501
|
+
};
|
|
8502
|
+
|
|
8503
|
+
// ── Active device flow polling state ──
|
|
8504
|
+
let _deviceFlowPollTimer = null;
|
|
8505
|
+
|
|
8506
|
+
async function renderOAuthIntegrations() {
|
|
8507
|
+
const container = document.getElementById('oauth-integrations');
|
|
8508
|
+
if (!container) return;
|
|
8509
|
+
const port = location.port || '3456';
|
|
8510
|
+
const callbackUrl = `http://localhost:${port}/api/auth/callback`;
|
|
8511
|
+
|
|
8512
|
+
let tokenStatus = {};
|
|
8513
|
+
let configData = {};
|
|
8514
|
+
try {
|
|
8515
|
+
[tokenStatus, configData] = await Promise.all([
|
|
8516
|
+
fetch('/api/auth/tokens').then(r => r.json()),
|
|
8517
|
+
fetch('/api/config').then(r => r.json()),
|
|
8518
|
+
]);
|
|
8519
|
+
} catch { container.innerHTML = '<div style="color:var(--red);font-size:13px;padding:16px 0">Failed to load integration status</div>'; return; }
|
|
8520
|
+
|
|
8521
|
+
let cards = '';
|
|
8522
|
+
for (const [provider, meta] of Object.entries(OAUTH_PROVIDER_META)) {
|
|
8523
|
+
const token = tokenStatus[provider];
|
|
8524
|
+
const isConnected = token?.connected;
|
|
8525
|
+
const hasCredentials = !!token?.hasCredentials;
|
|
8526
|
+
|
|
8527
|
+
// Badge (only shown when connected or has credentials)
|
|
8528
|
+
let badgeHtml = '';
|
|
8529
|
+
if (isConnected) badgeHtml = '<span class="oauth-card-badge connected">Connected</span>';
|
|
8530
|
+
else if (hasCredentials) badgeHtml = '<span class="oauth-card-badge ready">Ready</span>';
|
|
8531
|
+
|
|
8532
|
+
// Action area
|
|
8533
|
+
let actionHtml = '';
|
|
8534
|
+
if (isConnected) {
|
|
8535
|
+
const connDate = token.connectedAt ? new Date(token.connectedAt).toLocaleDateString() : '';
|
|
8536
|
+
actionHtml = `
|
|
8537
|
+
<div class="oauth-connected-meta">Connected ${connDate}</div>
|
|
8538
|
+
<div class="oauth-card-actions">
|
|
8539
|
+
<button class="oauth-connect-btn secondary" onclick="syncOAuthProvider('${provider}')">Sync Now</button>
|
|
8540
|
+
<button class="oauth-connect-btn danger" onclick="disconnectOAuth('${provider}')">Disconnect</button>
|
|
8541
|
+
</div>`;
|
|
8542
|
+
} else if (hasCredentials) {
|
|
8543
|
+
actionHtml = `
|
|
8544
|
+
<div class="oauth-card-actions">
|
|
8545
|
+
<button class="oauth-connect-btn primary" onclick="startOAuthConnect('${provider}')">Connect</button>
|
|
8546
|
+
</div>`;
|
|
8547
|
+
} else if (provider === 'github') {
|
|
8548
|
+
// GitHub: one-click Device Flow (no credentials needed)
|
|
8549
|
+
actionHtml = `
|
|
8550
|
+
<div class="oauth-card-actions">
|
|
8551
|
+
<button class="oauth-connect-btn primary" id="gh-device-btn" onclick="startGitHubDeviceFlow()">Connect with GitHub</button>
|
|
8552
|
+
</div>
|
|
8553
|
+
<div id="gh-device-flow-ui"></div>`;
|
|
8554
|
+
} else {
|
|
8555
|
+
// Other providers without credentials: show Connect button (credentials resolved at connect time)
|
|
8556
|
+
actionHtml = `
|
|
8557
|
+
<div class="oauth-card-actions">
|
|
8558
|
+
<button class="oauth-connect-btn primary" onclick="startOAuthConnect('${provider}')" id="oauth-connect-${provider}">Connect</button>
|
|
8559
|
+
</div>`;
|
|
8560
|
+
}
|
|
8561
|
+
|
|
8562
|
+
// Advanced setup (collapsible, for non-connected providers without credentials)
|
|
8563
|
+
let advancedHtml = '';
|
|
8564
|
+
if (!isConnected && !hasCredentials && provider !== 'github') {
|
|
8565
|
+
const stepsHtml = meta.steps.map(s =>
|
|
8566
|
+
`<li>${s.replace(/CALLBACK/g, '<code>' + esc(callbackUrl) + '</code>').replace(/PORT/g, port)}</li>`
|
|
8567
|
+
).join('');
|
|
8568
|
+
const hasId = !!configData[meta.idKey];
|
|
8569
|
+
const hasSecret = !!configData[meta.secretKey];
|
|
8570
|
+
advancedHtml = `
|
|
8571
|
+
<div class="oauth-advanced-body" id="oauth-adv-${provider}">
|
|
8572
|
+
<ol class="oauth-setup-steps">${stepsHtml}</ol>
|
|
8573
|
+
<div class="oauth-input-row">
|
|
8574
|
+
<input class="settings-input" id="oauth-id-${provider}" type="text" placeholder="Client ID" value="${hasId ? '••••••••' : ''}" ${hasId ? 'data-saved="true"' : ''} onfocus="if(this.dataset.saved){this.value='';this.dataset.saved=''}" />
|
|
8575
|
+
<input class="settings-input" id="oauth-secret-${provider}" type="password" placeholder="Client Secret" value="${hasSecret ? '••••••••' : ''}" ${hasSecret ? 'data-saved="true"' : ''} onfocus="if(this.dataset.saved){this.value='';this.dataset.saved=''}" />
|
|
8576
|
+
</div>
|
|
8577
|
+
<div class="oauth-actions">
|
|
8578
|
+
<button class="settings-btn settings-btn-primary" onclick="saveOAuthCredentials('${provider}')" style="font-size:12px;padding:5px 14px">Save & Connect</button>
|
|
8579
|
+
<span class="settings-status" id="oauth-save-status-${provider}"></span>
|
|
8580
|
+
</div>
|
|
8581
|
+
</div>`;
|
|
8582
|
+
}
|
|
8583
|
+
|
|
8584
|
+
cards += `<div class="oauth-card${isConnected ? ' oauth-connected' : ''}" id="oauth-card-${provider}">
|
|
8585
|
+
<div class="oauth-card-top">
|
|
8586
|
+
<div class="oauth-card-icon ${provider}">${meta.icon}</div>
|
|
8587
|
+
<div class="oauth-card-info">
|
|
8588
|
+
<div class="oauth-card-name">${meta.name}</div>
|
|
8589
|
+
<div class="oauth-card-desc">${meta.desc}</div>
|
|
8590
|
+
</div>
|
|
8591
|
+
${badgeHtml}
|
|
8592
|
+
</div>
|
|
8593
|
+
${actionHtml}
|
|
8594
|
+
${advancedHtml}
|
|
8595
|
+
</div>`;
|
|
8596
|
+
}
|
|
8597
|
+
container.innerHTML = `<div class="oauth-grid">${cards}</div>`;
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8600
|
+
function toggleAdvancedSetup(provider) {
|
|
8601
|
+
const body = document.getElementById('oauth-adv-' + provider);
|
|
8602
|
+
if (!body) return;
|
|
8603
|
+
body.classList.toggle('open');
|
|
8604
|
+
}
|
|
8605
|
+
|
|
8606
|
+
// ── GitHub Device Flow ──
|
|
8607
|
+
async function startGitHubDeviceFlow() {
|
|
8608
|
+
const btn = document.getElementById('gh-device-btn');
|
|
8609
|
+
const ui = document.getElementById('gh-device-flow-ui');
|
|
8610
|
+
if (!ui) return;
|
|
8611
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }
|
|
8612
|
+
|
|
8613
|
+
try {
|
|
8614
|
+
const res = await fetch('/api/auth/device-flow/start', { method: 'POST' });
|
|
8615
|
+
const data = await res.json();
|
|
8616
|
+
|
|
8617
|
+
if (data.error === 'no_client_id') {
|
|
8618
|
+
// No device client ID configured — fall back to credential setup
|
|
8619
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Connect with GitHub'; }
|
|
8620
|
+
ui.innerHTML = `<div class="oauth-info-tip" style="margin-top:4px">
|
|
8621
|
+
Device flow requires a GitHub OAuth App client ID.<br/>
|
|
8622
|
+
Set <code style="font-size:10px;color:var(--accent)">WIKIMEM_GITHUB_CLIENT_ID</code> env var, or <a href="https://github.com/settings/developers" target="_blank">create an OAuth App</a> and enter credentials below.
|
|
8623
|
+
</div>
|
|
8624
|
+
<div class="oauth-advanced-body open" style="padding-top:8px">
|
|
8625
|
+
<div class="oauth-input-row">
|
|
8626
|
+
<input class="settings-input" id="oauth-id-github" type="text" placeholder="Client ID" />
|
|
8627
|
+
<input class="settings-input" id="oauth-secret-github" type="password" placeholder="Client Secret" />
|
|
8628
|
+
</div>
|
|
8629
|
+
<div class="oauth-actions">
|
|
8630
|
+
<button class="settings-btn settings-btn-primary" onclick="saveOAuthCredentials('github')" style="font-size:12px;padding:5px 14px">Save & Connect</button>
|
|
8631
|
+
<span class="settings-status" id="oauth-save-status-github"></span>
|
|
8632
|
+
</div>
|
|
8633
|
+
</div>`;
|
|
8634
|
+
return;
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8637
|
+
if (data.error) {
|
|
8638
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Connect with GitHub'; }
|
|
8639
|
+
showToast('Device flow error: ' + (data.message || data.error));
|
|
8640
|
+
return;
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
// Show the device code UI
|
|
8644
|
+
if (btn) btn.style.display = 'none';
|
|
8645
|
+
ui.innerHTML = `<div class="device-flow-box">
|
|
8646
|
+
<div class="device-flow-steps">
|
|
8647
|
+
1. Go to <a href="${data.verification_uri}" target="_blank">${data.verification_uri}</a><br/>
|
|
8648
|
+
2. Enter this code:
|
|
8649
|
+
</div>
|
|
8650
|
+
<div class="device-flow-code">${data.user_code}</div>
|
|
8651
|
+
<div style="margin-top:4px">
|
|
8652
|
+
<button class="oauth-connect-btn secondary" style="flex:0;padding:5px 14px;font-size:11px" onclick="navigator.clipboard.writeText('${data.user_code}');this.textContent='Copied!';setTimeout(()=>this.textContent='Copy Code',1500)">Copy Code</button>
|
|
8653
|
+
</div>
|
|
8654
|
+
<div class="device-flow-status" id="gh-device-status">
|
|
8655
|
+
<span class="device-flow-spinner"></span>
|
|
8656
|
+
Waiting for authorization...
|
|
8657
|
+
</div>
|
|
8658
|
+
</div>`;
|
|
8659
|
+
|
|
8660
|
+
// Start polling
|
|
8661
|
+
pollGitHubDeviceFlow(data.device_code, data.interval || 5);
|
|
8662
|
+
} catch (err) {
|
|
8663
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Connect with GitHub'; }
|
|
8664
|
+
showToast('Device flow failed: ' + (err.message || 'unknown'));
|
|
8665
|
+
}
|
|
8666
|
+
}
|
|
8667
|
+
|
|
8668
|
+
function pollGitHubDeviceFlow(deviceCode, interval) {
|
|
8669
|
+
if (_deviceFlowPollTimer) clearInterval(_deviceFlowPollTimer);
|
|
8670
|
+
const pollMs = Math.max((interval || 5) * 1000, 5000);
|
|
8671
|
+
|
|
8672
|
+
_deviceFlowPollTimer = setInterval(async () => {
|
|
8673
|
+
try {
|
|
8674
|
+
const res = await fetch('/api/auth/device-flow/poll', {
|
|
8675
|
+
method: 'POST',
|
|
8676
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8677
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
8678
|
+
});
|
|
8679
|
+
const data = await res.json();
|
|
8680
|
+
const statusEl = document.getElementById('gh-device-status');
|
|
8681
|
+
|
|
8682
|
+
if (data.status === 'complete') {
|
|
8683
|
+
clearInterval(_deviceFlowPollTimer);
|
|
8684
|
+
_deviceFlowPollTimer = null;
|
|
8685
|
+
if (statusEl) statusEl.innerHTML = '<span style="color:var(--green);font-weight:600">Connected! Syncing...</span>';
|
|
8686
|
+
showToast('GitHub connected — syncing data...');
|
|
8687
|
+
setTimeout(() => renderOAuthIntegrations(), 1000);
|
|
8688
|
+
loadOAuthStatus();
|
|
8689
|
+
syncOAuthProvider('github');
|
|
8690
|
+
} else if (data.status === 'expired') {
|
|
8691
|
+
clearInterval(_deviceFlowPollTimer);
|
|
8692
|
+
_deviceFlowPollTimer = null;
|
|
8693
|
+
if (statusEl) statusEl.innerHTML = '<span style="color:var(--red)">Code expired. </span><a href="#" onclick="event.preventDefault();startGitHubDeviceFlow()" style="color:var(--accent)">Try again</a>';
|
|
8694
|
+
} else if (data.status === 'slow_down') {
|
|
8695
|
+
// GitHub asked us to slow down — we'll just wait for next interval
|
|
8696
|
+
}
|
|
8697
|
+
// 'pending' — keep polling
|
|
8698
|
+
} catch {
|
|
8699
|
+
// Network error — keep trying
|
|
8700
|
+
}
|
|
8701
|
+
}, pollMs);
|
|
8702
|
+
}
|
|
8703
|
+
|
|
8704
|
+
async function saveOAuthCredentials(provider) {
|
|
8705
|
+
const meta = OAUTH_PROVIDER_META[provider];
|
|
8706
|
+
if (!meta) return;
|
|
8707
|
+
const idInput = document.getElementById('oauth-id-' + provider);
|
|
8708
|
+
const secretInput = document.getElementById('oauth-secret-' + provider);
|
|
8709
|
+
const statusEl = document.getElementById('oauth-save-status-' + provider);
|
|
8710
|
+
if (!idInput || !secretInput) return;
|
|
8711
|
+
|
|
8712
|
+
const clientId = idInput.value.trim();
|
|
8713
|
+
const clientSecret = secretInput.value.trim();
|
|
8714
|
+
|
|
8715
|
+
if (idInput.dataset.saved && !clientId) { showToast('Client ID is empty — clear field and enter new value'); return; }
|
|
8716
|
+
if (secretInput.dataset.saved && !clientSecret) { showToast('Client Secret is empty — clear field and enter new value'); return; }
|
|
8717
|
+
|
|
8718
|
+
const updates = {};
|
|
8719
|
+
if (clientId && clientId !== '••••••••') updates[meta.idKey] = clientId;
|
|
8720
|
+
if (clientSecret && clientSecret !== '••••••••') updates[meta.secretKey] = clientSecret;
|
|
8721
|
+
|
|
8722
|
+
if (Object.keys(updates).length === 0) { showToast('Enter Client ID and Secret first'); return; }
|
|
8723
|
+
|
|
8724
|
+
if (statusEl) { statusEl.textContent = 'Saving…'; statusEl.style.color = 'var(--text-muted)'; }
|
|
8725
|
+
|
|
8726
|
+
try {
|
|
8727
|
+
const res = await fetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) });
|
|
8728
|
+
const data = await res.json();
|
|
8729
|
+
if (data.error) throw new Error(data.error);
|
|
8730
|
+
if (statusEl) { statusEl.textContent = 'Saved'; statusEl.style.color = 'var(--green)'; }
|
|
8731
|
+
showToast(`${meta.name} credentials saved`);
|
|
8732
|
+
setTimeout(() => renderOAuthIntegrations(), 800);
|
|
8733
|
+
} catch (err) {
|
|
8734
|
+
if (statusEl) { statusEl.textContent = 'Failed: ' + (err.message || 'unknown'); statusEl.style.color = 'var(--red)'; }
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8738
|
+
async function disconnectOAuth(provider) {
|
|
8739
|
+
const meta = OAUTH_PROVIDER_META[provider];
|
|
8740
|
+
try {
|
|
8741
|
+
await fetch('/api/auth/tokens/' + encodeURIComponent(provider), { method: 'DELETE' });
|
|
8742
|
+
showToast(`${meta?.name || provider} disconnected`);
|
|
8743
|
+
renderOAuthIntegrations();
|
|
8744
|
+
} catch (err) { showToast('Disconnect failed: ' + (err.message || 'unknown')); }
|
|
8745
|
+
}
|
|
8746
|
+
|
|
8747
|
+
async function syncOAuthProvider(provider) {
|
|
8748
|
+
const meta = OAUTH_PROVIDER_META[provider];
|
|
8749
|
+
showToast(`Syncing ${meta?.name || provider}…`);
|
|
8750
|
+
try {
|
|
8751
|
+
const res = await fetch('/api/sync/' + encodeURIComponent(provider), { method: 'POST' });
|
|
8752
|
+
const data = await res.json();
|
|
8753
|
+
if (data.error || data.errors?.length) {
|
|
8754
|
+
showToast(`Sync error: ${data.errors?.[0] || data.error}`);
|
|
8755
|
+
} else {
|
|
8756
|
+
showToast(`✓ ${meta?.name || provider}: ${data.filesWritten || 0} files synced in ${((data.duration || 0) / 1000).toFixed(1)}s`);
|
|
8757
|
+
loadTree();
|
|
8758
|
+
}
|
|
8759
|
+
} catch (err) { showToast('Sync failed: ' + (err.message || 'unknown')); }
|
|
8760
|
+
}
|
|
8761
|
+
|
|
7305
8762
|
function showStageDetail(stepId) {
|
|
7306
8763
|
const step = PIPELINE_STEPS.find(s => s.id === stepId);
|
|
7307
8764
|
const event = currentPipelineSteps[stepId];
|
|
@@ -7437,6 +8894,9 @@
|
|
|
7437
8894
|
_tlGraphLastHash: null,
|
|
7438
8895
|
_tlSliderRaf: null,
|
|
7439
8896
|
_tlSliderPendingIdx: null,
|
|
8897
|
+
_tlPlayRaf: null,
|
|
8898
|
+
_tlDragging: false,
|
|
8899
|
+
_tlRenderDebounce: null,
|
|
7440
8900
|
};
|
|
7441
8901
|
|
|
7442
8902
|
function railTimelapseClick() {
|
|
@@ -7560,10 +9020,13 @@
|
|
|
7560
9020
|
const commit = tlState.commits[idx];
|
|
7561
9021
|
if(!commit) return;
|
|
7562
9022
|
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
9023
|
+
// Don't overwrite slider position while user is actively dragging
|
|
9024
|
+
if(!tlState._tlDragging) {
|
|
9025
|
+
const slider = document.getElementById('tl-slider');
|
|
9026
|
+
slider.value = String(idx);
|
|
9027
|
+
const max = parseInt(slider.max) || 1;
|
|
9028
|
+
slider.style.setProperty('--slider-pct', `${(idx / max) * 100}%`);
|
|
9029
|
+
}
|
|
7567
9030
|
|
|
7568
9031
|
const date = new Date(commit.date);
|
|
7569
9032
|
const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
|
@@ -7934,7 +9397,8 @@
|
|
|
7934
9397
|
const cs = k => getComputedStyle(document.documentElement).getPropertyValue(k).trim();
|
|
7935
9398
|
const COLORS = ['#4f9eff','#4ec9b0','#d7ba7d','#c586c0','#9cdcfe','#dcdcaa','#ce9178','#608b4e','#b5cea8'];
|
|
7936
9399
|
const cColor = c => COLORS[(c||0) % COLORS.length];
|
|
7937
|
-
const
|
|
9400
|
+
const totalNodes = data.nodes.length;
|
|
9401
|
+
const isHub = d => (d.linksIn||0) >= Math.max(5, Math.ceil(totalNodes * 0.06));
|
|
7938
9402
|
const nR = d => Math.max(4, Math.min(18, 4 + Math.sqrt(d.linksIn||0) * 3));
|
|
7939
9403
|
const linkKey = d => `${d.source?.id??d.source}\u2192${d.target?.id??d.target}`;
|
|
7940
9404
|
|
|
@@ -7970,11 +9434,8 @@
|
|
|
7970
9434
|
svg.call(d3.zoom().scaleExtent([0.15,5]).on('zoom', e => g.attr('transform', e.transform)));
|
|
7971
9435
|
}
|
|
7972
9436
|
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
} else {
|
|
7976
|
-
g.style('opacity', 1);
|
|
7977
|
-
}
|
|
9437
|
+
// No opacity flash — let D3 data joins handle smooth transitions
|
|
9438
|
+
g.style('opacity', 1);
|
|
7978
9439
|
|
|
7979
9440
|
let tip = container.querySelector('.tl-tip');
|
|
7980
9441
|
if(!tip) { tip = document.createElement('div'); tip.className = 'tl-tip'; container.appendChild(tip); }
|
|
@@ -8008,7 +9469,7 @@
|
|
|
8008
9469
|
sim.force('charge', d3.forceManyBody().strength(-220));
|
|
8009
9470
|
sim.force('center', d3.forceCenter(width/2, height/2));
|
|
8010
9471
|
sim.force('collision', d3.forceCollide().radius(d => isHub(d)?26:18));
|
|
8011
|
-
sim.alpha(0.3).restart();
|
|
9472
|
+
sim.alpha(tlState.playing ? 0.15 : 0.3).restart();
|
|
8012
9473
|
} else {
|
|
8013
9474
|
if(tlState._tlGraphSim) {
|
|
8014
9475
|
tlState._tlGraphSim.stop();
|
|
@@ -8108,9 +9569,6 @@
|
|
|
8108
9569
|
});
|
|
8109
9570
|
|
|
8110
9571
|
tlState._tlGraphLastHash = commit.hash;
|
|
8111
|
-
if(crossfade) {
|
|
8112
|
-
g.transition().duration(280).style('opacity', 1);
|
|
8113
|
-
}
|
|
8114
9572
|
|
|
8115
9573
|
if(typeof idx === 'number') {
|
|
8116
9574
|
const lo = Math.max(0,idx-5), hi = Math.min(tlState.commits.length-1,idx+5);
|
|
@@ -8129,14 +9587,47 @@
|
|
|
8129
9587
|
document.getElementById('tl-play').textContent = '⏸';
|
|
8130
9588
|
document.getElementById('tl-play').classList.add('playing');
|
|
8131
9589
|
const speed = parseInt(document.getElementById('tl-speed').value) || 2000;
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
9590
|
+
const slider = document.getElementById('tl-slider');
|
|
9591
|
+
const max = tlState.commits.length - 1;
|
|
9592
|
+
if(max <= 0) { tlPause(); return; }
|
|
9593
|
+
|
|
9594
|
+
let lastTime = null;
|
|
9595
|
+
let fractionalIdx = tlState.currentIdx;
|
|
9596
|
+
|
|
9597
|
+
function animate(ts) {
|
|
9598
|
+
if(!tlState.playing || tlState._tlDragging) return;
|
|
9599
|
+
if(lastTime === null) { lastTime = ts; tlState._tlPlayRaf = requestAnimationFrame(animate); return; }
|
|
9600
|
+
const dt = ts - lastTime;
|
|
9601
|
+
lastTime = ts;
|
|
9602
|
+
|
|
9603
|
+
// Advance fractional index smoothly based on speed setting
|
|
9604
|
+
// speed = ms per commit, so rate = 1/speed commits per ms
|
|
9605
|
+
fractionalIdx += dt / speed;
|
|
9606
|
+
|
|
9607
|
+
// Smoothly update slider position (continuous, not integer-locked)
|
|
9608
|
+
const pct = Math.min(fractionalIdx / max, 1) * 100;
|
|
9609
|
+
slider.style.setProperty('--slider-pct', `${pct}%`);
|
|
9610
|
+
slider.value = String(Math.min(Math.round(fractionalIdx), max));
|
|
9611
|
+
|
|
9612
|
+
// Only render at integer commit boundaries
|
|
9613
|
+
const targetIdx = Math.floor(fractionalIdx);
|
|
9614
|
+
if(targetIdx > tlState.currentIdx && targetIdx <= max) {
|
|
9615
|
+
renderTimelapseAt(targetIdx, { crossfade: false });
|
|
9616
|
+
}
|
|
9617
|
+
|
|
9618
|
+
if(fractionalIdx >= max) {
|
|
9619
|
+
renderTimelapseAt(max, { crossfade: false });
|
|
9620
|
+
tlPause();
|
|
9621
|
+
return;
|
|
9622
|
+
}
|
|
9623
|
+
tlState._tlPlayRaf = requestAnimationFrame(animate);
|
|
9624
|
+
}
|
|
9625
|
+
tlState._tlPlayRaf = requestAnimationFrame(animate);
|
|
8136
9626
|
}
|
|
8137
9627
|
|
|
8138
9628
|
function tlPause() {
|
|
8139
9629
|
tlState.playing = false;
|
|
9630
|
+
if(tlState._tlPlayRaf) { cancelAnimationFrame(tlState._tlPlayRaf); tlState._tlPlayRaf = null; }
|
|
8140
9631
|
clearInterval(tlState.interval);
|
|
8141
9632
|
document.getElementById('tl-play').textContent = '▶';
|
|
8142
9633
|
document.getElementById('tl-play').classList.remove('playing');
|
|
@@ -8186,6 +9677,11 @@
|
|
|
8186
9677
|
if(e.target === e.currentTarget) closePRModal();
|
|
8187
9678
|
});
|
|
8188
9679
|
|
|
9680
|
+
const cxClose = document.getElementById('cx-modal-close');
|
|
9681
|
+
if (cxClose) cxClose.addEventListener('click', closeCxModal);
|
|
9682
|
+
const cxOv = document.getElementById('cx-modal-overlay');
|
|
9683
|
+
if (cxOv) cxOv.addEventListener('click', (e) => { if (e.target === e.currentTarget) closeCxModal(); });
|
|
9684
|
+
|
|
8189
9685
|
// Settings nav
|
|
8190
9686
|
document.querySelectorAll('.settings-nav-item').forEach(el => {
|
|
8191
9687
|
el.addEventListener('click', () => renderSettingsSection(el.dataset.section));
|
|
@@ -8218,32 +9714,72 @@
|
|
|
8218
9714
|
alert('Failed to create page');
|
|
8219
9715
|
}
|
|
8220
9716
|
});
|
|
9717
|
+
document.getElementById('sa-new-folder').addEventListener('click', () => createSubfolder('', 'wiki'));
|
|
8221
9718
|
document.getElementById('sa-collapse').addEventListener('click', collapseAllFolders);
|
|
8222
9719
|
document.getElementById('sa-refresh').addEventListener('click', () => { loadTree(); loadHome(); });
|
|
8223
9720
|
document.getElementById('git-status-badge').addEventListener('click', toggleGitDropdown);
|
|
8224
9721
|
|
|
9722
|
+
// Drag detection: pointerdown sets dragging flag and pauses playback immediately
|
|
9723
|
+
document.getElementById('tl-slider').addEventListener('pointerdown', () => {
|
|
9724
|
+
tlState._tlDragging = true;
|
|
9725
|
+
if(tlState.playing) tlPause();
|
|
9726
|
+
});
|
|
9727
|
+
// pointerup clears dragging flag (also on document in case pointer leaves slider)
|
|
9728
|
+
function tlSliderPointerUp() {
|
|
9729
|
+
if(!tlState._tlDragging) return;
|
|
9730
|
+
tlState._tlDragging = false;
|
|
9731
|
+
// Flush any pending debounced render at the final position
|
|
9732
|
+
if(tlState._tlRenderDebounce != null) {
|
|
9733
|
+
clearTimeout(tlState._tlRenderDebounce);
|
|
9734
|
+
tlState._tlRenderDebounce = null;
|
|
9735
|
+
}
|
|
9736
|
+
if(tlState._tlSliderRaf != null) {
|
|
9737
|
+
cancelAnimationFrame(tlState._tlSliderRaf);
|
|
9738
|
+
tlState._tlSliderRaf = null;
|
|
9739
|
+
}
|
|
9740
|
+
renderTimelapseAt(tlState._tlSliderPendingIdx != null ? tlState._tlSliderPendingIdx : tlState.currentIdx, { crossfade: true });
|
|
9741
|
+
}
|
|
9742
|
+
document.getElementById('tl-slider').addEventListener('pointerup', tlSliderPointerUp);
|
|
9743
|
+
document.addEventListener('pointerup', tlSliderPointerUp);
|
|
9744
|
+
|
|
9745
|
+
// input: fires continuously during drag — keep slider visual responsive, debounce heavy render
|
|
8225
9746
|
document.getElementById('tl-slider').addEventListener('input', e => {
|
|
8226
9747
|
const val = parseInt(e.target.value, 10);
|
|
8227
9748
|
const max = parseInt(e.target.max, 10) || 1;
|
|
9749
|
+
// Always update the slider track fill immediately (cheap CSS-only)
|
|
8228
9750
|
e.target.style.setProperty('--slider-pct', `${(val / max) * 100}%`);
|
|
8229
9751
|
tlState._tlSliderPendingIdx = val;
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
9752
|
+
|
|
9753
|
+
// Update commit info label immediately (lightweight DOM update)
|
|
9754
|
+
const commit = tlState.commits[val];
|
|
9755
|
+
if(commit) {
|
|
9756
|
+
document.getElementById('tl-label-current').textContent = `${val + 1} / ${tlState.commits.length}`;
|
|
9757
|
+
}
|
|
9758
|
+
|
|
9759
|
+
// Debounce the expensive tree/graph render (80ms) to avoid blocking the UI thread
|
|
9760
|
+
if(tlState._tlRenderDebounce != null) clearTimeout(tlState._tlRenderDebounce);
|
|
9761
|
+
tlState._tlRenderDebounce = setTimeout(() => {
|
|
9762
|
+
tlState._tlRenderDebounce = null;
|
|
9763
|
+
renderTimelapseAt(val, { crossfade: false });
|
|
9764
|
+
}, 80);
|
|
8235
9765
|
});
|
|
9766
|
+
|
|
9767
|
+
// change: fires once on slider release (mouse up / touch end) — final authoritative render
|
|
8236
9768
|
document.getElementById('tl-slider').addEventListener('change', e => {
|
|
8237
9769
|
const val = parseInt(e.target.value, 10);
|
|
9770
|
+
if(tlState._tlRenderDebounce != null) {
|
|
9771
|
+
clearTimeout(tlState._tlRenderDebounce);
|
|
9772
|
+
tlState._tlRenderDebounce = null;
|
|
9773
|
+
}
|
|
8238
9774
|
if(tlState._tlSliderRaf != null) {
|
|
8239
9775
|
cancelAnimationFrame(tlState._tlSliderRaf);
|
|
8240
9776
|
tlState._tlSliderRaf = null;
|
|
8241
9777
|
}
|
|
8242
|
-
renderTimelapseAt(val);
|
|
9778
|
+
renderTimelapseAt(val, { crossfade: true });
|
|
8243
9779
|
});
|
|
8244
9780
|
document.getElementById('tl-play').addEventListener('click', tlPlay);
|
|
8245
|
-
document.getElementById('tl-prev').addEventListener('click', () => { if(tlState.currentIdx > 0) renderTimelapseAt(tlState.currentIdx - 1); });
|
|
8246
|
-
document.getElementById('tl-next').addEventListener('click', () => { if(tlState.currentIdx < tlState.commits.length - 1) renderTimelapseAt(tlState.currentIdx + 1); });
|
|
9781
|
+
document.getElementById('tl-prev').addEventListener('click', () => { if(tlState.currentIdx > 0) renderTimelapseAt(tlState.currentIdx - 1, { crossfade: true }); });
|
|
9782
|
+
document.getElementById('tl-next').addEventListener('click', () => { if(tlState.currentIdx < tlState.commits.length - 1) renderTimelapseAt(tlState.currentIdx + 1, { crossfade: true }); });
|
|
8247
9783
|
document.getElementById('tl-speed').addEventListener('change', () => { if(tlState.playing) { tlPause(); tlPlay(); } });
|
|
8248
9784
|
document.getElementById('tl-mode-tree').addEventListener('click', () => {
|
|
8249
9785
|
tlState.mode = 'tree';
|
|
@@ -8320,14 +9856,18 @@
|
|
|
8320
9856
|
setTimeout(positionSelToolbar, 10);
|
|
8321
9857
|
});
|
|
8322
9858
|
document.addEventListener('mousedown', (e) => {
|
|
8323
|
-
if (!_selToolbar.contains(e.target)) hideSelToolbar();
|
|
9859
|
+
if (!_selToolbar.contains(e.target) && !_selPreventHide) hideSelToolbar();
|
|
8324
9860
|
});
|
|
8325
9861
|
|
|
8326
|
-
|
|
9862
|
+
let _selPreventHide = false;
|
|
9863
|
+
_selToolbar.addEventListener('mousedown', (e) => {
|
|
9864
|
+
e.preventDefault();
|
|
9865
|
+
_selPreventHide = true;
|
|
8327
9866
|
const btn = e.target.closest('.st-btn');
|
|
8328
9867
|
if (!btn) return;
|
|
8329
|
-
e.preventDefault();
|
|
8330
9868
|
const cmd = btn.dataset.cmd;
|
|
9869
|
+
const body = document.getElementById('page-body');
|
|
9870
|
+
body.focus();
|
|
8331
9871
|
if (cmd === 'bold') {
|
|
8332
9872
|
document.execCommand('bold');
|
|
8333
9873
|
} else if (cmd === 'italic') {
|
|
@@ -8335,23 +9875,165 @@
|
|
|
8335
9875
|
} else if (cmd === 'code') {
|
|
8336
9876
|
const sel = window.getSelection();
|
|
8337
9877
|
if (sel && sel.rangeCount) {
|
|
9878
|
+
let node = sel.anchorNode;
|
|
9879
|
+
while (node && node !== body) {
|
|
9880
|
+
if (node.nodeType === 1 && node.tagName === 'CODE') {
|
|
9881
|
+
const text = document.createTextNode(node.textContent);
|
|
9882
|
+
node.parentNode.replaceChild(text, node);
|
|
9883
|
+
const r = document.createRange(); r.selectNodeContents(text);
|
|
9884
|
+
sel.removeAllRanges(); sel.addRange(r);
|
|
9885
|
+
markDirty(); setTimeout(positionSelToolbar, 10);
|
|
9886
|
+
setTimeout(() => { _selPreventHide = false; }, 100);
|
|
9887
|
+
return;
|
|
9888
|
+
}
|
|
9889
|
+
node = node.parentNode;
|
|
9890
|
+
}
|
|
8338
9891
|
const range = sel.getRangeAt(0);
|
|
8339
9892
|
const code = document.createElement('code');
|
|
8340
|
-
code.appendChild(range.extractContents());
|
|
8341
|
-
|
|
8342
|
-
sel.
|
|
9893
|
+
try { range.surroundContents(code); } catch { code.appendChild(range.extractContents()); range.insertNode(code); }
|
|
9894
|
+
const nr = document.createRange(); nr.selectNodeContents(code);
|
|
9895
|
+
sel.removeAllRanges(); sel.addRange(nr);
|
|
8343
9896
|
}
|
|
8344
9897
|
} else if (cmd === 'link') {
|
|
8345
9898
|
const sel = window.getSelection();
|
|
8346
9899
|
if (!sel || sel.isCollapsed) return;
|
|
9900
|
+
let linkNode = sel.anchorNode;
|
|
9901
|
+
while (linkNode && linkNode !== body) {
|
|
9902
|
+
if (linkNode.nodeType === 1 && linkNode.tagName === 'A') {
|
|
9903
|
+
document.execCommand('unlink'); markDirty();
|
|
9904
|
+
setTimeout(() => { _selPreventHide = false; }, 100);
|
|
9905
|
+
return;
|
|
9906
|
+
}
|
|
9907
|
+
linkNode = linkNode.parentNode;
|
|
9908
|
+
}
|
|
8347
9909
|
const url = prompt('Enter URL:');
|
|
8348
|
-
if (!url) return;
|
|
9910
|
+
if (!url) { setTimeout(() => { _selPreventHide = false; }, 100); return; }
|
|
8349
9911
|
document.execCommand('createLink', false, url);
|
|
8350
9912
|
} else if (cmd === 'h2' || cmd === 'h3') {
|
|
8351
9913
|
document.execCommand('formatBlock', false, cmd);
|
|
8352
9914
|
}
|
|
8353
9915
|
markDirty();
|
|
8354
9916
|
setTimeout(positionSelToolbar, 10);
|
|
9917
|
+
setTimeout(() => { _selPreventHide = false; }, 100);
|
|
9918
|
+
});
|
|
9919
|
+
|
|
9920
|
+
// ── @Mention Autocomplete (ENT-002) ──
|
|
9921
|
+
const _mentionPopup = document.getElementById('mention-popup');
|
|
9922
|
+
let _mentionActive = false, _mentionQuery = '', _mentionItems = [], _mentionIdx = 0;
|
|
9923
|
+
|
|
9924
|
+
function getCaretCoords() {
|
|
9925
|
+
const sel = window.getSelection();
|
|
9926
|
+
if (!sel || !sel.rangeCount) return null;
|
|
9927
|
+
const range = sel.getRangeAt(0).cloneRange();
|
|
9928
|
+
range.collapse(true);
|
|
9929
|
+
const rect = range.getBoundingClientRect();
|
|
9930
|
+
return { left: rect.left, top: rect.bottom + 4 };
|
|
9931
|
+
}
|
|
9932
|
+
|
|
9933
|
+
function showMentionPopup(query) {
|
|
9934
|
+
if (!state.allPages || !state.allPages.length) { hideMentionPopup(); return; }
|
|
9935
|
+
const q = query.toLowerCase();
|
|
9936
|
+
_mentionItems = state.allPages
|
|
9937
|
+
.filter(p => p.title.toLowerCase().includes(q) || (p.slug || '').toLowerCase().includes(q))
|
|
9938
|
+
.slice(0, 8);
|
|
9939
|
+
if (!_mentionItems.length) { hideMentionPopup(); return; }
|
|
9940
|
+
_mentionIdx = 0;
|
|
9941
|
+
_mentionPopup.innerHTML = _mentionItems.map((p, i) => {
|
|
9942
|
+
const typeLabel = (p.frontmatter?.type || 'page').toLowerCase();
|
|
9943
|
+
return `<div class="mention-item${i === 0 ? ' active' : ''}" data-idx="${i}">
|
|
9944
|
+
<span class="mention-item-type">${esc(typeLabel)}</span>
|
|
9945
|
+
<span>${esc(p.title)}</span>
|
|
9946
|
+
</div>`;
|
|
9947
|
+
}).join('');
|
|
9948
|
+
const coords = getCaretCoords();
|
|
9949
|
+
if (coords) {
|
|
9950
|
+
_mentionPopup.style.left = Math.min(coords.left, window.innerWidth - 330) + 'px';
|
|
9951
|
+
_mentionPopup.style.top = coords.top + 'px';
|
|
9952
|
+
}
|
|
9953
|
+
_mentionPopup.classList.add('visible');
|
|
9954
|
+
_mentionActive = true;
|
|
9955
|
+
_mentionPopup.querySelectorAll('.mention-item').forEach(el => {
|
|
9956
|
+
el.addEventListener('mousedown', (e) => {
|
|
9957
|
+
e.preventDefault();
|
|
9958
|
+
insertMention(_mentionItems[+el.dataset.idx]);
|
|
9959
|
+
});
|
|
9960
|
+
});
|
|
9961
|
+
}
|
|
9962
|
+
|
|
9963
|
+
function hideMentionPopup() {
|
|
9964
|
+
_mentionPopup.classList.remove('visible');
|
|
9965
|
+
_mentionPopup.innerHTML = '';
|
|
9966
|
+
_mentionActive = false;
|
|
9967
|
+
_mentionQuery = '';
|
|
9968
|
+
_mentionItems = [];
|
|
9969
|
+
}
|
|
9970
|
+
|
|
9971
|
+
function insertMention(page) {
|
|
9972
|
+
if (!page) return;
|
|
9973
|
+
const sel = window.getSelection();
|
|
9974
|
+
if (!sel || !sel.rangeCount) return;
|
|
9975
|
+
const range = sel.getRangeAt(0);
|
|
9976
|
+
const textNode = range.startContainer;
|
|
9977
|
+
if (textNode.nodeType !== Node.TEXT_NODE) { hideMentionPopup(); return; }
|
|
9978
|
+
const text = textNode.textContent;
|
|
9979
|
+
const caretPos = range.startOffset;
|
|
9980
|
+
const atIdx = text.lastIndexOf('@', caretPos - 1);
|
|
9981
|
+
if (atIdx === -1) { hideMentionPopup(); return; }
|
|
9982
|
+
const before = text.substring(0, atIdx);
|
|
9983
|
+
const after = text.substring(caretPos);
|
|
9984
|
+
textNode.textContent = before + after;
|
|
9985
|
+
const link = document.createElement('a');
|
|
9986
|
+
link.className = 'wikilink';
|
|
9987
|
+
link.href = '#';
|
|
9988
|
+
link.dataset.wikilink = page.title;
|
|
9989
|
+
link.textContent = page.title;
|
|
9990
|
+
link.onclick = (e) => { e.preventDefault(); if (!state.editing) openPage(page.title); };
|
|
9991
|
+
const afterNode = textNode.splitText(before.length);
|
|
9992
|
+
textNode.parentNode.insertBefore(link, afterNode);
|
|
9993
|
+
const space = document.createTextNode(' ');
|
|
9994
|
+
link.parentNode.insertBefore(space, afterNode);
|
|
9995
|
+
const r = document.createRange();
|
|
9996
|
+
r.setStartAfter(space);
|
|
9997
|
+
r.collapse(true);
|
|
9998
|
+
sel.removeAllRanges();
|
|
9999
|
+
sel.addRange(r);
|
|
10000
|
+
hideMentionPopup();
|
|
10001
|
+
markDirty();
|
|
10002
|
+
}
|
|
10003
|
+
|
|
10004
|
+
document.getElementById('page-body').addEventListener('input', () => {
|
|
10005
|
+
if (!state.editing) return;
|
|
10006
|
+
const sel = window.getSelection();
|
|
10007
|
+
if (!sel || !sel.rangeCount) { hideMentionPopup(); return; }
|
|
10008
|
+
const range = sel.getRangeAt(0);
|
|
10009
|
+
const textNode = range.startContainer;
|
|
10010
|
+
if (textNode.nodeType !== Node.TEXT_NODE) { hideMentionPopup(); return; }
|
|
10011
|
+
const text = textNode.textContent.substring(0, range.startOffset);
|
|
10012
|
+
const atMatch = text.match(/@([a-zA-Z0-9 _-]{0,30})$/);
|
|
10013
|
+
if (atMatch) {
|
|
10014
|
+
_mentionQuery = atMatch[1];
|
|
10015
|
+
showMentionPopup(_mentionQuery);
|
|
10016
|
+
} else {
|
|
10017
|
+
hideMentionPopup();
|
|
10018
|
+
}
|
|
10019
|
+
});
|
|
10020
|
+
|
|
10021
|
+
document.getElementById('page-body').addEventListener('keydown', (e) => {
|
|
10022
|
+
if (!_mentionActive) return;
|
|
10023
|
+
if (e.key === 'ArrowDown') {
|
|
10024
|
+
e.preventDefault();
|
|
10025
|
+
_mentionIdx = Math.min(_mentionIdx + 1, _mentionItems.length - 1);
|
|
10026
|
+
_mentionPopup.querySelectorAll('.mention-item').forEach((el, i) => el.classList.toggle('active', i === _mentionIdx));
|
|
10027
|
+
} else if (e.key === 'ArrowUp') {
|
|
10028
|
+
e.preventDefault();
|
|
10029
|
+
_mentionIdx = Math.max(_mentionIdx - 1, 0);
|
|
10030
|
+
_mentionPopup.querySelectorAll('.mention-item').forEach((el, i) => el.classList.toggle('active', i === _mentionIdx));
|
|
10031
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
10032
|
+
if (_mentionItems[_mentionIdx]) { e.preventDefault(); insertMention(_mentionItems[_mentionIdx]); }
|
|
10033
|
+
} else if (e.key === 'Escape') {
|
|
10034
|
+
e.preventDefault();
|
|
10035
|
+
hideMentionPopup();
|
|
10036
|
+
}
|
|
8355
10037
|
});
|
|
8356
10038
|
</script>
|
|
8357
10039
|
</body>
|