@alfacapital-tech/c4builder 0.8.0 → 0.9.0-rc.2
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/agent.js +21 -0
- package/dist/cli/commands/feedback.js +45 -0
- package/dist/cli/commands/site.comments.js +585 -0
- package/dist/cli/commands/site.js +242 -2
- package/dist/cli/dispatch.js +24 -10
- package/dist/config/defaults.js +2 -1
- package/dist/config/schema.js +3 -0
- package/dist/core/build.js +3 -3
- package/dist/core/compose/markdown.js +5 -1
- package/dist/core/feedback.js +59 -0
- package/dist/core/plugins/tree.js +3 -1
- package/dist/core/questions.js +227 -0
- package/dist/core/scan/tree.js +3 -1
- package/dist/plugins/openspec/index.js +6 -1
- package/dist/plugins/openspec/render.js +6 -3
- package/dist/util/lock.js +23 -0
- package/package.json +1 -1
- package/template/AGENTS.md +7 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const AGENT_VARS = [
|
|
2
|
+
['CLAUDECODE', 'claude-code'],
|
|
3
|
+
['CODEX_THREAD_ID', 'codex'],
|
|
4
|
+
['GEMINI_CLI', 'gemini-cli'],
|
|
5
|
+
['OPENCODE', 'opencode'],
|
|
6
|
+
['COPILOT_CLI', 'copilot-cli']
|
|
7
|
+
];
|
|
8
|
+
const fromEnv = (env) => {
|
|
9
|
+
const own = env.C4BUILDER_AGENT;
|
|
10
|
+
if (own)
|
|
11
|
+
return { on: own !== '0', name: own, explicit: true };
|
|
12
|
+
const known = AGENT_VARS.find(([key]) => env[key]);
|
|
13
|
+
const name = known ? known[1] : env.AI_AGENT;
|
|
14
|
+
return { on: !!name, name: name ?? '', explicit: false };
|
|
15
|
+
};
|
|
16
|
+
export const detectAgent = (env, flags) => {
|
|
17
|
+
const found = fromEnv(env);
|
|
18
|
+
if (flags.agent === undefined)
|
|
19
|
+
return found;
|
|
20
|
+
return { on: flags.agent, name: found.on ? found.name : 'agent', explicit: true };
|
|
21
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { parseConfig } from "../../config/options.js";
|
|
4
|
+
import { FEEDBACK_HEADER, FEEDBACK_PATH } from "./site.comments.js";
|
|
5
|
+
// `c4builder feedback` (change site-agent-feedback, design D9): забирает пачку замечаний у `site -w`.
|
|
6
|
+
// Конфиг читается как JSON без Configstore — тот создал бы `.c4builder` в любом каталоге. Ошибки —
|
|
7
|
+
// через exitCode, а не process.exit: напечатанная в pipe пачка должна успеть уйти целиком.
|
|
8
|
+
export default async (configPath, opts) => {
|
|
9
|
+
let raw;
|
|
10
|
+
try {
|
|
11
|
+
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
const config = parseConfig(raw && typeof raw === 'object' ? raw : {});
|
|
15
|
+
const port = opts.port ?? (config.ok ? config.value : config.salvaged).webPort;
|
|
16
|
+
const shown = `http://localhost:${port}`;
|
|
17
|
+
const fail = (message) => {
|
|
18
|
+
console.error(chalk.red(message));
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
};
|
|
21
|
+
// 127.0.0.1, а не localhost: сервер слушает только IPv4, а `localhost` → `::1` на части машин
|
|
22
|
+
// подвешивает соединение.
|
|
23
|
+
const url = `http://127.0.0.1:${port}${FEEDBACK_PATH}/next${opts.wait ? '' : '?wait=0'}`;
|
|
24
|
+
for (;;) {
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetch(url, { headers: { [FEEDBACK_HEADER]: '1' } });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return fail(`no c4builder site -w on ${shown} — start it first`);
|
|
31
|
+
}
|
|
32
|
+
if (res.status === 200) {
|
|
33
|
+
process.stdout.write(await res.text());
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 204) {
|
|
37
|
+
if (opts.wait)
|
|
38
|
+
continue;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (res.status === 404)
|
|
42
|
+
return fail(`agent feedback is disabled on ${shown} — the server runs without -w or with --no-agent`);
|
|
43
|
+
return fail(`unexpected response ${res.status} from ${shown}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
export const COMMENTS_PATH = '/__c4builder/comments';
|
|
2
|
+
export const FEEDBACK_PATH = '/__c4builder/feedback';
|
|
3
|
+
export const FEEDBACK_HEADER = 'X-C4builder-Feedback';
|
|
4
|
+
const CSS = [
|
|
5
|
+
'.c4b-cbtns{position:absolute;z-index:1001;white-space:nowrap}',
|
|
6
|
+
'.c4b-cbtns button{margin-right:4px;padding:4px 10px;border:1px solid #42b983;border-radius:4px;background:#fff;color:#2c3e50;font:13px/1.4 sans-serif;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.15)}',
|
|
7
|
+
'.c4b-cbtns button.c4b-fb{border-color:#3b82f6}',
|
|
8
|
+
'.c4b-cpop{position:absolute;z-index:1002;width:380px;max-width:calc(100vw - 32px);padding:10px;border:1px solid #ddd;border-radius:6px;background:#fff;color:#2c3e50;font:13px/1.4 sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.2)}',
|
|
9
|
+
'.c4b-cpop blockquote{margin:0 0 8px;padding:4px 8px;border-left:3px solid #42b983;background:#f8f8f8;max-height:80px;overflow:auto;font-size:12px;white-space:pre-wrap}',
|
|
10
|
+
'.c4b-cpop textarea,.c4b-review textarea{width:100%;box-sizing:border-box;min-height:70px;padding:6px;border:1px solid #ccc;border-radius:4px;font:13px/1.4 sans-serif;resize:vertical}',
|
|
11
|
+
'.c4b-cpop .c4b-row{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:8px}',
|
|
12
|
+
'.c4b-cpop small{color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
13
|
+
'.c4b-cpop button,.c4b-review button{padding:4px 12px;border:1px solid #ccc;border-radius:4px;background:#fff;color:#2c3e50;cursor:pointer;font:13px sans-serif}',
|
|
14
|
+
'.c4b-cpop button.c4b-primary,.c4b-review button.c4b-primary{border-color:#42b983;background:#42b983;color:#fff}',
|
|
15
|
+
'.c4b-cpop button:disabled,.c4b-review button:disabled{opacity:.6;cursor:default}',
|
|
16
|
+
// Панель review — ниже кнопок у выделения и поповера: она прибита к низу экрана и иначе
|
|
17
|
+
// перекрывает их у выделения в нижних строках.
|
|
18
|
+
'.c4b-review{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:1000;width:520px;max-width:calc(100vw - 32px);box-sizing:border-box;padding:8px 12px;border:1px solid #3b82f6;border-radius:6px;background:#fff;color:#2c3e50;font:13px/1.4 sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.2)}',
|
|
19
|
+
'.c4b-review .c4b-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}',
|
|
20
|
+
'.c4b-review .c4b-row small{flex:1;color:#888;white-space:nowrap}',
|
|
21
|
+
'.c4b-review button.c4b-link{padding:0;border:0;background:none;color:#2c3e50;font-weight:700;text-decoration:underline dotted}',
|
|
22
|
+
'.c4b-review ul{margin:6px 0;padding:0;list-style:none;max-height:40vh;overflow:auto}',
|
|
23
|
+
'.c4b-review li{padding:3px 0;border-top:1px solid #eee;overflow-wrap:anywhere}',
|
|
24
|
+
'.c4b-review textarea{margin-top:6px}',
|
|
25
|
+
'.c4b-review .c4b-del{opacity:1}',
|
|
26
|
+
'.c4b-ctoast{position:fixed;left:16px;bottom:16px;z-index:1003;max-width:420px;padding:8px 14px;border-radius:6px;background:#2c3e50;color:#fff;font:13px/1.4 sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.3)}',
|
|
27
|
+
'.c4b-ctoast.c4b-err{background:#c0392b}',
|
|
28
|
+
'mark.c4b-hl{background:#fff3a0;color:inherit;padding:0;border-radius:2px}',
|
|
29
|
+
'mark.c4b-dhl{background:#cfe3ff;color:inherit;padding:0;border-radius:2px}',
|
|
30
|
+
'sup.c4b-ref{font-size:.7em;line-height:0;margin-left:1px;user-select:none}',
|
|
31
|
+
'sup.c4b-ref a{color:#b58900;text-decoration:none;font-weight:700;cursor:pointer}',
|
|
32
|
+
'.c4b-qrow{transition:background .3s}',
|
|
33
|
+
'.c4b-qflash{background:#fff3a0!important}',
|
|
34
|
+
'.c4b-del{margin-left:6px;padding:0 5px;border:1px solid transparent;border-radius:3px;background:none;color:#999;font:12px/1.4 sans-serif;cursor:pointer;opacity:0;vertical-align:middle}',
|
|
35
|
+
'.c4b-qrow:hover .c4b-del{opacity:1}',
|
|
36
|
+
'.c4b-del:hover{color:#fff;background:#c0392b;border-color:#c0392b}',
|
|
37
|
+
'.c4b-del:disabled{opacity:.5;cursor:default}'
|
|
38
|
+
].join('');
|
|
39
|
+
const jsString = (s) => JSON.stringify(s)
|
|
40
|
+
.replace(/</g, '\\u003c')
|
|
41
|
+
.replace(/[\u0080-]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
42
|
+
// Начальное состояние агентского режима и ключ проекта вставляет сервер, дальше его обновляют
|
|
43
|
+
// события `c4builder:agent` от сниппета livereload (change site-agent-feedback, design D4, D8).
|
|
44
|
+
export const commentsSnippet = (heading, { agent = false, listening = false, project = '' } = {}) => `
|
|
45
|
+
<script>
|
|
46
|
+
(function () {
|
|
47
|
+
if (window.__c4builderComments) return;
|
|
48
|
+
window.__c4builderComments = true;
|
|
49
|
+
var ENDPOINT = '${COMMENTS_PATH}';
|
|
50
|
+
var FEEDBACK = '${FEEDBACK_PATH}';
|
|
51
|
+
var HEADING = ${jsString(heading)};
|
|
52
|
+
var TOAST_KEY = '__c4builderCommentToast';
|
|
53
|
+
var DRAFT_KEY = '__c4builderReview:' + ${jsString(project)};
|
|
54
|
+
var AGENT = { on: ${agent}, listening: ${listening} };
|
|
55
|
+
var BLOCKS = 'p,li,td,th,h1,h2,h3,h4,h5,h6,pre,blockquote,dd,dt,div,section,details,summary';
|
|
56
|
+
var style = document.createElement('style');
|
|
57
|
+
style.textContent = ${jsString(CSS)};
|
|
58
|
+
document.head.appendChild(style);
|
|
59
|
+
|
|
60
|
+
var collapse = function (s) { return String(s).replace(/\\s+/g, ' ').trim(); };
|
|
61
|
+
var same = function (a, b) { return collapse(a).toLowerCase() === collapse(b).toLowerCase(); };
|
|
62
|
+
|
|
63
|
+
var toastEl = null, toastTimer = 0;
|
|
64
|
+
var toast = function (msg, err) {
|
|
65
|
+
if (toastEl) toastEl.remove();
|
|
66
|
+
toastEl = document.createElement('div');
|
|
67
|
+
toastEl.className = 'c4b-ctoast' + (err ? ' c4b-err' : '');
|
|
68
|
+
toastEl.textContent = msg;
|
|
69
|
+
document.body.appendChild(toastEl);
|
|
70
|
+
clearTimeout(toastTimer);
|
|
71
|
+
toastTimer = setTimeout(function () { if (toastEl) { toastEl.remove(); toastEl = null; } }, err ? 8000 : 5000);
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
var saved = sessionStorage.getItem(TOAST_KEY);
|
|
75
|
+
if (saved) { sessionStorage.removeItem(TOAST_KEY); toast(saved); }
|
|
76
|
+
} catch (e) {}
|
|
77
|
+
|
|
78
|
+
var content = function () { return document.querySelector('.markdown-section'); };
|
|
79
|
+
var headingLevel = function (el) { return /^H[1-6]$/.test(el.tagName) ? Number(el.tagName[1]) : 0; };
|
|
80
|
+
var sectionOf = function (h) {
|
|
81
|
+
var out = [], lvl = headingLevel(h);
|
|
82
|
+
for (var el = h.nextElementSibling; el; el = el.nextElementSibling) {
|
|
83
|
+
var l = headingLevel(el);
|
|
84
|
+
if (l && l <= lvl) break;
|
|
85
|
+
out.push(el);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
};
|
|
89
|
+
var findHeading = function (root, text) {
|
|
90
|
+
var hs = root.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
91
|
+
for (var i = 0; i < hs.length; i++) if (same(hs[i].textContent, text)) return hs[i];
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
var SEP = ' \\u2014 \\u00ab';
|
|
96
|
+
var parseQuestions = function (text) {
|
|
97
|
+
var s = collapse(text), out = [];
|
|
98
|
+
for (var at = s.indexOf(SEP); at >= 0; at = s.indexOf(SEP, at + 1)) {
|
|
99
|
+
var m = s.slice(at + SEP.length).match(/^([\\s\\S]*)\\u00bb(?: \\(\\u00a7 (.*)\\))?$/);
|
|
100
|
+
var quote = m ? m[1].replace(/\\u2026$/, '').trim() : '';
|
|
101
|
+
if (quote) out.push({ comment: s.slice(0, at), quote: quote, section: m[2] || '' });
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
105
|
+
var textIndex = function (roots, skip) {
|
|
106
|
+
var chars = [], map = [], lastBlock = null, prevSpace = true;
|
|
107
|
+
var walk = function (node) {
|
|
108
|
+
if (node.nodeType === 1) {
|
|
109
|
+
if (skip.indexOf(node) >= 0 || (node.tagName === 'SUP' && node.classList.contains('c4b-ref'))) return;
|
|
110
|
+
if (/^(SCRIPT|STYLE)$/.test(node.tagName)) return;
|
|
111
|
+
for (var c = node.firstChild; c; c = c.nextSibling) walk(c);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (node.nodeType !== 3) return;
|
|
115
|
+
var block = node.parentNode.closest ? node.parentNode.closest(BLOCKS) : null;
|
|
116
|
+
if (block !== lastBlock && !prevSpace && chars.length) { chars.push(' '); map.push(null); prevSpace = true; }
|
|
117
|
+
lastBlock = block;
|
|
118
|
+
var s = node.nodeValue;
|
|
119
|
+
for (var i = 0; i < s.length; i++) {
|
|
120
|
+
var ch = s[i];
|
|
121
|
+
if (/\\s/.test(ch)) {
|
|
122
|
+
if (prevSpace) continue;
|
|
123
|
+
chars.push(' '); map.push(null); prevSpace = true;
|
|
124
|
+
} else {
|
|
125
|
+
chars.push(ch); map.push({ node: node, offset: i }); prevSpace = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
for (var r = 0; r < roots.length; r++) walk(roots[r]);
|
|
130
|
+
return { text: chars.join(''), map: map };
|
|
131
|
+
};
|
|
132
|
+
var wrapRange = function (idx, from, to, title, cls) {
|
|
133
|
+
var first = null, last = null, i;
|
|
134
|
+
for (i = from; i < to && !first; i++) first = idx.map[i];
|
|
135
|
+
for (i = to - 1; i >= from && !last; i--) last = idx.map[i];
|
|
136
|
+
if (!first || !last) return null;
|
|
137
|
+
var nodes = [], cur = first.node;
|
|
138
|
+
var walker = document.createTreeWalker(content(), NodeFilter.SHOW_TEXT);
|
|
139
|
+
walker.currentNode = cur;
|
|
140
|
+
while (cur) { nodes.push(cur); if (cur === last.node) break; cur = walker.nextNode(); }
|
|
141
|
+
var marks = [];
|
|
142
|
+
for (i = nodes.length - 1; i >= 0; i--) {
|
|
143
|
+
var n = nodes[i];
|
|
144
|
+
var start = n === first.node ? first.offset : 0;
|
|
145
|
+
var end = n === last.node ? last.offset + 1 : n.nodeValue.length;
|
|
146
|
+
if (end < n.nodeValue.length) n.splitText(end);
|
|
147
|
+
var mid = start > 0 ? n.splitText(start) : n;
|
|
148
|
+
// Overlapping quotes: the number link of a question is not part of the text.
|
|
149
|
+
if (!mid.nodeValue.trim() || mid.parentNode.closest('sup.c4b-ref')) continue;
|
|
150
|
+
var mark = document.createElement('mark');
|
|
151
|
+
mark.className = cls;
|
|
152
|
+
mark.title = title;
|
|
153
|
+
mid.parentNode.insertBefore(mark, mid);
|
|
154
|
+
mark.appendChild(mid);
|
|
155
|
+
marks.unshift(mark);
|
|
156
|
+
}
|
|
157
|
+
return marks;
|
|
158
|
+
};
|
|
159
|
+
var tryIn = function (idx, quote, cls, title) {
|
|
160
|
+
var at = idx.text.indexOf(quote);
|
|
161
|
+
var marks = at < 0 ? null : wrapRange(idx, at, at + quote.length, title, cls);
|
|
162
|
+
return marks && marks.length ? marks : null;
|
|
163
|
+
};
|
|
164
|
+
// The quote is searched in its own section first: the same words may repeat on the page.
|
|
165
|
+
var locate = function (root, q, skip, cls, title) {
|
|
166
|
+
var marks = null;
|
|
167
|
+
if (q.section) {
|
|
168
|
+
var sh = findHeading(root, q.section);
|
|
169
|
+
if (sh && skip.indexOf(sh) < 0) marks = tryIn(textIndex(sectionOf(sh), skip), q.quote, cls, title);
|
|
170
|
+
}
|
|
171
|
+
return marks || tryIn(textIndex([root], skip), q.quote, cls, title);
|
|
172
|
+
};
|
|
173
|
+
var flash = function (row) {
|
|
174
|
+
row.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
175
|
+
row.classList.add('c4b-qflash');
|
|
176
|
+
setTimeout(function () { row.classList.remove('c4b-qflash'); }, 1500);
|
|
177
|
+
};
|
|
178
|
+
var highlight = function () {
|
|
179
|
+
var root = content();
|
|
180
|
+
if (!root || root.querySelector('mark.c4b-hl, .c4b-del')) return;
|
|
181
|
+
var h = findHeading(root, HEADING);
|
|
182
|
+
if (!h) return;
|
|
183
|
+
var section = sectionOf(h);
|
|
184
|
+
var items = [];
|
|
185
|
+
section.forEach(function (el) {
|
|
186
|
+
if (el.tagName === 'TABLE') {
|
|
187
|
+
var rows = el.querySelectorAll('tbody tr');
|
|
188
|
+
for (var i = 0; i < rows.length; i++) {
|
|
189
|
+
var cells = rows[i].querySelectorAll('td');
|
|
190
|
+
if (!cells.length) continue;
|
|
191
|
+
var numeric = /^\\d+$/.test(collapse(cells[0].textContent));
|
|
192
|
+
var textCell = numeric && cells.length > 1 ? cells[1] : cells[0];
|
|
193
|
+
if (!collapse(textCell.textContent)) continue;
|
|
194
|
+
items.push({ row: rows[i], cell: textCell, number: numeric ? collapse(cells[0].textContent) : String(items.length + 1), text: textCell.textContent });
|
|
195
|
+
}
|
|
196
|
+
} else if (/^[UO]L$/.test(el.tagName)) {
|
|
197
|
+
var lis = el.children;
|
|
198
|
+
for (var j = 0; j < lis.length; j++) items.push({ row: lis[j], cell: lis[j], number: String(items.length + 1), text: lis[j].textContent });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
var addDelete = function (item) {
|
|
202
|
+
var del = document.createElement('button');
|
|
203
|
+
del.type = 'button';
|
|
204
|
+
del.className = 'c4b-del';
|
|
205
|
+
del.textContent = '\\u00d7';
|
|
206
|
+
del.title = 'Remove this question';
|
|
207
|
+
del.addEventListener('click', function (e) {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
del.disabled = true;
|
|
211
|
+
send({ page: pageFile(), number: Number(item.number) || null, text: collapse(item.text) }, 'DELETE').then(function (r) {
|
|
212
|
+
var msg = 'Removed from ' + r.file;
|
|
213
|
+
try { sessionStorage.setItem(TOAST_KEY, msg); } catch (e) {}
|
|
214
|
+
toast(msg);
|
|
215
|
+
}, function (err) {
|
|
216
|
+
del.disabled = false;
|
|
217
|
+
toast('Remove failed: ' + (err && err.message ? err.message : err), true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
item.cell.appendChild(del);
|
|
221
|
+
};
|
|
222
|
+
var skip = [h].concat(section);
|
|
223
|
+
items.forEach(function (item) {
|
|
224
|
+
item.row.classList.add('c4b-qrow');
|
|
225
|
+
item.row.id = item.row.id || 'c4b-q-' + item.number;
|
|
226
|
+
addDelete(item);
|
|
227
|
+
// Index is rebuilt per try: wrapping splits text nodes and stales the previous map.
|
|
228
|
+
var q = null, marks = null;
|
|
229
|
+
parseQuestions(item.text).some(function (c) {
|
|
230
|
+
marks = locate(root, c, skip, 'c4b-hl', c.comment);
|
|
231
|
+
q = marks ? c : null;
|
|
232
|
+
return !!marks;
|
|
233
|
+
});
|
|
234
|
+
if (!marks) return;
|
|
235
|
+
var sup = document.createElement('sup');
|
|
236
|
+
sup.className = 'c4b-ref';
|
|
237
|
+
var a = document.createElement('a');
|
|
238
|
+
a.setAttribute('role', 'button');
|
|
239
|
+
a.tabIndex = 0;
|
|
240
|
+
a.textContent = item.number;
|
|
241
|
+
a.title = q.comment;
|
|
242
|
+
a.addEventListener('click', function (e) { e.preventDefault(); flash(item.row); });
|
|
243
|
+
sup.appendChild(a);
|
|
244
|
+
var lastMark = marks[marks.length - 1];
|
|
245
|
+
lastMark.parentNode.insertBefore(sup, lastMark.nextSibling);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
var schedule = function () {
|
|
249
|
+
setTimeout(function () {
|
|
250
|
+
try { highlight(); } catch (e) {}
|
|
251
|
+
try { highlightDraft(); } catch (e) {}
|
|
252
|
+
}, 50);
|
|
253
|
+
};
|
|
254
|
+
var docsifyVm = null;
|
|
255
|
+
if (window.$docsify) {
|
|
256
|
+
window.$docsify.plugins = [].concat(window.$docsify.plugins || [], function (hook, vm) {
|
|
257
|
+
docsifyVm = vm || null;
|
|
258
|
+
hook.doneEach(schedule);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
window.addEventListener('hashchange', function () { setTimeout(schedule, 300); });
|
|
262
|
+
window.addEventListener('load', function () { setTimeout(schedule, 500); });
|
|
263
|
+
|
|
264
|
+
var button = function (label, onClick, cls) {
|
|
265
|
+
var b = document.createElement('button');
|
|
266
|
+
b.type = 'button';
|
|
267
|
+
b.textContent = label;
|
|
268
|
+
if (cls) b.className = cls;
|
|
269
|
+
if (onClick) b.addEventListener('click', onClick);
|
|
270
|
+
return b;
|
|
271
|
+
};
|
|
272
|
+
var btns = document.createElement('div');
|
|
273
|
+
btns.className = 'c4b-cbtns';
|
|
274
|
+
btns.hidden = true;
|
|
275
|
+
var fbBtn = button('Feedback', null, 'c4b-fb');
|
|
276
|
+
fbBtn.title = 'Tell the agent what to change';
|
|
277
|
+
var qBtn = button('Question');
|
|
278
|
+
qBtn.title = 'Add a question to the ' + HEADING + ' section of the source file';
|
|
279
|
+
btns.appendChild(fbBtn);
|
|
280
|
+
btns.appendChild(qBtn);
|
|
281
|
+
document.body.appendChild(btns);
|
|
282
|
+
var pop = null;
|
|
283
|
+
|
|
284
|
+
var selection = function () {
|
|
285
|
+
var sel = window.getSelection();
|
|
286
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
287
|
+
var range = sel.getRangeAt(0);
|
|
288
|
+
var root = content();
|
|
289
|
+
if (!root) return null;
|
|
290
|
+
var node = range.commonAncestorContainer;
|
|
291
|
+
var el = node.nodeType === 1 ? node : node.parentNode;
|
|
292
|
+
if (!root.contains(el) || (pop && pop.contains(el))) return null;
|
|
293
|
+
var text = sel.toString().trim();
|
|
294
|
+
return text ? { range: range, text: text } : null;
|
|
295
|
+
};
|
|
296
|
+
var headingBefore = function (range) {
|
|
297
|
+
var root = content();
|
|
298
|
+
if (!root) return '';
|
|
299
|
+
var hs = root.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
300
|
+
var found = '';
|
|
301
|
+
for (var i = 0; i < hs.length; i++) {
|
|
302
|
+
if (hs[i].compareDocumentPosition(range.startContainer) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
303
|
+
found = hs[i].textContent.trim();
|
|
304
|
+
else break;
|
|
305
|
+
}
|
|
306
|
+
return found;
|
|
307
|
+
};
|
|
308
|
+
var anchorRect = function (range) {
|
|
309
|
+
var rects = range.getClientRects();
|
|
310
|
+
return rects.length ? rects[rects.length - 1] : range.getBoundingClientRect();
|
|
311
|
+
};
|
|
312
|
+
var place = function (el, rect) {
|
|
313
|
+
el.style.top = (rect.bottom + window.scrollY + 6) + 'px';
|
|
314
|
+
el.style.left = Math.max(8, Math.min(rect.left + window.scrollX, window.innerWidth - el.offsetWidth - 8)) + 'px';
|
|
315
|
+
};
|
|
316
|
+
var update = function () {
|
|
317
|
+
if (pop) return;
|
|
318
|
+
var s = selection();
|
|
319
|
+
if (!s) { btns.hidden = true; return; }
|
|
320
|
+
btns.hidden = false;
|
|
321
|
+
place(btns, anchorRect(s.range));
|
|
322
|
+
};
|
|
323
|
+
document.addEventListener('selectionchange', function () { setTimeout(update, 0); });
|
|
324
|
+
|
|
325
|
+
var pageFile = function () {
|
|
326
|
+
var file = docsifyVm && docsifyVm.route && docsifyVm.route.file;
|
|
327
|
+
if (typeof file === 'string' && file) {
|
|
328
|
+
try { file = decodeURIComponent(file); } catch (e) {}
|
|
329
|
+
file = file.replace(/^https?:\\/\\/[^/]+/, '').replace(/^\\/+/, '');
|
|
330
|
+
if (file) return file;
|
|
331
|
+
}
|
|
332
|
+
var p = location.hash.replace(/^#/, '').split('?')[0];
|
|
333
|
+
try { p = decodeURIComponent(p); } catch (e) {}
|
|
334
|
+
p = p.replace(/^\\/+/, '').replace(/\\/+$/, '');
|
|
335
|
+
if (!p) return (window.$docsify && window.$docsify.homepage) || 'README.md';
|
|
336
|
+
return /\\.md$/i.test(p) ? p : p + '.md';
|
|
337
|
+
};
|
|
338
|
+
var send = function (body, method, url) {
|
|
339
|
+
return fetch(url || ENDPOINT, {
|
|
340
|
+
method: method || 'POST',
|
|
341
|
+
headers: { 'Content-Type': 'application/json' },
|
|
342
|
+
body: JSON.stringify(body)
|
|
343
|
+
}).then(function (res) {
|
|
344
|
+
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
345
|
+
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
|
|
346
|
+
return data;
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
var failure = function (what) {
|
|
351
|
+
return function (err) { toast(what + ' failed: ' + (err && err.message ? err.message : err), true); };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Feedback to the agent: "Delivered" only after the server handed the batch to
|
|
355
|
+
// "c4builder feedback"; the event may outrun the POST response, hence both maps.
|
|
356
|
+
var delivered = {}, pending = {};
|
|
357
|
+
var sendBatch = function (decision, comments, general) {
|
|
358
|
+
return send({ decision: decision, comments: comments, general: general }, 'POST', FEEDBACK).then(function (r) {
|
|
359
|
+
if (delivered[r.id]) return toast('Delivered to the agent');
|
|
360
|
+
pending[r.id] = true;
|
|
361
|
+
toast(r.queued ? 'Queued \\u2014 agent is not listening' : 'Sent to the agent');
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
window.addEventListener('c4builder:delivered', function (e) {
|
|
365
|
+
var id = e.detail && e.detail.id;
|
|
366
|
+
delivered[id] = true;
|
|
367
|
+
if (pending[id]) { delete pending[id]; toast('Delivered to the agent'); }
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Review draft lives in localStorage per project: it survives livereload and a closed tab;
|
|
371
|
+
// without storage it lives until the page reloads.
|
|
372
|
+
var memDraft = [];
|
|
373
|
+
var draft = function () {
|
|
374
|
+
try {
|
|
375
|
+
var v = JSON.parse(localStorage.getItem(DRAFT_KEY) || '[]');
|
|
376
|
+
return Array.isArray(v) ? v : [];
|
|
377
|
+
} catch (e) { return memDraft; }
|
|
378
|
+
};
|
|
379
|
+
var saveDraft = function (items) {
|
|
380
|
+
memDraft = items;
|
|
381
|
+
try {
|
|
382
|
+
if (items.length) localStorage.setItem(DRAFT_KEY, JSON.stringify(items));
|
|
383
|
+
else localStorage.removeItem(DRAFT_KEY);
|
|
384
|
+
} catch (e) {}
|
|
385
|
+
renderPanel();
|
|
386
|
+
highlightDraft();
|
|
387
|
+
};
|
|
388
|
+
var highlightDraft = function () {
|
|
389
|
+
var root = content();
|
|
390
|
+
if (!root) return;
|
|
391
|
+
var marks = root.querySelectorAll('mark.c4b-dhl');
|
|
392
|
+
for (var i = 0; i < marks.length; i++) {
|
|
393
|
+
var m = marks[i], parent = m.parentNode;
|
|
394
|
+
while (m.firstChild) parent.insertBefore(m.firstChild, m);
|
|
395
|
+
parent.removeChild(m);
|
|
396
|
+
parent.normalize();
|
|
397
|
+
}
|
|
398
|
+
var page = pageFile();
|
|
399
|
+
var h = findHeading(root, HEADING);
|
|
400
|
+
var skip = h ? [h].concat(sectionOf(h)) : [];
|
|
401
|
+
draft().forEach(function (it) {
|
|
402
|
+
if (it.page === page) locate(root, { quote: collapse(it.quote), section: it.section }, skip, 'c4b-dhl', it.comment);
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
var panel = document.createElement('div');
|
|
407
|
+
panel.className = 'c4b-review';
|
|
408
|
+
var expanded = false, busy = false;
|
|
409
|
+
var count = button('', function () { expanded = !expanded; renderPanel(); }, 'c4b-link');
|
|
410
|
+
var listen = document.createElement('small');
|
|
411
|
+
listen.className = 'c4b-listen';
|
|
412
|
+
var list = document.createElement('ul');
|
|
413
|
+
var general = document.createElement('textarea');
|
|
414
|
+
general.placeholder = 'Overall comment (optional, Ctrl+Enter to submit, Esc to hide)';
|
|
415
|
+
general.hidden = true;
|
|
416
|
+
var finish = function (decision) {
|
|
417
|
+
var items = draft(), ids = items.map(function (it) { return it.id; });
|
|
418
|
+
busy = true;
|
|
419
|
+
renderPanel();
|
|
420
|
+
sendBatch(decision, items, general.hidden ? '' : general.value.trim()).then(function () {
|
|
421
|
+
general.value = '';
|
|
422
|
+
general.hidden = true;
|
|
423
|
+
expanded = false;
|
|
424
|
+
saveDraft(draft().filter(function (it) { return ids.indexOf(it.id) < 0; }));
|
|
425
|
+
}, failure('Review')).then(function () { busy = false; renderPanel(); });
|
|
426
|
+
};
|
|
427
|
+
var discard = button('Discard', function () {
|
|
428
|
+
var n = draft().length;
|
|
429
|
+
if (n && !window.confirm('Discard ' + n + ' pending comment' + (n === 1 ? '' : 's') + '?')) return;
|
|
430
|
+
general.value = '';
|
|
431
|
+
general.hidden = true;
|
|
432
|
+
saveDraft([]);
|
|
433
|
+
});
|
|
434
|
+
var approve = button('Approve', function () { finish('approved'); });
|
|
435
|
+
approve.title = 'Tell the agent there is nothing more to change (pending comments go along)';
|
|
436
|
+
var submit = button('Submit review', function () {
|
|
437
|
+
if (!general.hidden) return finish('submitted');
|
|
438
|
+
general.hidden = false;
|
|
439
|
+
general.focus();
|
|
440
|
+
}, 'c4b-primary');
|
|
441
|
+
general.addEventListener('keydown', function (e) {
|
|
442
|
+
if (e.key === 'Escape') { e.preventDefault(); general.hidden = true; }
|
|
443
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); submit.click(); }
|
|
444
|
+
});
|
|
445
|
+
var head = document.createElement('div');
|
|
446
|
+
head.className = 'c4b-row';
|
|
447
|
+
[count, listen, discard, approve, submit].forEach(function (el) { head.appendChild(el); });
|
|
448
|
+
panel.appendChild(head);
|
|
449
|
+
panel.appendChild(list);
|
|
450
|
+
panel.appendChild(general);
|
|
451
|
+
document.body.appendChild(panel);
|
|
452
|
+
var renderPanel = function () {
|
|
453
|
+
panel.hidden = !AGENT.on;
|
|
454
|
+
var items = draft(), pages = {};
|
|
455
|
+
items.forEach(function (it) { pages[it.page] = true; });
|
|
456
|
+
var n = Object.keys(pages).length;
|
|
457
|
+
count.textContent = items.length
|
|
458
|
+
? 'Review: ' + items.length + ' pending (' + n + (n === 1 ? ' page)' : ' pages)')
|
|
459
|
+
: 'Review: nothing pending';
|
|
460
|
+
count.disabled = !items.length;
|
|
461
|
+
list.textContent = '';
|
|
462
|
+
items.forEach(function (it) {
|
|
463
|
+
var li = document.createElement('li');
|
|
464
|
+
var q = collapse(it.quote);
|
|
465
|
+
li.textContent = it.page + ' \\u2014 \\u00ab' + (q.length > 60 ? q.slice(0, 59) + '\\u2026' : q) + '\\u00bb: ' + it.comment;
|
|
466
|
+
var del = button('\\u00d7', function () {
|
|
467
|
+
saveDraft(draft().filter(function (x) { return x.id !== it.id; }));
|
|
468
|
+
}, 'c4b-del');
|
|
469
|
+
del.title = 'Remove from the review';
|
|
470
|
+
li.appendChild(del);
|
|
471
|
+
list.appendChild(li);
|
|
472
|
+
});
|
|
473
|
+
list.hidden = !expanded || !items.length;
|
|
474
|
+
discard.disabled = busy;
|
|
475
|
+
approve.disabled = busy;
|
|
476
|
+
submit.disabled = busy || !items.length;
|
|
477
|
+
};
|
|
478
|
+
var renderAgent = function () {
|
|
479
|
+
fbBtn.hidden = !AGENT.on;
|
|
480
|
+
renderPanel();
|
|
481
|
+
var text = 'agent: ' + (AGENT.listening ? 'listening \\u25cf' : 'not listening \\u25cb');
|
|
482
|
+
var els = document.querySelectorAll('.c4b-listen');
|
|
483
|
+
for (var i = 0; i < els.length; i++) { els[i].textContent = text; els[i].title = text; }
|
|
484
|
+
};
|
|
485
|
+
window.addEventListener('c4builder:agent', function (e) {
|
|
486
|
+
var d = e.detail || {};
|
|
487
|
+
AGENT.on = !!d.on;
|
|
488
|
+
AGENT.listening = !!d.listening;
|
|
489
|
+
renderAgent();
|
|
490
|
+
});
|
|
491
|
+
window.addEventListener('storage', function (e) {
|
|
492
|
+
if (e.key === DRAFT_KEY) { renderPanel(); highlightDraft(); }
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
var close = function () {
|
|
496
|
+
if (pop) { pop.remove(); pop = null; }
|
|
497
|
+
btns.hidden = true;
|
|
498
|
+
};
|
|
499
|
+
var open = function (data, rect, feedback) {
|
|
500
|
+
close();
|
|
501
|
+
pop = document.createElement('div');
|
|
502
|
+
pop.className = 'c4b-cpop';
|
|
503
|
+
var quote = document.createElement('blockquote');
|
|
504
|
+
quote.textContent = data.quote;
|
|
505
|
+
var ta = document.createElement('textarea');
|
|
506
|
+
ta.placeholder = (feedback ? 'What should the agent change?' : 'Your question') + ' (Ctrl+Enter to send, Esc to cancel)';
|
|
507
|
+
var row = document.createElement('div');
|
|
508
|
+
row.className = 'c4b-row';
|
|
509
|
+
var hint = document.createElement('small');
|
|
510
|
+
if (feedback) hint.className = 'c4b-listen';
|
|
511
|
+
else hint.textContent = hint.title = 'To: ' + HEADING + (data.section ? ' / ' + data.section : '');
|
|
512
|
+
var actions = document.createElement('span');
|
|
513
|
+
var buttons = [button('Cancel', close)];
|
|
514
|
+
var run = function (action) {
|
|
515
|
+
return function () {
|
|
516
|
+
var comment = ta.value.trim();
|
|
517
|
+
if (!comment) { ta.focus(); return; }
|
|
518
|
+
buttons.forEach(function (b) { b.disabled = true; });
|
|
519
|
+
action(comment).then(close, function (err) {
|
|
520
|
+
buttons.forEach(function (b) { b.disabled = false; });
|
|
521
|
+
failure(feedback ? 'Feedback' : 'Question')(err);
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
var item = function (comment) {
|
|
526
|
+
return { page: pageFile(), quote: data.quote, section: data.section, comment: comment };
|
|
527
|
+
};
|
|
528
|
+
var primary;
|
|
529
|
+
if (feedback) {
|
|
530
|
+
var add = button('Add to review', run(function (c) {
|
|
531
|
+
var it = item(c);
|
|
532
|
+
it.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
533
|
+
saveDraft(draft().concat([it]));
|
|
534
|
+
return Promise.resolve();
|
|
535
|
+
}));
|
|
536
|
+
var now = button('Send now', run(function (c) { return sendBatch('submitted', [item(c)], ''); }));
|
|
537
|
+
buttons.push(add, now);
|
|
538
|
+
// As in a GitLab merge request: once a review is started, comments go into it.
|
|
539
|
+
primary = draft().length ? add : now;
|
|
540
|
+
} else {
|
|
541
|
+
primary = button('Add question', run(function (c) {
|
|
542
|
+
return send(item(c)).then(function (r) {
|
|
543
|
+
var msg = 'Added to ' + r.file + (r.number ? ' as #' + r.number : '');
|
|
544
|
+
try { sessionStorage.setItem(TOAST_KEY, msg); } catch (e) {}
|
|
545
|
+
toast(msg);
|
|
546
|
+
});
|
|
547
|
+
}));
|
|
548
|
+
buttons.push(primary);
|
|
549
|
+
}
|
|
550
|
+
primary.className = 'c4b-primary';
|
|
551
|
+
buttons.forEach(function (b) {
|
|
552
|
+
actions.appendChild(b);
|
|
553
|
+
actions.appendChild(document.createTextNode(' '));
|
|
554
|
+
});
|
|
555
|
+
row.appendChild(hint);
|
|
556
|
+
row.appendChild(actions);
|
|
557
|
+
pop.appendChild(quote);
|
|
558
|
+
pop.appendChild(ta);
|
|
559
|
+
pop.appendChild(row);
|
|
560
|
+
document.body.appendChild(pop);
|
|
561
|
+
renderAgent();
|
|
562
|
+
place(pop, rect);
|
|
563
|
+
ta.focus();
|
|
564
|
+
ta.addEventListener('keydown', function (e) {
|
|
565
|
+
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
566
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); primary.click(); }
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
var openFor = function (feedback) {
|
|
571
|
+
return function () {
|
|
572
|
+
var s = selection();
|
|
573
|
+
if (s) open({ quote: s.text, section: headingBefore(s.range) }, anchorRect(s.range), feedback);
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
btns.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
577
|
+
fbBtn.addEventListener('click', openFor(true));
|
|
578
|
+
qBtn.addEventListener('click', openFor(false));
|
|
579
|
+
document.addEventListener('mousedown', function (e) {
|
|
580
|
+
if (pop && !pop.contains(e.target) && !btns.contains(e.target)) close();
|
|
581
|
+
});
|
|
582
|
+
window.addEventListener('hashchange', close);
|
|
583
|
+
renderAgent();
|
|
584
|
+
})();
|
|
585
|
+
</script>`;
|