Spaces:
Running on Zero
Running on Zero
feat: add turn watchdog
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +5 -0
- static/app.js +38 -1
- static/styles.css +17 -0
README.md
CHANGED
|
@@ -75,6 +75,11 @@ Every scored fate page now carries a deterministic `wood_map` artifact: backgrou
|
|
| 75 |
the closest cited echoes, and a green/red "you" dot for the current idea. The live UI and PNG export render the same
|
| 76 |
map, so the share artifact visually proves whether the page sits in an empty margin or near existing work.
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
## Tool-Call Contract
|
| 79 |
|
| 80 |
`/api/tool-contracts` exposes the JSON schemas intended for MiniCPM-style tool calling. `tool_contract_check` accepts a
|
|
|
|
| 75 |
the closest cited echoes, and a green/red "you" dot for the current idea. The live UI and PNG export render the same
|
| 76 |
map, so the share artifact visually proves whether the page sits in an empty margin or near existing work.
|
| 77 |
|
| 78 |
+
## Latency Watchdog
|
| 79 |
+
|
| 80 |
+
The custom frontend shows optimistic ink immediately after submit. If the first streamed token is slow, a lightweight
|
| 81 |
+
watchdog updates the page text so the demo never sits in a silent blank state during Space startup or model routing.
|
| 82 |
+
|
| 83 |
## Tool-Call Contract
|
| 84 |
|
| 85 |
`/api/tool-contracts` exposes the JSON schemas intended for MiniCPM-style tool calling. `tool_contract_check` accepts a
|
static/app.js
CHANGED
|
@@ -26,6 +26,8 @@ let clientPromise = Client.connect(window.location.origin);
|
|
| 26 |
let currentArtifact = null;
|
| 27 |
let targetOptions = [];
|
| 28 |
let profileFields = [];
|
|
|
|
|
|
|
| 29 |
|
| 30 |
bootstrap();
|
| 31 |
|
|
@@ -83,10 +85,10 @@ async function runTurn(message) {
|
|
| 83 |
input.value = "";
|
| 84 |
submit.disabled = true;
|
| 85 |
setCommandDisabled(true);
|
| 86 |
-
ink.textContent = "";
|
| 87 |
ink.classList.remove("bleed", "gold");
|
| 88 |
corrections.textContent = "";
|
| 89 |
planEl.innerHTML = "";
|
|
|
|
| 90 |
|
| 91 |
try {
|
| 92 |
const client = await clientPromise;
|
|
@@ -103,9 +105,12 @@ async function runTurn(message) {
|
|
| 103 |
}
|
| 104 |
}
|
| 105 |
} catch (error) {
|
|
|
|
| 106 |
ink.textContent = `The page tore before it could answer: ${error.message}`;
|
|
|
|
| 107 |
ink.classList.add("bleed");
|
| 108 |
} finally {
|
|
|
|
| 109 |
submit.disabled = false;
|
| 110 |
setCommandDisabled(false);
|
| 111 |
input.focus();
|
|
@@ -190,11 +195,17 @@ function handleEvent(event) {
|
|
| 190 |
}
|
| 191 |
|
| 192 |
if (event.type === "token") {
|
|
|
|
| 193 |
ink.textContent += event.text;
|
| 194 |
return;
|
| 195 |
}
|
| 196 |
|
| 197 |
if (event.type === "done") {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
session = event.state || {};
|
| 199 |
session.profile = session.profile || {};
|
| 200 |
session.targets = Array.isArray(session.targets) ? session.targets : [];
|
|
@@ -404,6 +415,32 @@ function setCommandDisabled(disabled) {
|
|
| 404 |
});
|
| 405 |
}
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
function syncCurrentIdeaTargets() {
|
| 408 |
const currentId = session.current_idea_id;
|
| 409 |
if (!currentId || !Array.isArray(session.ideas)) return;
|
|
|
|
| 26 |
let currentArtifact = null;
|
| 27 |
let targetOptions = [];
|
| 28 |
let profileFields = [];
|
| 29 |
+
let turnWatchdog = null;
|
| 30 |
+
let sawTurnToken = false;
|
| 31 |
|
| 32 |
bootstrap();
|
| 33 |
|
|
|
|
| 85 |
input.value = "";
|
| 86 |
submit.disabled = true;
|
| 87 |
setCommandDisabled(true);
|
|
|
|
| 88 |
ink.classList.remove("bleed", "gold");
|
| 89 |
corrections.textContent = "";
|
| 90 |
planEl.innerHTML = "";
|
| 91 |
+
startTurnWatchdog();
|
| 92 |
|
| 93 |
try {
|
| 94 |
const client = await clientPromise;
|
|
|
|
| 105 |
}
|
| 106 |
}
|
| 107 |
} catch (error) {
|
| 108 |
+
clearTurnWatchdog();
|
| 109 |
ink.textContent = `The page tore before it could answer: ${error.message}`;
|
| 110 |
+
ink.classList.remove("thinking");
|
| 111 |
ink.classList.add("bleed");
|
| 112 |
} finally {
|
| 113 |
+
clearTurnWatchdog();
|
| 114 |
submit.disabled = false;
|
| 115 |
setCommandDisabled(false);
|
| 116 |
input.focus();
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
if (event.type === "token") {
|
| 198 |
+
markFirstTokenSeen();
|
| 199 |
ink.textContent += event.text;
|
| 200 |
return;
|
| 201 |
}
|
| 202 |
|
| 203 |
if (event.type === "done") {
|
| 204 |
+
if (!sawTurnToken) {
|
| 205 |
+
clearTurnWatchdog();
|
| 206 |
+
ink.textContent = event.response || ink.textContent;
|
| 207 |
+
ink.classList.remove("thinking");
|
| 208 |
+
}
|
| 209 |
session = event.state || {};
|
| 210 |
session.profile = session.profile || {};
|
| 211 |
session.targets = Array.isArray(session.targets) ? session.targets : [];
|
|
|
|
| 415 |
});
|
| 416 |
}
|
| 417 |
|
| 418 |
+
function startTurnWatchdog() {
|
| 419 |
+
clearTurnWatchdog();
|
| 420 |
+
sawTurnToken = false;
|
| 421 |
+
ink.textContent = "The page is choosing its words.";
|
| 422 |
+
ink.classList.add("thinking");
|
| 423 |
+
turnWatchdog = window.setTimeout(() => {
|
| 424 |
+
if (sawTurnToken) return;
|
| 425 |
+
ink.textContent = "Still riffling the inked pages.";
|
| 426 |
+
}, 2200);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
function markFirstTokenSeen() {
|
| 430 |
+
if (sawTurnToken) return;
|
| 431 |
+
sawTurnToken = true;
|
| 432 |
+
clearTurnWatchdog();
|
| 433 |
+
ink.textContent = "";
|
| 434 |
+
ink.classList.remove("thinking");
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function clearTurnWatchdog() {
|
| 438 |
+
if (turnWatchdog) {
|
| 439 |
+
window.clearTimeout(turnWatchdog);
|
| 440 |
+
turnWatchdog = null;
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
function syncCurrentIdeaTargets() {
|
| 445 |
const currentId = session.current_idea_id;
|
| 446 |
if (!currentId || !Array.isArray(session.ideas)) return;
|
static/styles.css
CHANGED
|
@@ -127,6 +127,23 @@ h2 {
|
|
| 127 |
text-shadow: 0 0 18px rgba(230, 189, 63, 0.42);
|
| 128 |
}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
.prompt-row {
|
| 131 |
display: grid;
|
| 132 |
grid-template-columns: minmax(0, 1fr) 84px;
|
|
|
|
| 127 |
text-shadow: 0 0 18px rgba(230, 189, 63, 0.42);
|
| 128 |
}
|
| 129 |
|
| 130 |
+
.ink.thinking {
|
| 131 |
+
color: var(--muted-ink);
|
| 132 |
+
font-style: italic;
|
| 133 |
+
animation: ink-pulse 1.6s ease-in-out infinite;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
@keyframes ink-pulse {
|
| 137 |
+
0%,
|
| 138 |
+
100% {
|
| 139 |
+
opacity: 0.62;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
50% {
|
| 143 |
+
opacity: 1;
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
.prompt-row {
|
| 148 |
display: grid;
|
| 149 |
grid-template-columns: minmax(0, 1fr) 84px;
|