lore-memory 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lore.js +10 -0
- package/package.json +4 -2
- package/src/commands/mine.js +4 -3
- package/src/commands/ui.js +171 -0
- package/src/ui/public/app.js +286 -0
- package/src/ui/public/index.html +118 -0
- package/src/ui/public/style.css +321 -0
- package/src/watcher/comments.js +3 -2
package/bin/lore.js
CHANGED
|
@@ -10,6 +10,9 @@ program
|
|
|
10
10
|
.description('Persistent project memory for developers')
|
|
11
11
|
.version('0.1.0')
|
|
12
12
|
.action(async () => {
|
|
13
|
+
// Only launch the interactive menu if strictly NO arguments were provided
|
|
14
|
+
if (process.argv.length !== 2) return;
|
|
15
|
+
|
|
13
16
|
const inquirer = require('inquirer');
|
|
14
17
|
const chalk = require('chalk');
|
|
15
18
|
const { execSync } = require('child_process');
|
|
@@ -36,6 +39,7 @@ program
|
|
|
36
39
|
{ name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
|
|
37
40
|
{ name: '📊 View project health (lore score)', value: 'score' },
|
|
38
41
|
{ name: '🔍 Search knowledge base (lore search)', value: 'search' },
|
|
42
|
+
{ name: '🌐 Open Local Web Dashboard (lore ui)', value: 'ui' },
|
|
39
43
|
{ name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
|
|
40
44
|
new inquirer.Separator(),
|
|
41
45
|
{ name: '❓ Show Help', value: 'help' },
|
|
@@ -192,4 +196,10 @@ program
|
|
|
192
196
|
.option('--build', 'Rebuild the full graph from source')
|
|
193
197
|
.action(require('../src/commands/graph'));
|
|
194
198
|
|
|
199
|
+
program
|
|
200
|
+
.command('ui')
|
|
201
|
+
.description('Start the local Lore web dashboard')
|
|
202
|
+
.option('-p, --port <port>', 'Port to run the UI server on', '3333')
|
|
203
|
+
.action(require('../src/commands/ui'));
|
|
204
|
+
|
|
195
205
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lore-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Persistent project memory for developers. Captures decisions, invariants, gotchas, and graveyard entries — automatically and manually — and injects them into AI coding sessions.",
|
|
5
5
|
"main": "bin/lore.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,11 +43,13 @@
|
|
|
43
43
|
"chalk": "^4",
|
|
44
44
|
"chokidar": "^3.6.0",
|
|
45
45
|
"commander": "^11",
|
|
46
|
+
"express": "^5.2.1",
|
|
46
47
|
"fs-extra": "^11",
|
|
47
48
|
"glob": "^10.5.0",
|
|
48
49
|
"inquirer": "^8",
|
|
49
50
|
"js-yaml": "^4",
|
|
50
51
|
"natural": "^6.12.0",
|
|
51
|
-
"ollama": "^0.6.3"
|
|
52
|
+
"ollama": "^0.6.3",
|
|
53
|
+
"open": "^11.0.0"
|
|
52
54
|
}
|
|
53
55
|
}
|
package/src/commands/mine.js
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require('fs-extra');
|
|
|
6
6
|
const { mineFile, mineDirectory } = require('../watcher/comments');
|
|
7
7
|
const { requireInit } = require('../lib/guard');
|
|
8
8
|
|
|
9
|
-
function mine(targetPath) {
|
|
9
|
+
async function mine(targetPath) {
|
|
10
10
|
requireInit();
|
|
11
11
|
const projectRoot = process.cwd();
|
|
12
12
|
const target = targetPath || '.';
|
|
@@ -18,10 +18,11 @@ function mine(targetPath) {
|
|
|
18
18
|
|
|
19
19
|
if (stat.isDirectory()) {
|
|
20
20
|
console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
|
|
21
|
-
count = mineDirectory(abs, projectRoot);
|
|
21
|
+
count = await mineDirectory(abs, projectRoot);
|
|
22
22
|
} else {
|
|
23
23
|
console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
|
|
24
|
-
|
|
24
|
+
const drafts = await mineFile(abs, projectRoot);
|
|
25
|
+
count = drafts.length;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
if (count === 0) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { requireInit } = require('../lib/guard');
|
|
7
|
+
const { readIndex, LORE_DIR } = require('../lib/index');
|
|
8
|
+
const { readEntry } = require('../lib/entries');
|
|
9
|
+
const { computeScore } = require('../lib/scorer');
|
|
10
|
+
const { listDrafts, acceptDraft, deleteDraft } = require('../lib/drafts');
|
|
11
|
+
|
|
12
|
+
// Only load 'open' dynamically to avoid overhead on other CLI commands if not needed
|
|
13
|
+
async function openBrowser(url) {
|
|
14
|
+
const open = (await import('open')).default;
|
|
15
|
+
await open(url);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ui(options) {
|
|
19
|
+
requireInit();
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
const PORT = options.port || 3333;
|
|
23
|
+
|
|
24
|
+
app.use(express.json());
|
|
25
|
+
|
|
26
|
+
// CORS for local dev
|
|
27
|
+
app.use((req, res, next) => {
|
|
28
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
29
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
30
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
|
|
31
|
+
next();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// API Endpoints
|
|
35
|
+
|
|
36
|
+
app.get('/api/stats', (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const scoreData = computeScore();
|
|
39
|
+
const drafts = listDrafts();
|
|
40
|
+
|
|
41
|
+
const index = readIndex();
|
|
42
|
+
const counts = { decision: 0, invariant: 0, graveyard: 0, gotcha: 0 };
|
|
43
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
44
|
+
const entry = readEntry(entryPath);
|
|
45
|
+
if (entry && counts[entry.type] !== undefined) counts[entry.type]++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
res.json({
|
|
49
|
+
score: scoreData,
|
|
50
|
+
counts,
|
|
51
|
+
draftCount: drafts.length,
|
|
52
|
+
totalEntries: Object.keys(index.entries).length
|
|
53
|
+
});
|
|
54
|
+
} catch (e) {
|
|
55
|
+
res.status(500).json({ error: e.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
app.get('/api/entries', (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const index = readIndex();
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
64
|
+
const entry = readEntry(entryPath);
|
|
65
|
+
if (entry) entries.push(entry);
|
|
66
|
+
}
|
|
67
|
+
// Sort newest first
|
|
68
|
+
entries.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
69
|
+
res.json(entries);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
res.status(500).json({ error: e.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
app.get('/api/drafts', (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const drafts = listDrafts();
|
|
78
|
+
res.json(drafts);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
res.status(500).json({ error: e.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.post('/api/drafts/:id/accept', (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const entry = acceptDraft(req.params.id);
|
|
87
|
+
res.json({ success: true, entry });
|
|
88
|
+
} catch (e) {
|
|
89
|
+
res.status(500).json({ error: e.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
app.delete('/api/drafts/:id', (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
deleteDraft(req.params.id);
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
res.status(500).json({ error: e.message });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.get('/api/graph', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const { loadGraph, saveGraph } = require('../lib/graph');
|
|
105
|
+
let g = loadGraph();
|
|
106
|
+
|
|
107
|
+
if (Object.keys(g.imports).length === 0) {
|
|
108
|
+
const { buildFullGraph } = require('../watcher/graph');
|
|
109
|
+
g = buildFullGraph(process.cwd());
|
|
110
|
+
saveGraph(g);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nodesSet = new Set();
|
|
114
|
+
const edges = [];
|
|
115
|
+
for (const [file, deps] of Object.entries(g.imports)) {
|
|
116
|
+
nodesSet.add(file);
|
|
117
|
+
for (const dep of deps) {
|
|
118
|
+
nodesSet.add(dep);
|
|
119
|
+
edges.push({ from: file, to: dep });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const nodes = Array.from(nodesSet).map(id => ({ id, label: id }));
|
|
124
|
+
res.json({ nodes, edges });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
res.status(500).json({ error: e.message });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Handle unmapped API routes with 404 JSON (instead of serving index.html)
|
|
131
|
+
app.use('/api', (req, res) => {
|
|
132
|
+
res.status(404).json({ error: 'API route not found' });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Serve the frontend application
|
|
136
|
+
const uiPath = path.join(__dirname, '..', 'ui', 'public');
|
|
137
|
+
app.use(express.static(uiPath));
|
|
138
|
+
|
|
139
|
+
// Catch-all to serve index.html for SPA routing
|
|
140
|
+
app.use((req, res) => {
|
|
141
|
+
res.sendFile(path.join(uiPath, 'index.html'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const server = app.listen(PORT, () => {
|
|
145
|
+
const url = `http://localhost:${PORT}`;
|
|
146
|
+
console.log(chalk.green(`\n🚀 Lore UI Dashboard running at ${chalk.bold(url)}\n`));
|
|
147
|
+
console.log(chalk.cyan(` Press Ctrl+C to stop the server.`));
|
|
148
|
+
|
|
149
|
+
// Use native exec to open browser to avoid ESM import issues with 'open'
|
|
150
|
+
const { exec } = require('child_process');
|
|
151
|
+
const startPath = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
152
|
+
exec(`${startPath} ${url}`, (err) => {
|
|
153
|
+
if (err) {
|
|
154
|
+
console.log(chalk.dim(` (Could not open browser automatically. Please visit ${url} manually)`));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
server.on('error', (e) => {
|
|
160
|
+
if (e.code === 'EADDRINUSE') {
|
|
161
|
+
console.error(chalk.red(`\nPort ${PORT} is already in use by another process.`));
|
|
162
|
+
console.error(chalk.yellow(`Use 'lore ui --port <number>' to specify a different port.\n`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
} else {
|
|
165
|
+
console.error(chalk.red(`\nFailed to start server: ${e.message}\n`));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = ui;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
// --- Security ---
|
|
3
|
+
function escapeHtml(unsafe) {
|
|
4
|
+
if (!unsafe) return '';
|
|
5
|
+
return unsafe.toString()
|
|
6
|
+
.replace(/&/g, "&")
|
|
7
|
+
.replace(/</g, "<")
|
|
8
|
+
.replace(/>/g, ">")
|
|
9
|
+
.replace(/"/g, """)
|
|
10
|
+
.replace(/'/g, "'");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// --- Navigation ---
|
|
14
|
+
const navLinks = document.querySelectorAll('.nav-links a');
|
|
15
|
+
const views = document.querySelectorAll('.view');
|
|
16
|
+
|
|
17
|
+
function showView(targetId) {
|
|
18
|
+
navLinks.forEach(l => l.classList.remove('active'));
|
|
19
|
+
document.querySelector(`[data-target="${targetId}"]`).classList.add('active');
|
|
20
|
+
|
|
21
|
+
views.forEach(v => v.classList.remove('active'));
|
|
22
|
+
document.getElementById(`view-${targetId}`).classList.add('active');
|
|
23
|
+
|
|
24
|
+
if (targetId === 'graph') {
|
|
25
|
+
loadGraph();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
navLinks.forEach(link => {
|
|
30
|
+
link.addEventListener('click', (e) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
showView(e.target.dataset.target);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// --- Toast Notifications ---
|
|
37
|
+
function showToast(msg) {
|
|
38
|
+
const toast = document.getElementById('toast');
|
|
39
|
+
toast.textContent = msg;
|
|
40
|
+
toast.classList.remove('hidden');
|
|
41
|
+
setTimeout(() => toast.classList.add('hidden'), 3000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Data Loading ---
|
|
45
|
+
async function loadStats() {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch('/api/stats');
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
|
|
50
|
+
// Dashboard
|
|
51
|
+
document.getElementById('score-value').textContent = data.score.score;
|
|
52
|
+
document.getElementById('stat-coverage').textContent = `${data.score.coverage}%`;
|
|
53
|
+
document.getElementById('fill-coverage').style.width = `${data.score.coverage}%`;
|
|
54
|
+
document.getElementById('stat-freshness').textContent = `${data.score.freshness}%`;
|
|
55
|
+
document.getElementById('fill-freshness').style.width = `${data.score.freshness}%`;
|
|
56
|
+
document.getElementById('stat-depth').textContent = `${data.score.depth}%`;
|
|
57
|
+
document.getElementById('fill-depth').style.width = `${data.score.depth}%`;
|
|
58
|
+
|
|
59
|
+
// Tips
|
|
60
|
+
const tipsUl = document.getElementById('score-tips');
|
|
61
|
+
tipsUl.innerHTML = '';
|
|
62
|
+
if (data.score.topUnlogged && data.score.topUnlogged.length > 0) {
|
|
63
|
+
const top = data.score.topUnlogged[0];
|
|
64
|
+
const li = document.createElement('li');
|
|
65
|
+
li.textContent = `CRITICAL: Unlogged high-activity module [${top.module}] (${top.commits} commits)`;
|
|
66
|
+
tipsUl.appendChild(li);
|
|
67
|
+
} else {
|
|
68
|
+
tipsUl.innerHTML = '<li>System nominal. All core modules documented.</li>';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Nav Badge
|
|
72
|
+
document.getElementById('nav-draft-count').textContent = data.draftCount;
|
|
73
|
+
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('Failed to load stats', e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let allEntries = [];
|
|
80
|
+
|
|
81
|
+
async function loadEntries() {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch('/api/entries');
|
|
84
|
+
allEntries = await res.json();
|
|
85
|
+
renderEntries(allEntries);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('Failed to load entries', e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderEntries(entries) {
|
|
92
|
+
const container = document.getElementById('kb-list');
|
|
93
|
+
container.innerHTML = '';
|
|
94
|
+
|
|
95
|
+
if (entries.length === 0) {
|
|
96
|
+
container.innerHTML = '<p class="muted">No knowledge entries found in memory banks.</p>';
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
entries.forEach(entry => {
|
|
101
|
+
const card = document.createElement('div');
|
|
102
|
+
card.className = 'entry-card';
|
|
103
|
+
|
|
104
|
+
const badgeClass = `type-${entry.type.toLowerCase()}`;
|
|
105
|
+
|
|
106
|
+
let filesHtml = '';
|
|
107
|
+
if (entry.files && entry.files.length > 0) {
|
|
108
|
+
filesHtml = `<div class="entry-meta">Files: ${escapeHtml(entry.files.join(', '))}</div>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let tagsHtml = '';
|
|
112
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
113
|
+
tagsHtml = `<div class="entry-meta">Tags: ${escapeHtml(entry.tags.join(', '))}</div>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
card.innerHTML = `
|
|
117
|
+
<div class="entry-header">
|
|
118
|
+
<span class="type-badge ${escapeHtml(badgeClass)}">[${escapeHtml(entry.type).toUpperCase()}]</span>
|
|
119
|
+
<span class="entry-date muted">${escapeHtml(entry.date.split('T')[0])}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="entry-title">${escapeHtml(entry.title)}</div>
|
|
122
|
+
<div class="entry-context">${escapeHtml(entry.context)}</div>
|
|
123
|
+
${filesHtml}
|
|
124
|
+
${tagsHtml}
|
|
125
|
+
`;
|
|
126
|
+
container.appendChild(card);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Knowledge Base Filter & Search
|
|
131
|
+
document.getElementById('kb-search').addEventListener('input', (e) => {
|
|
132
|
+
const query = e.target.value.toLowerCase();
|
|
133
|
+
const typeMode = document.getElementById('kb-filter').value;
|
|
134
|
+
const filtered = allEntries.filter(e => {
|
|
135
|
+
const matchQuery = e.title.toLowerCase().includes(query) || e.context.toLowerCase().includes(query);
|
|
136
|
+
const matchType = typeMode === 'all' || e.type === typeMode;
|
|
137
|
+
return matchQuery && matchType;
|
|
138
|
+
});
|
|
139
|
+
renderEntries(filtered);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
document.getElementById('kb-filter').addEventListener('change', (e) => {
|
|
143
|
+
const typeMode = e.target.value;
|
|
144
|
+
const query = document.getElementById('kb-search').value.toLowerCase();
|
|
145
|
+
const filtered = allEntries.filter(e => {
|
|
146
|
+
const matchQuery = e.title.toLowerCase().includes(query) || e.context.toLowerCase().includes(query);
|
|
147
|
+
const matchType = typeMode === 'all' || e.type === typeMode;
|
|
148
|
+
return matchQuery && matchType;
|
|
149
|
+
});
|
|
150
|
+
renderEntries(filtered);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Drafts
|
|
154
|
+
async function loadDrafts() {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch('/api/drafts');
|
|
157
|
+
const drafts = await res.json();
|
|
158
|
+
|
|
159
|
+
const container = document.getElementById('drafts-list');
|
|
160
|
+
container.innerHTML = '';
|
|
161
|
+
|
|
162
|
+
if (drafts.length === 0) {
|
|
163
|
+
container.innerHTML = '<p class="muted">All drafts have been processed.</p>';
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
drafts.forEach(draft => {
|
|
168
|
+
const confPercent = Math.round((draft.confidence || 0) * 100);
|
|
169
|
+
const card = document.createElement('div');
|
|
170
|
+
card.className = 'entry-card';
|
|
171
|
+
card.id = `draft-${draft.draftId}`;
|
|
172
|
+
|
|
173
|
+
const badgeClass = `type-${draft.suggestedType.toLowerCase()}`;
|
|
174
|
+
|
|
175
|
+
let filesHtml = '';
|
|
176
|
+
if (draft.files && draft.files.length > 0) {
|
|
177
|
+
filesHtml = `<div class="entry-meta">Linked File: ${escapeHtml(draft.files[0])}</div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
card.innerHTML = `
|
|
181
|
+
<div class="entry-header">
|
|
182
|
+
<span class="type-badge ${escapeHtml(badgeClass)}">SUGGESTED: [${escapeHtml(draft.suggestedType).toUpperCase()}]</span>
|
|
183
|
+
<span class="entry-date muted">Confidence: ${confPercent}%</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="entry-title">${escapeHtml(draft.suggestedTitle)}</div>
|
|
186
|
+
<div class="entry-context">Evidence: ${escapeHtml(draft.evidence)}</div>
|
|
187
|
+
${filesHtml}
|
|
188
|
+
<div class="draft-actions">
|
|
189
|
+
<button class="btn btn-accept" onclick="acceptDraft('${escapeHtml(draft.draftId)}')">Accept</button>
|
|
190
|
+
<button class="btn btn-delete" onclick="deleteDraft('${escapeHtml(draft.draftId)}')">Delete</button>
|
|
191
|
+
</div>
|
|
192
|
+
`;
|
|
193
|
+
container.appendChild(card);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
} catch (e) {
|
|
197
|
+
console.error('Failed to load drafts', e);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Expose actions to global scope for inline handlers
|
|
202
|
+
window.acceptDraft = async (id) => {
|
|
203
|
+
try {
|
|
204
|
+
const res = await fetch(`/api/drafts/${id}/accept`, { method: 'POST' });
|
|
205
|
+
if (res.ok) {
|
|
206
|
+
document.getElementById(`draft-${id}`).remove();
|
|
207
|
+
showToast('Draft Accepted into Memory');
|
|
208
|
+
loadStats(); // update count
|
|
209
|
+
loadEntries(); // refresh KB
|
|
210
|
+
}
|
|
211
|
+
} catch (e) { }
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
window.deleteDraft = async (id) => {
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(`/api/drafts/${id}`, { method: 'DELETE' });
|
|
217
|
+
if (res.ok) {
|
|
218
|
+
document.getElementById(`draft-${id}`).remove();
|
|
219
|
+
showToast('Draft Deleted');
|
|
220
|
+
loadStats();
|
|
221
|
+
}
|
|
222
|
+
} catch (e) { }
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Graph
|
|
226
|
+
let network = null;
|
|
227
|
+
async function loadGraph() {
|
|
228
|
+
if (network) return; // already loaded
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const res = await fetch('/api/graph');
|
|
232
|
+
const graphData = await res.json();
|
|
233
|
+
|
|
234
|
+
const container = document.getElementById('network-container');
|
|
235
|
+
|
|
236
|
+
const options = {
|
|
237
|
+
layout: {
|
|
238
|
+
hierarchical: {
|
|
239
|
+
enabled: true,
|
|
240
|
+
direction: 'UD', // Up-Down
|
|
241
|
+
sortMethod: 'directed', // Follows dependency arrows
|
|
242
|
+
levelSeparation: 150, // Space between tiers
|
|
243
|
+
nodeSpacing: 250,
|
|
244
|
+
treeSpacing: 400,
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
interaction: {
|
|
248
|
+
hover: true,
|
|
249
|
+
tooltipDelay: 200,
|
|
250
|
+
hideEdgesOnDrag: true
|
|
251
|
+
},
|
|
252
|
+
nodes: {
|
|
253
|
+
shape: 'dot',
|
|
254
|
+
size: 16,
|
|
255
|
+
font: { color: '#00FF41', face: 'monospace', size: 12 },
|
|
256
|
+
color: {
|
|
257
|
+
background: '#050505',
|
|
258
|
+
border: '#008F11',
|
|
259
|
+
highlight: { background: '#00FF41', border: '#FFFFFF' },
|
|
260
|
+
hover: { background: '#008F11', border: '#00FF41' }
|
|
261
|
+
},
|
|
262
|
+
shadow: { enabled: true, color: 'rgba(0, 255, 65, 0.4)', size: 10, x: 0, y: 0 }
|
|
263
|
+
},
|
|
264
|
+
edges: {
|
|
265
|
+
color: { color: '#004F09', highlight: '#00FF41', hover: '#008F11' },
|
|
266
|
+
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
267
|
+
smooth: { type: 'cubicBezier', forceDirection: 'vertical', roundness: 0.4 }
|
|
268
|
+
},
|
|
269
|
+
physics: {
|
|
270
|
+
enabled: false // Physics usually conflicts with strict hierarchical layouts
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
network = new vis.Network(container, graphData, options);
|
|
275
|
+
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error('Failed to load graph', e);
|
|
278
|
+
document.getElementById('network-container').innerHTML = '<p class="muted" style="padding: 20px;">Could not render graph data.</p>';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Init
|
|
283
|
+
loadStats();
|
|
284
|
+
loadEntries();
|
|
285
|
+
loadDrafts();
|
|
286
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Lore Dashboard</title>
|
|
7
|
+
<!-- Retro Fonts -->
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fira+Code:wght@400;600&display=swap" rel="stylesheet">
|
|
9
|
+
<!-- Vis Network for Graphs -->
|
|
10
|
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
11
|
+
<link rel="stylesheet" href="style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div class="crt-overlay"></div>
|
|
15
|
+
|
|
16
|
+
<div class="layout">
|
|
17
|
+
<!-- Sidebar Navigation -->
|
|
18
|
+
<nav class="sidebar">
|
|
19
|
+
<div class="logo">
|
|
20
|
+
<pre>
|
|
21
|
+
██╗ ██████╗ ██████╗ ███████╗
|
|
22
|
+
██║ ██╔═══██╗██╔══██╗██╔════╝
|
|
23
|
+
██║ ██║ ██║██████╔╝█████╗
|
|
24
|
+
██║ ██║ ██║██╔══██╗██╔══╝
|
|
25
|
+
███████╗╚██████╔╝██║ ██║███████╗
|
|
26
|
+
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
|
27
|
+
</pre>
|
|
28
|
+
<div class="subtitle">PROJECT MEMORY</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<ul class="nav-links">
|
|
32
|
+
<li><a href="#" class="active" data-target="dashboard">Dashboard</a></li>
|
|
33
|
+
<li><a href="#" data-target="knowledge">Knowledge Base</a></li>
|
|
34
|
+
<li><a href="#" data-target="drafts">Pending Drafts (<span id="nav-draft-count">0</span>)</a></li>
|
|
35
|
+
<li><a href="#" data-target="graph">Dependency Graph</a></li>
|
|
36
|
+
</ul>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<!-- Main Content Area -->
|
|
40
|
+
<main class="content">
|
|
41
|
+
|
|
42
|
+
<!-- Dashboard View -->
|
|
43
|
+
<section id="view-dashboard" class="view active">
|
|
44
|
+
<h1 class="glitch" data-text="LORE SCORE">LORE SCORE</h1>
|
|
45
|
+
<div class="score-card">
|
|
46
|
+
<div class="main-score">
|
|
47
|
+
<span id="score-value">--</span><span class="muted">/100</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="metrics-grid">
|
|
52
|
+
<div class="metric">
|
|
53
|
+
<h3>COVERAGE</h3>
|
|
54
|
+
<div class="progress-bar"><div class="fill" id="fill-coverage"></div></div>
|
|
55
|
+
<p class="stats" id="stat-coverage">--%</p>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="metric">
|
|
58
|
+
<h3>FRESHNESS</h3>
|
|
59
|
+
<div class="progress-bar"><div class="fill" id="fill-freshness"></div></div>
|
|
60
|
+
<p class="stats" id="stat-freshness">--%</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="metric">
|
|
63
|
+
<h3>DEPTH</h3>
|
|
64
|
+
<div class="progress-bar"><div class="fill" id="fill-depth"></div></div>
|
|
65
|
+
<p class="stats" id="stat-depth">--%</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="tips-container">
|
|
70
|
+
<h3>SYSTEM ALERTS</h3>
|
|
71
|
+
<ul id="score-tips" class="terminal-list">
|
|
72
|
+
<li>Loading heuristics...</li>
|
|
73
|
+
</ul>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<!-- Knowledge Base View -->
|
|
78
|
+
<section id="view-knowledge" class="view">
|
|
79
|
+
<h1>KNOWLEDGE BASE</h1>
|
|
80
|
+
<div class="controls">
|
|
81
|
+
<input type="text" id="kb-search" placeholder="Search memory..._">
|
|
82
|
+
<select id="kb-filter">
|
|
83
|
+
<option value="all">ALL TYPES</option>
|
|
84
|
+
<option value="decision">DECISIONS</option>
|
|
85
|
+
<option value="invariant">INVARIANTS</option>
|
|
86
|
+
<option value="gotcha">GOTCHAS</option>
|
|
87
|
+
<option value="graveyard">GRAVEYARD</option>
|
|
88
|
+
</select>
|
|
89
|
+
</div>
|
|
90
|
+
<div id="kb-list" class="entries-grid">
|
|
91
|
+
<!-- Entries injected here by JS -->
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
<!-- Drafts View -->
|
|
96
|
+
<section id="view-drafts" class="view">
|
|
97
|
+
<h1>PENDING DRAFTS</h1>
|
|
98
|
+
<p class="subtitle">Awaiting human approval.</p>
|
|
99
|
+
<div id="drafts-list" class="drafts-container">
|
|
100
|
+
<!-- Drafts injected here by JS -->
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<!-- Graph View -->
|
|
105
|
+
<section id="view-graph" class="view">
|
|
106
|
+
<h1>DEPENDENCY GRAPH</h1>
|
|
107
|
+
<div id="network-container"></div>
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
</main>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Modals / Tooltips -->
|
|
114
|
+
<div id="toast" class="toast hidden">Action successful</div>
|
|
115
|
+
|
|
116
|
+
<script src="app.js"></script>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-color: #050505;
|
|
3
|
+
--panel-bg: #111111;
|
|
4
|
+
--text-main: #00FF41; /* CRT Green */
|
|
5
|
+
--text-muted: #008F11;
|
|
6
|
+
--accent-decision: #00FFFF; /* Cyan */
|
|
7
|
+
--accent-invariant: #FF003C; /* Red */
|
|
8
|
+
--accent-gotcha: #FFB000; /* Amber */
|
|
9
|
+
--accent-graveyard: #555555;
|
|
10
|
+
--border: 1px solid #008F11;
|
|
11
|
+
--font-pixel: 'Press Start 2P', cursive;
|
|
12
|
+
--font-code: 'Fira Code', 'Courier New', monospace;
|
|
13
|
+
--glitch-offset: 2px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
background-color: var(--bg-color);
|
|
24
|
+
color: var(--text-main);
|
|
25
|
+
font-family: var(--font-code);
|
|
26
|
+
font-size: 14px;
|
|
27
|
+
line-height: 1.6;
|
|
28
|
+
overflow: hidden; /* App feels like a terminal */
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* CRT Scanline Overlay Effect */
|
|
32
|
+
.crt-overlay {
|
|
33
|
+
position: fixed;
|
|
34
|
+
top: 0;
|
|
35
|
+
left: 0;
|
|
36
|
+
width: 100vw;
|
|
37
|
+
height: 100vh;
|
|
38
|
+
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
|
39
|
+
background-size: 100% 4px, 3px 100%;
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
z-index: 999;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Layout */
|
|
45
|
+
.layout {
|
|
46
|
+
display: flex;
|
|
47
|
+
height: 100vh;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Sidebar */
|
|
51
|
+
.sidebar {
|
|
52
|
+
width: 350px;
|
|
53
|
+
background-color: var(--panel-bg);
|
|
54
|
+
border-right: var(--border);
|
|
55
|
+
padding: 30px;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.logo pre {
|
|
61
|
+
font-family: monospace;
|
|
62
|
+
color: var(--text-main);
|
|
63
|
+
font-size: 8px; /* Fit the ASCII art */
|
|
64
|
+
white-space: pre;
|
|
65
|
+
margin-bottom: 5px;
|
|
66
|
+
line-height: 1.2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.subtitle {
|
|
70
|
+
font-family: var(--font-code);
|
|
71
|
+
color: var(--text-muted);
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
letter-spacing: 2px;
|
|
74
|
+
margin-bottom: 40px;
|
|
75
|
+
text-align: center;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.nav-links {
|
|
79
|
+
list-style: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.nav-links li {
|
|
83
|
+
margin-bottom: 15px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nav-links a {
|
|
87
|
+
color: var(--text-muted);
|
|
88
|
+
text-decoration: none;
|
|
89
|
+
font-family: var(--font-pixel);
|
|
90
|
+
font-size: 10px;
|
|
91
|
+
display: block;
|
|
92
|
+
padding: 10px;
|
|
93
|
+
border: 1px solid transparent;
|
|
94
|
+
transition: all 0.2s ease;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.nav-links a:hover, .nav-links a.active {
|
|
98
|
+
color: var(--bg-color);
|
|
99
|
+
background-color: var(--text-main);
|
|
100
|
+
border: 1px solid var(--text-main);
|
|
101
|
+
box-shadow: 0 0 10px var(--text-main);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Main Content Area */
|
|
105
|
+
.content {
|
|
106
|
+
flex: 1;
|
|
107
|
+
padding: 40px;
|
|
108
|
+
overflow-y: auto;
|
|
109
|
+
position: relative;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.view {
|
|
113
|
+
display: none;
|
|
114
|
+
animation: flicker 0.15s ease-in 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.view.active {
|
|
118
|
+
display: block;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
h1 {
|
|
122
|
+
font-family: var(--font-pixel);
|
|
123
|
+
font-size: 24px;
|
|
124
|
+
margin-bottom: 30px;
|
|
125
|
+
color: var(--text-main);
|
|
126
|
+
text-shadow: 0 0 5px var(--text-main);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Dashboard Score Components */
|
|
130
|
+
.score-card {
|
|
131
|
+
border: var(--border);
|
|
132
|
+
padding: 40px;
|
|
133
|
+
text-align: center;
|
|
134
|
+
margin-bottom: 30px;
|
|
135
|
+
background: repeating-linear-gradient(
|
|
136
|
+
45deg,
|
|
137
|
+
transparent,
|
|
138
|
+
transparent 10px,
|
|
139
|
+
rgba(0, 143, 17, 0.05) 10px,
|
|
140
|
+
rgba(0, 143, 17, 0.05) 20px
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.main-score {
|
|
145
|
+
font-family: var(--font-pixel);
|
|
146
|
+
font-size: 64px;
|
|
147
|
+
text-shadow: 0 0 15px var(--text-main);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.muted {
|
|
151
|
+
font-size: 24px;
|
|
152
|
+
color: var(--text-muted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.metrics-grid {
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
158
|
+
gap: 20px;
|
|
159
|
+
margin-bottom: 40px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.metric h3 {
|
|
163
|
+
font-family: var(--font-pixel);
|
|
164
|
+
font-size: 10px;
|
|
165
|
+
margin-bottom: 15px;
|
|
166
|
+
color: var(--text-muted);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.progress-bar {
|
|
170
|
+
height: 20px;
|
|
171
|
+
border: var(--border);
|
|
172
|
+
padding: 2px;
|
|
173
|
+
margin-bottom: 10px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.progress-bar .fill {
|
|
177
|
+
height: 100%;
|
|
178
|
+
background-color: var(--text-main);
|
|
179
|
+
width: 0%;
|
|
180
|
+
transition: width 1s ease-out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.tips-container {
|
|
184
|
+
border-top: var(--border);
|
|
185
|
+
padding-top: 20px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.terminal-list {
|
|
189
|
+
list-style: none;
|
|
190
|
+
}
|
|
191
|
+
.terminal-list li::before {
|
|
192
|
+
content: "> ";
|
|
193
|
+
color: var(--accent-gotcha);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Knowledge Base Entries */
|
|
197
|
+
.controls {
|
|
198
|
+
display: flex;
|
|
199
|
+
gap: 15px;
|
|
200
|
+
margin-bottom: 20px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
input, select, button {
|
|
204
|
+
background: var(--bg-color);
|
|
205
|
+
color: var(--text-main);
|
|
206
|
+
border: var(--border);
|
|
207
|
+
padding: 10px;
|
|
208
|
+
font-family: var(--font-code);
|
|
209
|
+
font-size: 14px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
input:focus, select:focus {
|
|
213
|
+
outline: none;
|
|
214
|
+
box-shadow: 0 0 8px var(--text-muted);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.entries-grid, .drafts-container {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 20px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.entry-card {
|
|
224
|
+
border: var(--border);
|
|
225
|
+
padding: 20px;
|
|
226
|
+
background-color: rgba(17, 17, 17, 0.8);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.entry-header {
|
|
230
|
+
display: flex;
|
|
231
|
+
justify-content: space-between;
|
|
232
|
+
border-bottom: 1px dashed var(--text-muted);
|
|
233
|
+
padding-bottom: 10px;
|
|
234
|
+
margin-bottom: 15px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.type-badge {
|
|
238
|
+
font-family: var(--font-pixel);
|
|
239
|
+
font-size: 10px;
|
|
240
|
+
padding: 4px 8px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.type-decision { color: var(--bg-color); background-color: var(--accent-decision); }
|
|
244
|
+
.type-invariant { color: var(--bg-color); background-color: var(--accent-invariant); }
|
|
245
|
+
.type-gotcha { color: var(--bg-color); background-color: var(--accent-gotcha); }
|
|
246
|
+
.type-graveyard { color: var(--text-main); background-color: var(--accent-graveyard); }
|
|
247
|
+
|
|
248
|
+
.entry-title {
|
|
249
|
+
font-weight: bold;
|
|
250
|
+
font-size: 16px;
|
|
251
|
+
margin-bottom: 10px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.entry-meta {
|
|
255
|
+
color: var(--text-muted);
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
margin-top: 15px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Draft Specifics */
|
|
261
|
+
.draft-actions {
|
|
262
|
+
display: flex;
|
|
263
|
+
gap: 10px;
|
|
264
|
+
margin-top: 15px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.btn {
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
font-family: var(--font-pixel);
|
|
270
|
+
font-size: 8px;
|
|
271
|
+
text-transform: uppercase;
|
|
272
|
+
transition: all 0.2s;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.btn-accept {
|
|
276
|
+
border-color: var(--text-main);
|
|
277
|
+
color: var(--text-main);
|
|
278
|
+
}
|
|
279
|
+
.btn-accept:hover { background: var(--text-main); color: var(--bg-color); }
|
|
280
|
+
|
|
281
|
+
.btn-delete {
|
|
282
|
+
border-color: var(--accent-invariant);
|
|
283
|
+
color: var(--accent-invariant);
|
|
284
|
+
}
|
|
285
|
+
.btn-delete:hover { background: var(--accent-invariant); color: var(--bg-color); }
|
|
286
|
+
|
|
287
|
+
/* Graph Container */
|
|
288
|
+
#network-container {
|
|
289
|
+
width: 100%;
|
|
290
|
+
height: 600px;
|
|
291
|
+
border: var(--border);
|
|
292
|
+
background-color: var(--panel-bg);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Animations */
|
|
296
|
+
@keyframes flicker {
|
|
297
|
+
0% { opacity: 0; }
|
|
298
|
+
10% { opacity: 0.5; }
|
|
299
|
+
20% { opacity: 0; }
|
|
300
|
+
50% { opacity: 1; }
|
|
301
|
+
60% { opacity: 0.8; }
|
|
302
|
+
100% { opacity: 1; }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.toast {
|
|
306
|
+
position: fixed;
|
|
307
|
+
bottom: 20px;
|
|
308
|
+
right: 20px;
|
|
309
|
+
background-color: var(--text-main);
|
|
310
|
+
color: var(--bg-color);
|
|
311
|
+
padding: 15px 30px;
|
|
312
|
+
font-family: var(--font-code);
|
|
313
|
+
font-weight: bold;
|
|
314
|
+
border: 1px solid white;
|
|
315
|
+
z-index: 1000;
|
|
316
|
+
transition: opacity 0.3s;
|
|
317
|
+
}
|
|
318
|
+
.toast.hidden {
|
|
319
|
+
opacity: 0;
|
|
320
|
+
pointer-events: none;
|
|
321
|
+
}
|
package/src/watcher/comments.js
CHANGED
|
@@ -92,7 +92,7 @@ async function mineFile(absFilePath, projectRoot) {
|
|
|
92
92
|
* @param {string[]} ignore
|
|
93
93
|
* @returns {number} total drafts created
|
|
94
94
|
*/
|
|
95
|
-
function mineDirectory(absDirPath, projectRoot, ignore) {
|
|
95
|
+
async function mineDirectory(absDirPath, projectRoot, ignore) {
|
|
96
96
|
const { globSync } = require('glob');
|
|
97
97
|
const ignoreList = ignore || ['node_modules', 'dist', '.git', '.lore', 'coverage'];
|
|
98
98
|
const ignorePats = ignoreList.map(i => `${i}/**`);
|
|
@@ -105,7 +105,8 @@ function mineDirectory(absDirPath, projectRoot, ignore) {
|
|
|
105
105
|
|
|
106
106
|
let total = 0;
|
|
107
107
|
for (const file of files) {
|
|
108
|
-
|
|
108
|
+
const drafts = await mineFile(file, projectRoot);
|
|
109
|
+
total += drafts.length;
|
|
109
110
|
}
|
|
110
111
|
return total;
|
|
111
112
|
}
|