openvibers/openvibe.surf Commits
Surf's result page: a finished task leads with its answer (#102)
Changes
@@@ -351,11 +351,25 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state.sf-task-answer .sf-desk-report-text { font-size: 1rem; }.sf-task-answer h3 { margin: 16px 0 8px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }.sf-task-wait { margin: 0; color: var(--text-secondary, #96a7c2); }--.sf-task-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }++.sf-task-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; counter-reset: sf-src; }++.sf-task-sources li { counter-increment: sf-src; min-width: 0; max-width: 100%; }.sf-task-sources a { display: inline-grid; gap: 1px; max-width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); text-decoration: none; }.sf-task-sources a:hover { border-color: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 50%, var(--border, #1f2d47)); }.sf-task-sources b { font-size: .88rem; color: var(--text-primary, #e6edf7); }.sf-task-sources span { font-size: .78rem; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }++/* Surf's result page (Opus, 11-07): the sources carry the numbers Copy uses; once the task has ended with an answer, the++ answer comes first, right under the question, and the card that followed the work sits under it. The desk is closed++ by then, so its Watch button steps aside. */++.sf-task-sources a { grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: center; }++.sf-task-sources a::before { content: counter(sf-src); grid-row: 1 / span 2; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;++ font-size: .72rem; font-weight: 800; color: var(--room-strong, var(--accent, #3b82f6)); background: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 16%, transparent); }++.sf-task-sources a b, .sf-task-sources a span { grid-column: 2; }++.sf-desk-report .sf-task-answer-acts { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }++.sf-task-answer-acts[hidden] { display: none; }++.sf-desk-page.sf-task[data-sf-desk-open="0"]:has([data-sf-desk-report]:not(:empty)) .sf-task-head { order: -2; }++.sf-desk-page.sf-task[data-sf-desk-open="0"]:has([data-sf-desk-report]:not(:empty)) .sf-task-answer { order: -1; }++.sf-task[data-sf-desk-open="0"] .sf-task-acts { display: none; }++.sf-desk-page.sf-task > .sf-task-answer { margin-top: 0; }.sf-task-steps summary { display: flex; align-items: center; gap: 6px; min-height: 44px; cursor: pointer; font-weight: 750; list-style: none; }.sf-task-steps summary::-webkit-details-marker { display: none; }.sf-task-steps summary::before { content: '›'; display: inline-block; width: 14px; font-size: 1.2em; transition: transform .2s; }@@@ -444,6 +444,10 @@li.appendChild(a); list.appendChild(li);});root.querySelector('[data-sf-desk-sources-empty]').hidden = !!list.children.length;++ var acts = root.querySelector('[data-sf-answer-acts]');++ var finalAnswer = t.result && typeof t.result.answer === 'string' ? t.result.answer : '';++ if (acts) acts.hidden = !finalAnswer;++ if (!open && !finalAnswer) root.querySelector('[data-sf-desk-report-empty]').textContent = 'Your agent finished without an answer. Every step below shows how far it got.';var answer = t.result && typeof t.result.answer === 'string' ? t.result.answer : '';root.querySelector('[data-sf-desk-report]').textContent = answer;root.querySelector('[data-sf-desk-report-empty]').hidden = !!answer;@@@ -455,6 +459,18 @@if (latest) latest.textContent = '';}}++ // Copy the answer, with its sources numbered as they show (Opus, 11-07: Surf's result page).++ var copyBtn = root.querySelector('[data-sf-answer-copy]');++ if (copyBtn) copyBtn.addEventListener('click', function () {++ var text = (root.querySelector('[data-sf-desk-report]') || {}).textContent || '';++ var links = Array.prototype.map.call(root.querySelectorAll('[data-sf-desk-sources] a'), function (a, i) { return (i + 1) + '. ' + a.href; });++ if (links.length) text += '\n\nSources:\n' + links.join('\n');++ if (!navigator.clipboard || !text.trim()) return;++ navigator.clipboard.writeText(text.trim()).then(function () {++ copyBtn.textContent = 'Copied';++ setTimeout(function () { copyBtn.textContent = 'Copy the answer'; }, 1500);++ }, function () {});++ });function refresh() {fetch(base, { credentials: 'same-origin', headers: { accept: 'application/json' } }).then(function (r) { if (!r.ok) throw new Error('Task unavailable'); return r.json(); })@@@ -339,8 +339,9 @@ function webTask({ task, actorUrl } = {}) {<p class="sf-task-more"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open in Actor</a><span aria-hidden="true">·</span><a href="/web">Ask another</a></p></section><section class="sf-card sf-desk-report sf-task-answer" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">The answer</h2>--<div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p class="sf-task-wait" data-sf-desk-report-empty${report ? ' hidden' : ''}>It appears here when your agent finishes, with every source it used.</p>++<div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p class="sf-task-wait" data-sf-desk-report-empty${report ? ' hidden' : ''}>${open ? 'It appears here when your agent finishes, with every source it used.' : 'Your agent finished without an answer. Every step below shows how far it got.'}</p><div class="sf-task-cites"><h3>Sources</h3><ul class="sf-task-sources" data-sf-desk-sources>${sources.map(sourceChip).join('')}</ul><p class="sf-task-wait" data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>None yet.</p></div>++<p class="sf-task-answer-acts" data-sf-answer-acts${report ? '' : ' hidden'}><button class="ovk-btn ovk-btn--primary" type="button" data-sf-answer-copy>Copy the answer</button><a class="ovk-btn ovk-btn--quiet" href="/web?q=${encodeURIComponent(`Following up on \u201c${String(task.task || 'my last task').slice(0, 200)}\u201d: `)}">Ask a follow-up</a></p></section><details class="sf-card sf-desk-steps sf-task-steps"${open ? ' open' : ''}><summary>Every step <span class="sf-task-count" data-sf-desk-count></span></summary><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></details><p class="sf-fine sf-task-rule">It never signs in or pays for you: at a login, a captcha or a checkout it stops and hands over.</p></div>`;@@@ -152,6 +152,9 @@ const ORIGIN = 'https://openvibe.surf';assert.match(r.text, /openvibe\.bot\/machines/);assert.match(r.text, /openvibe\.actor\/tasks/);assert.ok(!r.text.includes(token));++ // The result page (Opus, 11-07): the answer carries Copy and a follow-up that starts a new ask about this task.++ assert.match(r.text, /<p class="sf-task-answer-acts" data-sf-answer-acts><button class="ovk-btn ovk-btn--primary" type="button" data-sf-answer-copy>Copy the answer<\/button>/);++ assert.match(r.text, /href="\/web\?q=Following%20up%20on%20%E2%80%9C%3Cfind%20a%20flight%3E%E2%80%9D%3A%20"/);const sent = t.upstreams.lastFor(`/api/v1/tasks/${t.upstreams.state.actor.id}`);assert.strictEqual(sent.headers.authorization, `Bearer ${token}`);});
@@ -351,11 +351,25 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state
@@ -351,11 +351,25 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state
.sf-task-answer .sf-desk-report-text { font-size: 1rem; }
.sf-task-answer .sf-desk-report-text { font-size: 1rem; }
.sf-task-answer h3 { margin: 16px 0 8px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
.sf-task-answer h3 { margin: 16px 0 8px; font-size: .76rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
.sf-task-wait { margin: 0; color: var(--text-secondary, #96a7c2); }
.sf-task-wait { margin: 0; color: var(--text-secondary, #96a7c2); }
-.sf-task-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
+.sf-task-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; counter-reset: sf-src; }
+.sf-task-sources li { counter-increment: sf-src; min-width: 0; max-width: 100%; }
.sf-task-sources a { display: inline-grid; gap: 1px; max-width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); text-decoration: none; }
.sf-task-sources a { display: inline-grid; gap: 1px; max-width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); text-decoration: none; }
.sf-task-sources a:hover { border-color: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 50%, var(--border, #1f2d47)); }
.sf-task-sources a:hover { border-color: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 50%, var(--border, #1f2d47)); }
.sf-task-sources b { font-size: .88rem; color: var(--text-primary, #e6edf7); }
.sf-task-sources b { font-size: .88rem; color: var(--text-primary, #e6edf7); }
.sf-task-sources span { font-size: .78rem; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }
.sf-task-sources span { font-size: .78rem; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }
+/* Surf's result page (Opus, 11-07): the sources carry the numbers Copy uses; once the task has ended with an answer, the
+ answer comes first, right under the question, and the card that followed the work sits under it. The desk is closed
+ by then, so its Watch button steps aside. */
+.sf-task-sources a { grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: center; }
+.sf-task-sources a::before { content: counter(sf-src); grid-row: 1 / span 2; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;
+ font-size: .72rem; font-weight: 800; color: var(--room-strong, var(--accent, #3b82f6)); background: color-mix(in srgb, var(--room-strong, var(--accent, #3b82f6)) 16%, transparent); }
+.sf-task-sources a b, .sf-task-sources a span { grid-column: 2; }
+.sf-desk-report .sf-task-answer-acts { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }
+.sf-task-answer-acts[hidden] { display: none; }
+.sf-desk-page.sf-task[data-sf-desk-open="0"]:has([data-sf-desk-report]:not(:empty)) .sf-task-head { order: -2; }
+.sf-desk-page.sf-task[data-sf-desk-open="0"]:has([data-sf-desk-report]:not(:empty)) .sf-task-answer { order: -1; }
+.sf-task[data-sf-desk-open="0"] .sf-task-acts { display: none; }
+.sf-desk-page.sf-task > .sf-task-answer { margin-top: 0; }
.sf-task-steps summary { display: flex; align-items: center; gap: 6px; min-height: 44px; cursor: pointer; font-weight: 750; list-style: none; }
.sf-task-steps summary { display: flex; align-items: center; gap: 6px; min-height: 44px; cursor: pointer; font-weight: 750; list-style: none; }
.sf-task-steps summary::-webkit-details-marker { display: none; }
.sf-task-steps summary::-webkit-details-marker { display: none; }
.sf-task-steps summary::before { content: '›'; display: inline-block; width: 14px; font-size: 1.2em; transition: transform .2s; }
.sf-task-steps summary::before { content: '›'; display: inline-block; width: 14px; font-size: 1.2em; transition: transform .2s; }
@@ -444,6 +444,10 @@
@@ -444,6 +444,10 @@
li.appendChild(a); list.appendChild(li);
li.appendChild(a); list.appendChild(li);
});
});
root.querySelector('[data-sf-desk-sources-empty]').hidden = !!list.children.length;
root.querySelector('[data-sf-desk-sources-empty]').hidden = !!list.children.length;
+ var acts = root.querySelector('[data-sf-answer-acts]');
+ var finalAnswer = t.result && typeof t.result.answer === 'string' ? t.result.answer : '';
+ if (acts) acts.hidden = !finalAnswer;
+ if (!open && !finalAnswer) root.querySelector('[data-sf-desk-report-empty]').textContent = 'Your agent finished without an answer. Every step below shows how far it got.';
var answer = t.result && typeof t.result.answer === 'string' ? t.result.answer : '';
var answer = t.result && typeof t.result.answer === 'string' ? t.result.answer : '';
root.querySelector('[data-sf-desk-report]').textContent = answer;
root.querySelector('[data-sf-desk-report]').textContent = answer;
root.querySelector('[data-sf-desk-report-empty]').hidden = !!answer;
root.querySelector('[data-sf-desk-report-empty]').hidden = !!answer;
@@ -455,6 +459,18 @@
@@ -455,6 +459,18 @@
if (latest) latest.textContent = '';
if (latest) latest.textContent = '';
}
}
}
}
+ // Copy the answer, with its sources numbered as they show (Opus, 11-07: Surf's result page).
+ var copyBtn = root.querySelector('[data-sf-answer-copy]');
+ if (copyBtn) copyBtn.addEventListener('click', function () {
+ var text = (root.querySelector('[data-sf-desk-report]') || {}).textContent || '';
+ var links = Array.prototype.map.call(root.querySelectorAll('[data-sf-desk-sources] a'), function (a, i) { return (i + 1) + '. ' + a.href; });
+ if (links.length) text += '\n\nSources:\n' + links.join('\n');
+ if (!navigator.clipboard || !text.trim()) return;
+ navigator.clipboard.writeText(text.trim()).then(function () {
+ copyBtn.textContent = 'Copied';
+ setTimeout(function () { copyBtn.textContent = 'Copy the answer'; }, 1500);
+ }, function () {});
+ });
function refresh() {
function refresh() {
fetch(base, { credentials: 'same-origin', headers: { accept: 'application/json' } })
fetch(base, { credentials: 'same-origin', headers: { accept: 'application/json' } })
.then(function (r) { if (!r.ok) throw new Error('Task unavailable'); return r.json(); })
.then(function (r) { if (!r.ok) throw new Error('Task unavailable'); return r.json(); })
@@ -339,8 +339,9 @@ function webTask({ task, actorUrl } = {}) {
@@ -339,8 +339,9 @@ function webTask({ task, actorUrl } = {}) {
<p class="sf-task-more"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open in Actor</a><span aria-hidden="true">·</span><a href="/web">Ask another</a></p>
<p class="sf-task-more"><a href="${esc(actor)}" target="_blank" rel="noopener noreferrer">Open in Actor</a><span aria-hidden="true">·</span><a href="/web">Ask another</a></p>
</section>
</section>
<section class="sf-card sf-desk-report sf-task-answer" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">The answer</h2>
<section class="sf-card sf-desk-report sf-task-answer" aria-labelledby="sf-desk-report-h"><h2 id="sf-desk-report-h">The answer</h2>
-<div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p class="sf-task-wait" data-sf-desk-report-empty${report ? ' hidden' : ''}>It appears here when your agent finishes, with every source it used.</p>
+<div class="sf-desk-report-text" data-sf-desk-report>${esc(report)}</div><p class="sf-task-wait" data-sf-desk-report-empty${report ? ' hidden' : ''}>${open ? 'It appears here when your agent finishes, with every source it used.' : 'Your agent finished without an answer. Every step below shows how far it got.'}</p>
<div class="sf-task-cites"><h3>Sources</h3><ul class="sf-task-sources" data-sf-desk-sources>${sources.map(sourceChip).join('')}</ul><p class="sf-task-wait" data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>None yet.</p></div>
<div class="sf-task-cites"><h3>Sources</h3><ul class="sf-task-sources" data-sf-desk-sources>${sources.map(sourceChip).join('')}</ul><p class="sf-task-wait" data-sf-desk-sources-empty${sources.length ? ' hidden' : ''}>None yet.</p></div>
+<p class="sf-task-answer-acts" data-sf-answer-acts${report ? '' : ' hidden'}><button class="ovk-btn ovk-btn--primary" type="button" data-sf-answer-copy>Copy the answer</button><a class="ovk-btn ovk-btn--quiet" href="/web?q=${encodeURIComponent(`Following up on \u201c${String(task.task || 'my last task').slice(0, 200)}\u201d: `)}">Ask a follow-up</a></p>
</section>
</section>
<details class="sf-card sf-desk-steps sf-task-steps"${open ? ' open' : ''}><summary>Every step <span class="sf-task-count" data-sf-desk-count></span></summary><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></details>
<details class="sf-card sf-desk-steps sf-task-steps"${open ? ' open' : ''}><summary>Every step <span class="sf-task-count" data-sf-desk-count></span></summary><ol data-sf-desk-steps></ol><p class="sf-fine" data-sf-desk-steps-note>Connecting to the agent’s live steps…</p></details>
<p class="sf-fine sf-task-rule">It never signs in or pays for you: at a login, a captcha or a checkout it stops and hands over.</p></div>`;
<p class="sf-fine sf-task-rule">It never signs in or pays for you: at a login, a captcha or a checkout it stops and hands over.</p></div>`;
@@ -152,6 +152,9 @@ const ORIGIN = 'https://openvibe.surf';
@@ -152,6 +152,9 @@ const ORIGIN = 'https://openvibe.surf';
assert.match(r.text, /openvibe\.bot\/machines/);
assert.match(r.text, /openvibe\.bot\/machines/);
assert.match(r.text, /openvibe\.actor\/tasks/);
assert.match(r.text, /openvibe\.actor\/tasks/);
assert.ok(!r.text.includes(token));
assert.ok(!r.text.includes(token));
+ // The result page (Opus, 11-07): the answer carries Copy and a follow-up that starts a new ask about this task.
+ assert.match(r.text, /<p class="sf-task-answer-acts" data-sf-answer-acts><button class="ovk-btn ovk-btn--primary" type="button" data-sf-answer-copy>Copy the answer<\/button>/);
+ assert.match(r.text, /href="\/web\?q=Following%20up%20on%20%E2%80%9C%3Cfind%20a%20flight%3E%E2%80%9D%3A%20"/);
const sent = t.upstreams.lastFor(`/api/v1/tasks/${t.upstreams.state.actor.id}`);
const sent = t.upstreams.lastFor(`/api/v1/tasks/${t.upstreams.state.actor.id}`);
assert.strictEqual(sent.headers.authorization, `Bearer ${token}`);
assert.strictEqual(sent.headers.authorization, `Bearer ${token}`);
});
});