feat(sagi): custom pets — Codex pet bundles, images, featured downloads
The mascot can wear a pet. Right-click it for Hide pet, Size (48-250 px), Choose pet (Sagi, featured, your own - deletable), Upload your own..., Import .codex-pet bundle..., Find more on codex-pets.net and Pet settings.... The worn pet and its size are viewer prefs (agenthub.pet, agenthub.pet.size); the pets live on the server under <data dir>/pets/<id>/ (server/lib/pets.js, /api/pets). A Codex pet is the .codex-pet.zip format: pet.json + a 1536x1872 spritesheet.webp, 8x9 frames of 192x208, one row per state. PetSprite.tsx plays the row of the mascot's mood; an uploaded image bobs. Only the two bundle files are read, sizes are checked from the zip directory before inflating, the sheet's WebP header must match, uploads are raster only (no SVG), ids are made by the server, every write is same-origin. The four featured pets (Goku, Bac Ho, Border Collie, 11) are codex-pets.net uploads with no redistribution licence, so none is bundled: a pick downloads it from codex-pets.net by a fixed id.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* @file prefs.ts
|
||||
* @description Tiny localStorage-backed preference store for Sagi (enabled +
|
||||
* muted). Broadcasts changes via a window CustomEvent so the Settings toggle
|
||||
* and the live widget stay in sync within the same tab without a reload.
|
||||
* @description Tiny localStorage-backed preference store for Sagi (enabled,
|
||||
* muted, the pet it wears and its size). Broadcasts changes via a window
|
||||
* CustomEvent so the Settings toggle and the live widget stay in sync within
|
||||
* the same tab without a reload.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
@@ -53,6 +54,54 @@
|
||||
const ENABLED_KEY = "agent-dashboard-sagi-enabled";
|
||||
const MUTED_KEY = "agent-dashboard-sagi-muted";
|
||||
const POS_KEY = "agent-dashboard-sagi-pos";
|
||||
const PET_KEY = "agenthub.pet";
|
||||
const SIZE_KEY = "agenthub.pet.size";
|
||||
/** Sagi's own size, and the slider's range. */
|
||||
export const PET_SIZE = { default: 68, min: 48, max: 250 };
|
||||
|
||||
/** The pet the mascot wears: Sagi itself, or a custom one (`/api/pets`).
|
||||
* The kind rides along so the mascot renders without a fetch. */
|
||||
export type PetChoice = { id: "sagi" } | { id: string; kind: "codex" | "image" };
|
||||
const SAGI: PetChoice = { id: "sagi" };
|
||||
|
||||
function readPet(): PetChoice {
|
||||
try {
|
||||
const p = JSON.parse(localStorage.getItem(PET_KEY) ?? "null") as Partial<{
|
||||
id: string;
|
||||
kind: string;
|
||||
}> | null;
|
||||
if (
|
||||
p &&
|
||||
typeof p.id === "string" &&
|
||||
p.id !== "sagi" &&
|
||||
(p.kind === "codex" || p.kind === "image")
|
||||
)
|
||||
return { id: p.id, kind: p.kind };
|
||||
} catch {
|
||||
/* unreadable: Sagi */
|
||||
}
|
||||
return SAGI;
|
||||
}
|
||||
|
||||
function readSize(): number {
|
||||
const n = Number(localStorage.getItem(SIZE_KEY));
|
||||
return Number.isFinite(n) && n > 0
|
||||
? Math.round(Math.min(PET_SIZE.max, Math.max(PET_SIZE.min, n)))
|
||||
: PET_SIZE.default;
|
||||
}
|
||||
|
||||
function writeAndNotify(key: string, value: string): void {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// Best-effort, like every pref here.
|
||||
}
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent(EVENT));
|
||||
} catch {
|
||||
// SSR / non-DOM contexts: nothing to notify.
|
||||
}
|
||||
}
|
||||
const EVENT = "sagi:prefs";
|
||||
|
||||
/**
|
||||
@@ -118,6 +167,20 @@ export const sagiPrefs = {
|
||||
setMuted: (v: boolean) => writeBool(MUTED_KEY, v),
|
||||
getPos: readPos,
|
||||
setPos: writePos,
|
||||
getPet: readPet,
|
||||
setPet: (pet: PetChoice) => writeAndNotify(PET_KEY, JSON.stringify(pet)),
|
||||
getSize: () => {
|
||||
try {
|
||||
return readSize();
|
||||
} catch {
|
||||
return PET_SIZE.default;
|
||||
}
|
||||
},
|
||||
setSize: (px: number) =>
|
||||
writeAndNotify(
|
||||
SIZE_KEY,
|
||||
String(Math.round(Math.min(PET_SIZE.max, Math.max(PET_SIZE.min, px))))
|
||||
),
|
||||
/** Subscribe to any pref change; returns an unsubscribe fn. */
|
||||
subscribe(handler: () => void): () => void {
|
||||
const listener = () => handler();
|
||||
|
||||
Reference in New Issue
Block a user