|
@@ -3,35 +3,207 @@
|
|
|
<head>
|
|
<head>
|
|
|
<pstm-uri>/tasks/{task_id}</pstm-uri>
|
|
<pstm-uri>/tasks/{task_id}</pstm-uri>
|
|
|
<pstm-template>main</pstm-template>
|
|
<pstm-template>main</pstm-template>
|
|
|
- <script>
|
|
|
|
|
- // Live updates: the page subscribes to the global pulse (see the
|
|
|
|
|
- // entrypoint); whenever task or saddle state changes anywhere in
|
|
|
|
|
- // the app, the sealed refresh action rehydrates the task slot —
|
|
|
|
|
- // no fixed-interval polling. RFC bodies get the shared
|
|
|
|
|
- // text-highlight -> comment popover (see main.html).
|
|
|
|
|
- if (window.statum) {
|
|
|
|
|
- (function () {
|
|
|
|
|
- 'use strict';
|
|
|
|
|
- statum.addStateListener('pulse', function () {
|
|
|
|
|
- var task = statum.read('task');
|
|
|
|
|
- if (task && task.refresh) {
|
|
|
|
|
- statum.act(task.refresh).catch(function () { });
|
|
|
|
|
|
|
+<script>
|
|
|
|
|
+ // Live updates: the page subscribes to the global pulse (see the
|
|
|
|
|
+ // entrypoint); whenever task or saddle state changes anywhere in
|
|
|
|
|
+ // the app, the sealed refresh action rehydrates the task slot —
|
|
|
|
|
+ // no fixed-interval polling. RFC bodies get the shared
|
|
|
|
|
+ // text-highlight -> comment popover (see main.html). Design-task
|
|
|
|
|
+ // plan reviews collect inline comments on the plan and submit them
|
|
|
|
|
+ // as the block answer.
|
|
|
|
|
+ if (window.statum) {
|
|
|
|
|
+ (function () {
|
|
|
|
|
+ 'use strict';
|
|
|
|
|
+ statum.addStateListener('pulse', function () {
|
|
|
|
|
+ var task = statum.read('task');
|
|
|
|
|
+ if (task && task.refresh) {
|
|
|
|
|
+ statum.act(task.refresh).catch(function () { });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (window.initQuoteComments) {
|
|
|
|
|
+ window.initQuoteComments({
|
|
|
|
|
+ matches: function (node) {
|
|
|
|
|
+ return node.closest && node.closest('.rfc-body') != null
|
|
|
|
|
+ && node.closest('[data-plan-body]') == null;
|
|
|
|
|
+ },
|
|
|
|
|
+ action: function () {
|
|
|
|
|
+ var task = statum.read('task');
|
|
|
|
|
+ return task && task.comment;
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
- if (window.initQuoteComments) {
|
|
|
|
|
- window.initQuoteComments({
|
|
|
|
|
- matches: function (node) {
|
|
|
|
|
- return node.closest && node.closest('.rfc-body') != null;
|
|
|
|
|
- },
|
|
|
|
|
- action: function () {
|
|
|
|
|
- var task = statum.read('task');
|
|
|
|
|
- return task && task.comment;
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Plan-review feedback: collected comments live in these maps
|
|
|
|
|
+ // (keyed by block id) so statum re-renders never lose them;
|
|
|
|
|
+ // the DOM list is rebuilt after every render cycle.
|
|
|
|
|
+ var feedbackByBlock = {};
|
|
|
|
|
+ var modeByBlock = {};
|
|
|
|
|
+ var planPopover = null;
|
|
|
|
|
+
|
|
|
|
|
+ statum.afterEvaluate(syncPlanFeedback);
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('click', function (event) {
|
|
|
|
|
+ if (!event.target.closest) return;
|
|
|
|
|
+ var toggle = event.target.closest('[data-plan-toggle]');
|
|
|
|
|
+ if (toggle) {
|
|
|
|
|
+ hidePlanPopover();
|
|
|
|
|
+ var review = toggle.closest('[data-plan-review]');
|
|
|
|
|
+ var id = review && review.getAttribute('data-block-id');
|
|
|
|
|
+ if (id) modeByBlock[id] = !modeByBlock[id];
|
|
|
|
|
+ syncPlanFeedback();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var submit = event.target.closest('[data-plan-submit]');
|
|
|
|
|
+ if (submit) {
|
|
|
|
|
+ submitPlanFeedback(submit.closest('[data-plan-review]'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var remove = event.target.closest('[data-plan-remove]');
|
|
|
|
|
+ if (remove) {
|
|
|
|
|
+ var entry = remove.closest('[data-plan-comment-id]');
|
|
|
|
|
+ var blockId = remove.closest('[data-plan-review]').getAttribute('data-block-id');
|
|
|
|
|
+ var list = feedbackByBlock[blockId] || [];
|
|
|
|
|
+ var index = parseInt(entry.getAttribute('data-plan-comment-id'), 10);
|
|
|
|
|
+ if (!isNaN(index) && list[index]) list.splice(index, 1);
|
|
|
|
|
+ feedbackByBlock[blockId] = list;
|
|
|
|
|
+ syncPlanFeedback();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('mouseup', function (event) {
|
|
|
|
|
+ if (!event.target.closest) return;
|
|
|
|
|
+ if (planPopover && planPopover.contains(event.target)) return;
|
|
|
|
|
+ var body = event.target.closest('[data-plan-body]');
|
|
|
|
|
+ if (!body) {
|
|
|
|
|
+ hidePlanPopover();
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ hidePlanPopover();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ showPlanPopover(event.pageX, event.pageY, text, review);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('mousedown', function (event) {
|
|
|
|
|
+ if (planPopover && event.target.closest && event.target.closest('.plan-popover')) return;
|
|
|
|
|
+ hidePlanPopover();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ function showPlanPopover(x, y, quote, review) {
|
|
|
|
|
+ hidePlanPopover();
|
|
|
|
|
+ planPopover = document.createElement('div');
|
|
|
|
|
+ planPopover.className = 'plan-popover';
|
|
|
|
|
+ planPopover.style.left = Math.min(x, window.innerWidth - 140) + 'px';
|
|
|
|
|
+ planPopover.style.top = y + 'px';
|
|
|
|
|
+
|
|
|
|
|
+ var quoted = document.createElement('blockquote');
|
|
|
|
|
+ quoted.textContent = quote;
|
|
|
|
|
+ planPopover.appendChild(quoted);
|
|
|
|
|
+
|
|
|
|
|
+ var body = document.createElement('textarea');
|
|
|
|
|
+ body.placeholder = 'Your comment on the selected plan text';
|
|
|
|
|
+ planPopover.appendChild(body);
|
|
|
|
|
+
|
|
|
|
|
+ var actions = document.createElement('div');
|
|
|
|
|
+ actions.className = 'actions';
|
|
|
|
|
+ var add = document.createElement('button');
|
|
|
|
|
+ add.type = 'button';
|
|
|
|
|
+ add.className = 'primary';
|
|
|
|
|
+ add.textContent = 'Add comment';
|
|
|
|
|
+ add.addEventListener('click', function () {
|
|
|
|
|
+ if (body.value.trim().length === 0) return;
|
|
|
|
|
+ var id = review.getAttribute('data-block-id');
|
|
|
|
|
+ var list = feedbackByBlock[id] || [];
|
|
|
|
|
+ list.push({ quote: quote, body: body.value.trim() });
|
|
|
|
|
+ feedbackByBlock[id] = list;
|
|
|
|
|
+ hidePlanPopover();
|
|
|
|
|
+ syncPlanFeedback();
|
|
|
|
|
+ });
|
|
|
|
|
+ var cancel = document.createElement('button');
|
|
|
|
|
+ cancel.type = 'button';
|
|
|
|
|
+ cancel.textContent = 'Cancel';
|
|
|
|
|
+ cancel.addEventListener('click', hidePlanPopover);
|
|
|
|
|
+ actions.appendChild(add);
|
|
|
|
|
+ actions.appendChild(cancel);
|
|
|
|
|
+ planPopover.appendChild(actions);
|
|
|
|
|
+
|
|
|
|
|
+ document.body.appendChild(planPopover);
|
|
|
|
|
+ body.focus();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function hidePlanPopover() {
|
|
|
|
|
+ if (planPopover) {
|
|
|
|
|
+ planPopover.remove();
|
|
|
|
|
+ planPopover = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function submitPlanFeedback(review) {
|
|
|
|
|
+ if (!review) return;
|
|
|
|
|
+ var id = review.getAttribute('data-block-id');
|
|
|
|
|
+ var list = feedbackByBlock[id] || [];
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ if (window.showToast) window.showToast('Highlight some plan text and add a comment first', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var task = statum.read('task');
|
|
|
|
|
+ var block = null;
|
|
|
|
|
+ (task && task.blocks || []).forEach(function (candidate) {
|
|
|
|
|
+ if (String(candidate.id) === String(id)) block = candidate;
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!block || !block.answer_action) return;
|
|
|
|
|
+ var answer = list.map(function (item) {
|
|
|
|
|
+ return item.quote
|
|
|
|
|
+ ? '> ' + item.quote.replace(/\n/g, '\n> ') + '\n\n' + item.body
|
|
|
|
|
+ : item.body;
|
|
|
|
|
+ }).join('\n\n');
|
|
|
|
|
+ feedbackByBlock[id] = [];
|
|
|
|
|
+ modeByBlock[id] = false;
|
|
|
|
|
+ statum.act(block.answer_action, { answer: answer }).catch(function () { });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function syncPlanFeedback() {
|
|
|
|
|
+ document.querySelectorAll('[data-plan-review]').forEach(function (review) {
|
|
|
|
|
+ var id = review.getAttribute('data-block-id');
|
|
|
|
|
+ var open = modeByBlock[id] ? '1' : '0';
|
|
|
|
|
+ review.setAttribute('data-plan-open', open);
|
|
|
|
|
+ var feedback = review.querySelector('[data-plan-feedback]');
|
|
|
|
|
+ if (feedback) {
|
|
|
|
|
+ if (modeByBlock[id]) feedback.removeAttribute('hidden');
|
|
|
|
|
+ else feedback.setAttribute('hidden', '');
|
|
|
|
|
+ }
|
|
|
|
|
+ var list = review.querySelector('[data-plan-comments]');
|
|
|
|
|
+ if (!list) return;
|
|
|
|
|
+ while (list.firstChild) list.removeChild(list.firstChild);
|
|
|
|
|
+ (feedbackByBlock[id] || []).forEach(function (item, index) {
|
|
|
|
|
+ var entry = document.createElement('li');
|
|
|
|
|
+ entry.setAttribute('data-plan-comment-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>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<section>
|
|
<section>
|
|
@@ -158,6 +330,24 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <div stm-if="!block.answered && block.is_plan_review" class="plan-review"
|
|
|
|
|
+ data-plan-review stm-attribute.data-block-id="block.id">
|
|
|
|
|
+ <div class="md rfc-body" stm-markdown="task.plan" data-plan-body>plan</div>
|
|
|
|
|
+ <div class="actions">
|
|
|
|
|
+ <form stm-action="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>
|
|
|
|
|
+ <button type="button" data-plan-toggle>Give feedback</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div data-plan-feedback hidden>
|
|
|
|
|
+ <p class="muted">Highlight plan text to attach a comment to it, then submit everything
|
|
|
|
|
+ as feedback — the agent revises the plan and asks again.</p>
|
|
|
|
|
+ <ul class="plan-comments" data-plan-comments></ul>
|
|
|
|
|
+ <button type="button" class="primary" data-plan-submit>Submit feedback</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
|
|
<p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
|
|
|
<strong stm-text="block.question">question</strong>
|
|
<strong stm-text="block.question">question</strong>
|
|
|
<span class="muted" stm-text="block.created_display">when</span></p>
|
|
<span class="muted" stm-text="block.created_display">when</span></p>
|
|
@@ -172,12 +362,13 @@
|
|
|
|
|
|
|
|
<form stm-if="!block.answered && block.block_type == 'question'" stm-action="block.answer_action">
|
|
<form stm-if="!block.answered && block.block_type == 'question'" stm-action="block.answer_action">
|
|
|
<div class="options">
|
|
<div class="options">
|
|
|
- <label stm-for-option-in="block.options" stm-key="option">
|
|
|
|
|
- <input type="radio" name="option" stm-attribute.value="option">
|
|
|
|
|
|
|
+ <label stm-for-option-in="block.options" stm-key="option" class="option-card">
|
|
|
|
|
+ <input type="radio" stm-attribute.name="'option-' + block.id"
|
|
|
|
|
+ stm-attribute.value="option">
|
|
|
<span stm-text="option">option</span>
|
|
<span stm-text="option">option</span>
|
|
|
</label>
|
|
</label>
|
|
|
<label>
|
|
<label>
|
|
|
- <input type="radio" name="option" value="__other__">
|
|
|
|
|
|
|
+ <input type="radio" stm-attribute.name="'option-' + block.id" value="__other__">
|
|
|
<span>Other:</span>
|
|
<span>Other:</span>
|
|
|
<textarea name="other" rows="2"></textarea>
|
|
<textarea name="other" rows="2"></textarea>
|
|
|
</label>
|
|
</label>
|
|
@@ -185,7 +376,7 @@
|
|
|
<button type="submit" class="primary">Answer</button>
|
|
<button type="submit" class="primary">Answer</button>
|
|
|
</form>
|
|
</form>
|
|
|
|
|
|
|
|
- <form stm-if="!block.answered && block.block_type != 'question' && block.block_type != 'review'"
|
|
|
|
|
|
|
+ <form stm-if="!block.answered && block.block_type != 'question' && block.block_type != 'review' && !block.is_plan_review"
|
|
|
stm-action="block.answer_action" class="inline">
|
|
stm-action="block.answer_action" class="inline">
|
|
|
<textarea name="answer" rows="3" placeholder="Your reply (delivered to the agent)"></textarea>
|
|
<textarea name="answer" rows="3" placeholder="Your reply (delivered to the agent)"></textarea>
|
|
|
<button type="submit" class="primary">Reply</button>
|
|
<button type="submit" class="primary">Reply</button>
|