fix: error state, leg grouping, prompt→generating transition
- log_parser: detect model load failure (child + router patterns) → state=error; switch to generating at progress=1.00 and at prompt eval summary line - frontend: add state-error (llama flashes red, CSS variable override); group each leg+hoof in <g> so walk animation moves hooves with legs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
// ── State class management ────────────────────────────────────────────────
|
||||
const ALL_STATES = [
|
||||
'offline','starting','loading','warming_up','idle',
|
||||
'sleeping','waiting','processing_prompt','generating','unloading'
|
||||
'sleeping','waiting','processing_prompt','generating','unloading','error'
|
||||
];
|
||||
|
||||
function applyState(stateName) {
|
||||
|
||||
@@ -64,18 +64,23 @@
|
||||
<!-- Mouth: open (talking) -->
|
||||
<ellipse class="part mouth-open" cx="50" cy="39" rx="4.5" ry="3"/>
|
||||
|
||||
<!-- Front legs -->
|
||||
<rect class="part leg leg-fl" x="32" y="100" width="10" height="30" rx="4"/>
|
||||
<rect class="part leg leg-fr" x="46" y="100" width="10" height="30" rx="4"/>
|
||||
<!-- Back legs -->
|
||||
<rect class="part leg leg-bl" x="57" y="100" width="10" height="30" rx="4"/>
|
||||
<rect class="part leg leg-br" x="71" y="100" width="10" height="30" rx="4"/>
|
||||
|
||||
<!-- Hooves -->
|
||||
<rect class="part hoof" x="32" y="126" width="10" height="4" rx="2"/>
|
||||
<rect class="part hoof" x="46" y="126" width="10" height="4" rx="2"/>
|
||||
<rect class="part hoof" x="57" y="126" width="10" height="4" rx="2"/>
|
||||
<rect class="part hoof" x="71" y="126" width="10" height="4" rx="2"/>
|
||||
<!-- Legs + hooves (grouped so animation moves both together) -->
|
||||
<g class="leg-fl">
|
||||
<rect class="part leg" x="29" y="100" width="10" height="30" rx="4"/>
|
||||
<rect class="part hoof" x="29" y="126" width="10" height="4" rx="2"/>
|
||||
</g>
|
||||
<g class="leg-fr">
|
||||
<rect class="part leg" x="40" y="100" width="10" height="30" rx="4"/>
|
||||
<rect class="part hoof" x="40" y="126" width="10" height="4" rx="2"/>
|
||||
</g>
|
||||
<g class="leg-bl">
|
||||
<rect class="part leg" x="51" y="100" width="10" height="30" rx="4"/>
|
||||
<rect class="part hoof" x="51" y="126" width="10" height="4" rx="2"/>
|
||||
</g>
|
||||
<g class="leg-br">
|
||||
<rect class="part leg" x="62" y="100" width="10" height="30" rx="4"/>
|
||||
<rect class="part hoof" x="62" y="126" width="10" height="4" rx="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Book (reading) -->
|
||||
<g class="part book">
|
||||
|
||||
@@ -145,6 +145,14 @@ body.state-processing_prompt #progress-wrap { display: flex; }
|
||||
/* offline */
|
||||
body.state-offline #llama { opacity: 0.15; }
|
||||
|
||||
/* error: llama flashes red */
|
||||
body.state-error { --amber: var(--red); --amber-dim: #cc3333; --amber-dk: #882222; }
|
||||
body.state-error #llama { animation: error-flash 1s ease-in-out infinite; }
|
||||
@keyframes error-flash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* ── ZZZ ─────────────────────────────────────────────────────────────────── */
|
||||
#zzz-wrap {
|
||||
position: absolute;
|
||||
@@ -341,6 +349,11 @@ body.state-unloading #row-speed,
|
||||
body.state-unloading #row-tokens,
|
||||
body.state-unloading #row-load-stage { display: none; }
|
||||
|
||||
body.state-error #row-prompt,
|
||||
body.state-error #row-speed,
|
||||
body.state-error #row-tokens,
|
||||
body.state-error #row-load-stage { display: none; }
|
||||
|
||||
body.state-processing_prompt #row-speed,
|
||||
body.state-processing_prompt #row-load-stage { display: none; }
|
||||
|
||||
@@ -390,12 +403,12 @@ body.state-waiting .right-ear {
|
||||
body.state-loading .leg-fl,
|
||||
body.state-loading .leg-bl {
|
||||
animation: walk-a 0.7s ease-in-out infinite;
|
||||
transform-origin: 37px 100px;
|
||||
transform-origin: 34px 100px;
|
||||
}
|
||||
body.state-loading .leg-fr,
|
||||
body.state-loading .leg-br {
|
||||
animation: walk-b 0.7s ease-in-out infinite;
|
||||
transform-origin: 51px 100px;
|
||||
transform-origin: 45px 100px;
|
||||
}
|
||||
|
||||
@keyframes walk-a {
|
||||
|
||||
@@ -83,6 +83,8 @@ _PROMPT_SUMMARY_RE = re.compile(
|
||||
)
|
||||
_RELEASE_RE = re.compile(r'slot\s+release:.*stop processing')
|
||||
_UNLOAD_RE = re.compile(r'unload_lru:|unload: stopping model instance')
|
||||
_LOAD_ERROR_RE = re.compile(r'exiting due to model loading error')
|
||||
_EXIT_ERROR_RE = re.compile(r'operator\(\):.*exited with status 1')
|
||||
|
||||
|
||||
# ── State machine ─────────────────────────────────────────────────────────────
|
||||
@@ -121,6 +123,8 @@ class LogParser:
|
||||
return self._set(state="waiting")
|
||||
if _UNLOAD_RE.search(content):
|
||||
return self._set(state="unloading")
|
||||
if _EXIT_ERROR_RE.search(content):
|
||||
return self._set(state="error")
|
||||
return None
|
||||
|
||||
# ── Child-process lines ──────────────────────────────────────────────────
|
||||
@@ -130,6 +134,8 @@ class LogParser:
|
||||
|
||||
if m := _CMD_STATE_RE.match(content):
|
||||
return self._handle_cmd_state(m.group(1))
|
||||
if _LOAD_ERROR_RE.search(content):
|
||||
return self._set(state="error")
|
||||
if _WARMUP_RE.search(content):
|
||||
return self._set(state="warming_up")
|
||||
if _IDLE_RE.search(content):
|
||||
@@ -137,14 +143,14 @@ class LogParser:
|
||||
if _LAUNCH_RE.search(content):
|
||||
return self._set(state="processing_prompt")
|
||||
if m := _PROMPT_TIMING_RE.search(content):
|
||||
progress = float(m.group(2))
|
||||
done = progress >= 1.0
|
||||
metrics = Metrics(
|
||||
prompt_speed=float(m.group(3)),
|
||||
prompt_tokens=int(m.group(1)),
|
||||
prompt_progress=float(m.group(2)),
|
||||
gen_speed=self._state.metrics.gen_speed,
|
||||
n_decoded=self._state.metrics.n_decoded,
|
||||
prompt_progress=None if done else progress,
|
||||
)
|
||||
return self._set(state="processing_prompt", metrics=metrics)
|
||||
return self._set(state="generating" if done else "processing_prompt", metrics=metrics)
|
||||
if m := _GEN_TIMING_RE.search(content):
|
||||
metrics = Metrics(
|
||||
prompt_speed=self._state.metrics.prompt_speed,
|
||||
@@ -154,14 +160,18 @@ class LogParser:
|
||||
)
|
||||
return self._set(state="generating", metrics=metrics)
|
||||
if m := _PROMPT_SUMMARY_RE.search(content):
|
||||
# End-of-slot summary: update prompt speed without changing state.
|
||||
# For short prompts this fires after generating starts (no live prompt line).
|
||||
metrics = Metrics(
|
||||
prompt_speed=float(m.group(2)),
|
||||
prompt_tokens=int(m.group(1)),
|
||||
gen_speed=self._state.metrics.gen_speed,
|
||||
n_decoded=self._state.metrics.n_decoded,
|
||||
)
|
||||
if self._state.state == "processing_prompt":
|
||||
# Summary fires right after prompt eval finishes, before first token.
|
||||
# Advance to generating now rather than waiting for the first tg line.
|
||||
# For short prompts the summary arrives after generating has already
|
||||
# started, so the guard prevents stepping back.
|
||||
return self._set(state="generating", metrics=metrics)
|
||||
return self._set(metrics=metrics)
|
||||
if _RELEASE_RE.search(content):
|
||||
return self._set(
|
||||
|
||||
Reference in New Issue
Block a user