JacobLinCool Codex commited on
Commit
1afd4af
·
verified ·
1 Parent(s): 902a11f

feat: add turn watchdog

Browse files

Co-authored-by: Codex <noreply@openai.com>

Files changed (3) hide show
  1. README.md +5 -0
  2. static/app.js +38 -1
  3. 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;