|
@@ -57,7 +57,11 @@
|
|
|
if (window.initQuoteComments) {
|
|
if (window.initQuoteComments) {
|
|
|
window.initQuoteComments({
|
|
window.initQuoteComments({
|
|
|
matches: function (node) {
|
|
matches: function (node) {
|
|
|
- return node.closest && node.closest('.goal-body') != null;
|
|
|
|
|
|
|
+ // The pending plan body feeds the REMARKS
|
|
|
|
|
+ // popover instead (below) — everything else
|
|
|
|
|
+ // long-form on the goal quote-comments.
|
|
|
|
|
+ return node.closest && node.closest('.goal-body') != null
|
|
|
|
|
+ && node.closest('[data-plan-body]') == null;
|
|
|
},
|
|
},
|
|
|
action: function () {
|
|
action: function () {
|
|
|
var goal = statum.read('goal');
|
|
var goal = statum.read('goal');
|
|
@@ -65,6 +69,202 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Plan 14 B2: the plan-review remarks queue — the
|
|
|
|
|
+ // task-page collect-remarks pattern (the feedbackByBlock
|
|
|
|
|
+ // maps) ported to the goal's plan review. The queue lives
|
|
|
|
|
+ // in these maps (keyed by goal block id) so statum
|
|
|
|
|
+ // re-renders never lose it; the DOM list is rebuilt after
|
|
|
|
|
+ // every render cycle.
|
|
|
|
|
+ var remarksByBlock = {};
|
|
|
|
|
+ var rejectOpenByBlock = {};
|
|
|
|
|
+ var remarkPopover = null;
|
|
|
|
|
+
|
|
|
|
|
+ statum.afterEvaluate(syncPlanRemarks);
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('click', function (event) {
|
|
|
|
|
+ if (!event.target.closest) return;
|
|
|
|
|
+ var accept = event.target.closest('[data-plan-accept]');
|
|
|
|
|
+ if (accept) {
|
|
|
|
|
+ // Accept ignores/clears the queued remarks.
|
|
|
|
|
+ var accepted = accept.closest('[data-plan-review]');
|
|
|
|
|
+ if (accepted) {
|
|
|
|
|
+ var acceptId = accepted.getAttribute('data-block-id');
|
|
|
|
|
+ remarksByBlock[acceptId] = [];
|
|
|
|
|
+ rejectOpenByBlock[acceptId] = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var toggle = event.target.closest('[data-plan-toggle]');
|
|
|
|
|
+ if (toggle) {
|
|
|
|
|
+ hideRemarkPopover();
|
|
|
|
|
+ var review = toggle.closest('[data-plan-review]');
|
|
|
|
|
+ var id = review && review.getAttribute('data-block-id');
|
|
|
|
|
+ if (id) rejectOpenByBlock[id] = !rejectOpenByBlock[id];
|
|
|
|
|
+ syncPlanRemarks();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var submit = event.target.closest('[data-plan-submit]');
|
|
|
|
|
+ if (submit) {
|
|
|
|
|
+ submitPlanRemarks(submit.closest('[data-plan-review]'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var remove = event.target.closest('[data-plan-remove]');
|
|
|
|
|
+ if (remove) {
|
|
|
|
|
+ var entry = remove.closest('[data-plan-remark-id]');
|
|
|
|
|
+ var blockId = remove.closest('[data-plan-review]').getAttribute('data-block-id');
|
|
|
|
|
+ var list = remarksByBlock[blockId] || [];
|
|
|
|
|
+ var index = parseInt(entry.getAttribute('data-plan-remark-id'), 10);
|
|
|
|
|
+ if (!isNaN(index) && list[index]) list.splice(index, 1);
|
|
|
|
|
+ remarksByBlock[blockId] = list;
|
|
|
|
|
+ syncPlanRemarks();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('mouseup', function (event) {
|
|
|
|
|
+ if (!event.target.closest) return;
|
|
|
|
|
+ if (remarkPopover && remarkPopover.contains(event.target)) return;
|
|
|
|
|
+ var body = event.target.closest('[data-plan-body]');
|
|
|
|
|
+ if (!body) {
|
|
|
|
|
+ hideRemarkPopover();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var review = body.closest('[data-plan-review]');
|
|
|
|
|
+ if (!review || review.getAttribute('data-plan-open') !== '1') return;
|
|
|
|
|
+ var selection = window.getSelection();
|
|
|
|
|
+ var text = selection ? String(selection.toString()).trim() : '';
|
|
|
|
|
+ if (text.length === 0) {
|
|
|
|
|
+ hideRemarkPopover();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ showRemarkPopover(event.pageX, event.pageY, text, review);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('mousedown', function (event) {
|
|
|
|
|
+ if (remarkPopover && event.target.closest
|
|
|
|
|
+ && event.target.closest('.plan-popover')) return;
|
|
|
|
|
+ hideRemarkPopover();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ function showRemarkPopover(x, y, quote, review) {
|
|
|
|
|
+ hideRemarkPopover();
|
|
|
|
|
+ remarkPopover = document.createElement('div');
|
|
|
|
|
+ remarkPopover.className = 'plan-popover';
|
|
|
|
|
+ remarkPopover.style.left = Math.min(x, window.innerWidth - 140) + 'px';
|
|
|
|
|
+ remarkPopover.style.top = y + 'px';
|
|
|
|
|
+
|
|
|
|
|
+ var quoted = document.createElement('blockquote');
|
|
|
|
|
+ quoted.textContent = quote;
|
|
|
|
|
+ remarkPopover.appendChild(quoted);
|
|
|
|
|
+
|
|
|
|
|
+ var body = document.createElement('textarea');
|
|
|
|
|
+ body.placeholder = 'Your remark on the selected plan text';
|
|
|
|
|
+ remarkPopover.appendChild(body);
|
|
|
|
|
+
|
|
|
|
|
+ var actions = document.createElement('div');
|
|
|
|
|
+ actions.className = 'actions';
|
|
|
|
|
+ var add = document.createElement('button');
|
|
|
|
|
+ add.type = 'button';
|
|
|
|
|
+ add.className = 'primary';
|
|
|
|
|
+ add.textContent = 'Add remark';
|
|
|
|
|
+ add.addEventListener('click', function () {
|
|
|
|
|
+ if (body.value.trim().length === 0) return;
|
|
|
|
|
+ var id = review.getAttribute('data-block-id');
|
|
|
|
|
+ var list = remarksByBlock[id] || [];
|
|
|
|
|
+ list.push({ quote: quote, body: body.value.trim() });
|
|
|
|
|
+ remarksByBlock[id] = list;
|
|
|
|
|
+ hideRemarkPopover();
|
|
|
|
|
+ syncPlanRemarks();
|
|
|
|
|
+ });
|
|
|
|
|
+ var cancel = document.createElement('button');
|
|
|
|
|
+ cancel.type = 'button';
|
|
|
|
|
+ cancel.textContent = 'Cancel';
|
|
|
|
|
+ cancel.addEventListener('click', hideRemarkPopover);
|
|
|
|
|
+ actions.appendChild(add);
|
|
|
|
|
+ actions.appendChild(cancel);
|
|
|
|
|
+ remarkPopover.appendChild(actions);
|
|
|
|
|
+
|
|
|
|
|
+ document.body.appendChild(remarkPopover);
|
|
|
|
|
+ body.focus();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function hideRemarkPopover() {
|
|
|
|
|
+ if (remarkPopover) {
|
|
|
|
|
+ remarkPopover.remove();
|
|
|
|
|
+ remarkPopover = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function submitPlanRemarks(review) {
|
|
|
|
|
+ if (!review) return;
|
|
|
|
|
+ var id = review.getAttribute('data-block-id');
|
|
|
|
|
+ var list = remarksByBlock[id] || [];
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ if (window.showToast) window.showToast('Highlight some plan text and add a remark first', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var goal = statum.read('goal');
|
|
|
|
|
+ var block = null;
|
|
|
|
|
+ if (goal && goal.plan_pending_block
|
|
|
|
|
+ && String(goal.plan_pending_block.id) === String(id)) {
|
|
|
|
|
+ block = goal.plan_pending_block;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!block || !block.answer_action) return;
|
|
|
|
|
+ // Every queued remark rides the rejection NUMBERED, in
|
|
|
|
|
+ // queue order, each with its selection quote prefixed
|
|
|
|
|
+ // like a comment; the wrapped answer IS the resume
|
|
|
|
|
+ // notice the agent reads.
|
|
|
|
|
+ var answer = 'Plan feedback received:';
|
|
|
|
|
+ list.forEach(function (item, index) {
|
|
|
|
|
+ answer += '\n\n';
|
|
|
|
|
+ if (item.quote) {
|
|
|
|
|
+ answer += '> ' + item.quote.replace(/\n/g, '\n> ') + '\n\n';
|
|
|
|
|
+ }
|
|
|
|
|
+ answer += (index + 1) + '. ' + item.body;
|
|
|
|
|
+ });
|
|
|
|
|
+ var extra = review.querySelector('[data-plan-feedback] textarea[name="feedback"]');
|
|
|
|
|
+ if (extra && extra.value.trim().length > 0) {
|
|
|
|
|
+ answer += '\n\n' + extra.value.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ remarksByBlock[id] = [];
|
|
|
|
|
+ rejectOpenByBlock[id] = false;
|
|
|
|
|
+ statum.act(block.answer_action, { answer: answer }).catch(function () { });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function syncPlanRemarks() {
|
|
|
|
|
+ document.querySelectorAll('[data-plan-review]').forEach(function (review) {
|
|
|
|
|
+ var id = review.getAttribute('data-block-id');
|
|
|
|
|
+ var open = rejectOpenByBlock[id] ? '1' : '0';
|
|
|
|
|
+ review.setAttribute('data-plan-open', open);
|
|
|
|
|
+ var feedback = review.querySelector('[data-plan-feedback]');
|
|
|
|
|
+ if (feedback) {
|
|
|
|
|
+ if (rejectOpenByBlock[id]) feedback.removeAttribute('hidden');
|
|
|
|
|
+ else feedback.setAttribute('hidden', '');
|
|
|
|
|
+ }
|
|
|
|
|
+ var list = review.querySelector('[data-plan-remarks]');
|
|
|
|
|
+ if (!list) return;
|
|
|
|
|
+ while (list.firstChild) list.removeChild(list.firstChild);
|
|
|
|
|
+ (remarksByBlock[id] || []).forEach(function (item, index) {
|
|
|
|
|
+ var entry = document.createElement('li');
|
|
|
|
|
+ entry.setAttribute('data-plan-remark-id', String(index));
|
|
|
|
|
+ if (item.quote) {
|
|
|
|
|
+ var quoted = document.createElement('blockquote');
|
|
|
|
|
+ quoted.textContent = item.quote;
|
|
|
|
|
+ entry.appendChild(quoted);
|
|
|
|
|
+ }
|
|
|
|
|
+ var body = document.createElement('div');
|
|
|
|
|
+ body.textContent = item.body;
|
|
|
|
|
+ entry.appendChild(body);
|
|
|
|
|
+ var remove = document.createElement('button');
|
|
|
|
|
+ remove.type = 'button';
|
|
|
|
|
+ remove.className = 'inline';
|
|
|
|
|
+ remove.setAttribute('data-plan-remove', '');
|
|
|
|
|
+ remove.textContent = 'Remove';
|
|
|
|
|
+ entry.appendChild(remove);
|
|
|
|
|
+ list.appendChild(entry);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
})();
|
|
})();
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
@@ -204,23 +404,27 @@
|
|
|
<p stm-if="goal.plan.length == 0 && goal.plan_pending_block == null"
|
|
<p stm-if="goal.plan.length == 0 && goal.plan_pending_block == null"
|
|
|
class="muted">No approved plan yet.</p>
|
|
class="muted">No approved plan yet.</p>
|
|
|
|
|
|
|
|
- <div stm-if="goal.plan_pending_block != null && goal.plan_pending_block != false" class="plan-review">
|
|
|
|
|
|
|
+ <div stm-if="goal.plan_pending_block != null && goal.plan_pending_block != false" class="plan-review" data-plan-review
|
|
|
|
|
+ stm-attribute.data-block-id="goal.plan_pending_block.id">
|
|
|
<h4>Plan submitted for review</h4>
|
|
<h4>Plan submitted for review</h4>
|
|
|
- <div class="md rfc-body goal-body" stm-markdown="goal.plan_pending_block.body">revision</div>
|
|
|
|
|
- <form stm-action="goal.plan_pending_block.answer_action" class="inline">
|
|
|
|
|
- <input type="hidden" name="answer"
|
|
|
|
|
- value="Plan approved. Proceed with implementation.">
|
|
|
|
|
- <button type="submit" class="approve">Approve plan</button>
|
|
|
|
|
- </form>
|
|
|
|
|
- <details>
|
|
|
|
|
- <summary>Reject with feedback</summary>
|
|
|
|
|
- <form stm-action="goal.plan_pending_block.answer_action">
|
|
|
|
|
- <div class="form-row">
|
|
|
|
|
- <textarea name="answer" rows="4" required
|
|
|
|
|
- placeholder="What must change before approval?"></textarea></div>
|
|
|
|
|
- <button type="submit" class="reject">Send feedback</button>
|
|
|
|
|
|
|
+ <div class="md rfc-body goal-body" data-plan-body
|
|
|
|
|
+ stm-markdown="goal.plan_pending_block.body">revision</div>
|
|
|
|
|
+ <div class="actions">
|
|
|
|
|
+ <form stm-action="goal.plan_pending_block.answer_action" class="inline">
|
|
|
|
|
+ <input type="hidden" name="answer"
|
|
|
|
|
+ value="Plan approved. Proceed with implementation.">
|
|
|
|
|
+ <button type="submit" class="approve" data-plan-accept>Approve plan</button>
|
|
|
</form>
|
|
</form>
|
|
|
- </details>
|
|
|
|
|
|
|
+ <button type="button" data-plan-toggle>Reject with feedback</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div data-plan-feedback hidden>
|
|
|
|
|
+ <p class="muted">Highlight plan text to attach a remark to it — the queue
|
|
|
|
|
+ rides the rejection numbered, then clears. Approving ignores the queue.</p>
|
|
|
|
|
+ <ul class="plan-comments" data-plan-remarks></ul>
|
|
|
|
|
+ <textarea name="feedback" rows="2"
|
|
|
|
|
+ placeholder="Additional feedback (optional)"></textarea>
|
|
|
|
|
+ <button type="button" class="primary" data-plan-submit>Submit feedback</button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<details class="history-expander" stm-if="goal.plan_revisions.length > 0">
|
|
<details class="history-expander" stm-if="goal.plan_revisions.length > 0">
|