mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
3c5415805c
* refactor(ui): share sidebar menu contracts * refactor(ui): share segmented controls * refactor(ui): deduplicate slash menu keys * refactor(ui): share persisted set storage * refactor(ui): centralize fnv hashing * refactor(ui): reuse workboard helpers * refactor(ui): remove stale terminal style * fix(ui): align sidebar menu callback type
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
// Whimsical long-wait status word ("Clawing…") for the chat working row.
|
|
// Silent for the first stretch of a run, then rotates through crab-themed
|
|
// gerunds so long quiet runs feel alive without claiming progress data the
|
|
// UI does not have. Decorative only — the row keeps its sr-only "Working…".
|
|
import { html, nothing } from "lit";
|
|
import { property } from "lit/decorators.js";
|
|
import { t } from "../i18n/index.ts";
|
|
import { fnv1aUtf16 } from "../lib/fnv1a.ts";
|
|
import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts";
|
|
import { PollController } from "../lit/poll-controller.ts";
|
|
|
|
const PHRASE_KEYS = [
|
|
"shelling",
|
|
"scuttling",
|
|
"clawing",
|
|
"pinching",
|
|
"molting",
|
|
"bubbling",
|
|
"tiding",
|
|
"reefing",
|
|
"cracking",
|
|
"sifting",
|
|
"brining",
|
|
"nautiling",
|
|
"krilling",
|
|
"barnacling",
|
|
"lobstering",
|
|
"tidepooling",
|
|
"pearling",
|
|
"snapping",
|
|
"surfacing",
|
|
] as const;
|
|
|
|
/** Quiet grace period before the first phrase appears. Mirrored as literals
|
|
* in working-phrase.test.ts (knip forbids test-only exports). */
|
|
const WORKING_PHRASE_SHOW_AFTER_MS = 30_000;
|
|
/** How long each phrase holds before rotating to the next. */
|
|
const WORKING_PHRASE_ROTATE_EVERY_MS = 45_000;
|
|
|
|
/** Constant-time stride walk over the phrase list: any stride in
|
|
* [1, len-1] guarantees adjacent buckets differ, and with a prime-length
|
|
* list (currently 19) every stride cycles through all phrases before
|
|
* repeating. Must stay O(1) in bucket — ChatItem.startedAt can be an
|
|
* arbitrarily old timestamp, and this runs on a one-second poll. */
|
|
function displayedPhraseIndex(seed: string, bucket: number): number {
|
|
const length = PHRASE_KEYS.length;
|
|
const offset = fnv1aUtf16(`${seed}:offset`) % length;
|
|
const stride = 1 + (fnv1aUtf16(`${seed}:stride`) % (length - 1));
|
|
return (offset + bucket * stride) % length;
|
|
}
|
|
|
|
class WorkingPhrase extends OpenClawLightDomContentsElement {
|
|
@property({ type: Number }) startMs: number | null = null;
|
|
@property() seed = "";
|
|
|
|
private readonly polling = new PollController(this, 1_000, () => this.requestUpdate(), false);
|
|
|
|
override connectedCallback() {
|
|
super.connectedCallback();
|
|
this.syncTimer();
|
|
}
|
|
|
|
override updated() {
|
|
this.syncTimer();
|
|
}
|
|
|
|
private syncTimer() {
|
|
if (this.isConnected && this.startMs != null) {
|
|
this.polling.start();
|
|
} else {
|
|
this.polling.stop();
|
|
}
|
|
}
|
|
|
|
override render() {
|
|
if (this.startMs == null) {
|
|
return nothing;
|
|
}
|
|
const elapsed = Date.now() - this.startMs;
|
|
if (elapsed < WORKING_PHRASE_SHOW_AFTER_MS) {
|
|
return nothing;
|
|
}
|
|
const sinceShown = elapsed - WORKING_PHRASE_SHOW_AFTER_MS;
|
|
const bucket = Math.floor(sinceShown / WORKING_PHRASE_ROTATE_EVERY_MS);
|
|
const index = displayedPhraseIndex(this.seed, bucket);
|
|
return html`<span>·</span> ${t(`chat.progressLabels.${PHRASE_KEYS[index]}`)}…`;
|
|
}
|
|
}
|
|
|
|
if (!customElements.get("openclaw-working-phrase")) {
|
|
customElements.define("openclaw-working-phrase", WorkingPhrase);
|
|
}
|