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 ────────────────────────────────────────────────
|
// ── State class management ────────────────────────────────────────────────
|
||||||
const ALL_STATES = [
|
const ALL_STATES = [
|
||||||
'offline','starting','loading','warming_up','idle',
|
'offline','starting','loading','warming_up','idle',
|
||||||
'sleeping','waiting','processing_prompt','generating','unloading'
|
'sleeping','waiting','processing_prompt','generating','unloading','error'
|
||||||
];
|
];
|
||||||
|
|
||||||
function applyState(stateName) {
|
function applyState(stateName) {
|
||||||
|
|||||||
@@ -64,18 +64,23 @@
|
|||||||
<!-- Mouth: open (talking) -->
|
<!-- Mouth: open (talking) -->
|
||||||
<ellipse class="part mouth-open" cx="50" cy="39" rx="4.5" ry="3"/>
|
<ellipse class="part mouth-open" cx="50" cy="39" rx="4.5" ry="3"/>
|
||||||
|
|
||||||
<!-- Front legs -->
|
<!-- Legs + hooves (grouped so animation moves both together) -->
|
||||||
<rect class="part leg leg-fl" x="32" y="100" width="10" height="30" rx="4"/>
|
<g class="leg-fl">
|
||||||
<rect class="part leg leg-fr" x="46" y="100" width="10" height="30" rx="4"/>
|
<rect class="part leg" x="29" y="100" width="10" height="30" rx="4"/>
|
||||||
<!-- Back legs -->
|
<rect class="part hoof" x="29" y="126" width="10" height="4" rx="2"/>
|
||||||
<rect class="part leg leg-bl" x="57" y="100" width="10" height="30" rx="4"/>
|
</g>
|
||||||
<rect class="part leg leg-br" x="71" y="100" width="10" height="30" rx="4"/>
|
<g class="leg-fr">
|
||||||
|
<rect class="part leg" x="40" y="100" width="10" height="30" rx="4"/>
|
||||||
<!-- Hooves -->
|
<rect class="part hoof" x="40" y="126" width="10" height="4" rx="2"/>
|
||||||
<rect class="part hoof" x="32" y="126" width="10" height="4" rx="2"/>
|
</g>
|
||||||
<rect class="part hoof" x="46" y="126" width="10" height="4" rx="2"/>
|
<g class="leg-bl">
|
||||||
<rect class="part hoof" x="57" y="126" width="10" height="4" rx="2"/>
|
<rect class="part leg" x="51" y="100" width="10" height="30" rx="4"/>
|
||||||
<rect class="part hoof" x="71" y="126" width="10" height="4" rx="2"/>
|
<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) -->
|
<!-- Book (reading) -->
|
||||||
<g class="part book">
|
<g class="part book">
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ body.state-processing_prompt #progress-wrap { display: flex; }
|
|||||||
/* offline */
|
/* offline */
|
||||||
body.state-offline #llama { opacity: 0.15; }
|
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 ─────────────────────────────────────────────────────────────────── */
|
||||||
#zzz-wrap {
|
#zzz-wrap {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -341,6 +349,11 @@ body.state-unloading #row-speed,
|
|||||||
body.state-unloading #row-tokens,
|
body.state-unloading #row-tokens,
|
||||||
body.state-unloading #row-load-stage { display: none; }
|
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-speed,
|
||||||
body.state-processing_prompt #row-load-stage { display: none; }
|
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-fl,
|
||||||
body.state-loading .leg-bl {
|
body.state-loading .leg-bl {
|
||||||
animation: walk-a 0.7s ease-in-out infinite;
|
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-fr,
|
||||||
body.state-loading .leg-br {
|
body.state-loading .leg-br {
|
||||||
animation: walk-b 0.7s ease-in-out infinite;
|
animation: walk-b 0.7s ease-in-out infinite;
|
||||||
transform-origin: 51px 100px;
|
transform-origin: 45px 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes walk-a {
|
@keyframes walk-a {
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ _PROMPT_SUMMARY_RE = re.compile(
|
|||||||
)
|
)
|
||||||
_RELEASE_RE = re.compile(r'slot\s+release:.*stop processing')
|
_RELEASE_RE = re.compile(r'slot\s+release:.*stop processing')
|
||||||
_UNLOAD_RE = re.compile(r'unload_lru:|unload: stopping model instance')
|
_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 ─────────────────────────────────────────────────────────────
|
# ── State machine ─────────────────────────────────────────────────────────────
|
||||||
@@ -121,6 +123,8 @@ class LogParser:
|
|||||||
return self._set(state="waiting")
|
return self._set(state="waiting")
|
||||||
if _UNLOAD_RE.search(content):
|
if _UNLOAD_RE.search(content):
|
||||||
return self._set(state="unloading")
|
return self._set(state="unloading")
|
||||||
|
if _EXIT_ERROR_RE.search(content):
|
||||||
|
return self._set(state="error")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ── Child-process lines ──────────────────────────────────────────────────
|
# ── Child-process lines ──────────────────────────────────────────────────
|
||||||
@@ -130,6 +134,8 @@ class LogParser:
|
|||||||
|
|
||||||
if m := _CMD_STATE_RE.match(content):
|
if m := _CMD_STATE_RE.match(content):
|
||||||
return self._handle_cmd_state(m.group(1))
|
return self._handle_cmd_state(m.group(1))
|
||||||
|
if _LOAD_ERROR_RE.search(content):
|
||||||
|
return self._set(state="error")
|
||||||
if _WARMUP_RE.search(content):
|
if _WARMUP_RE.search(content):
|
||||||
return self._set(state="warming_up")
|
return self._set(state="warming_up")
|
||||||
if _IDLE_RE.search(content):
|
if _IDLE_RE.search(content):
|
||||||
@@ -137,14 +143,14 @@ class LogParser:
|
|||||||
if _LAUNCH_RE.search(content):
|
if _LAUNCH_RE.search(content):
|
||||||
return self._set(state="processing_prompt")
|
return self._set(state="processing_prompt")
|
||||||
if m := _PROMPT_TIMING_RE.search(content):
|
if m := _PROMPT_TIMING_RE.search(content):
|
||||||
|
progress = float(m.group(2))
|
||||||
|
done = progress >= 1.0
|
||||||
metrics = Metrics(
|
metrics = Metrics(
|
||||||
prompt_speed=float(m.group(3)),
|
prompt_speed=float(m.group(3)),
|
||||||
prompt_tokens=int(m.group(1)),
|
prompt_tokens=int(m.group(1)),
|
||||||
prompt_progress=float(m.group(2)),
|
prompt_progress=None if done else progress,
|
||||||
gen_speed=self._state.metrics.gen_speed,
|
|
||||||
n_decoded=self._state.metrics.n_decoded,
|
|
||||||
)
|
)
|
||||||
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):
|
if m := _GEN_TIMING_RE.search(content):
|
||||||
metrics = Metrics(
|
metrics = Metrics(
|
||||||
prompt_speed=self._state.metrics.prompt_speed,
|
prompt_speed=self._state.metrics.prompt_speed,
|
||||||
@@ -154,14 +160,18 @@ class LogParser:
|
|||||||
)
|
)
|
||||||
return self._set(state="generating", metrics=metrics)
|
return self._set(state="generating", metrics=metrics)
|
||||||
if m := _PROMPT_SUMMARY_RE.search(content):
|
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(
|
metrics = Metrics(
|
||||||
prompt_speed=float(m.group(2)),
|
prompt_speed=float(m.group(2)),
|
||||||
prompt_tokens=int(m.group(1)),
|
prompt_tokens=int(m.group(1)),
|
||||||
gen_speed=self._state.metrics.gen_speed,
|
gen_speed=self._state.metrics.gen_speed,
|
||||||
n_decoded=self._state.metrics.n_decoded,
|
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)
|
return self._set(metrics=metrics)
|
||||||
if _RELEASE_RE.search(content):
|
if _RELEASE_RE.search(content):
|
||||||
return self._set(
|
return self._set(
|
||||||
|
|||||||
Reference in New Issue
Block a user