mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 17:22:00 +01:00
Refine agent details, identity editing, responsive controls, and organization charts with three card tiers, clearer save feedback, and stable filtering. Guard instruction-file renames and root configuration updates in one transaction through the agent lifecycle integration, preserving conflict checks, rollback, and configuration history. Co-Authored-By: Paperclip <noreply@paperclip.ing>
60 lines
3.8 KiB
TypeScript
60 lines
3.8 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const AGENT_PALETTE_IDS = ["bubblegum-sky", "pink-lemonade", "orchid-peach", "coral-mint", "lime-lagoon", "arctic-blue", "solar-flare", "violet-ember", "deep-tide", "coral-current", "golden-hour", "tangerine-cobalt", "electric-grove", "flamingo-jade", "cherry-pop", "turquoise-cherry", "ultraviolet-tide"] as const;
|
|
export type AgentPaletteId = typeof AGENT_PALETTE_IDS[number];
|
|
export type CharacterPaletteId = AgentPaletteId | "muted-dream";
|
|
export const AGENT_AVATAR_SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256, 512] as const;
|
|
export type AgentAvatarSize = typeof AGENT_AVATAR_SIZES[number];
|
|
export type AgentAvatarView = "front" | "portrait";
|
|
export type AgentAvatarBackground = "transparent" | "paperclip-dark";
|
|
/** sRGB PNG equivalent of index.css .dark --background: oklch(0.205 0 0). */
|
|
export const PAPERCLIP_DARK_AVATAR_BACKGROUND = "#171717";
|
|
export const CHARACTER_STATES = ["rest", "idle", "listening", "thinking", "working", "success", "confused", "sleepy", "loading"] as const;
|
|
export type CharacterState = typeof CHARACTER_STATES[number];
|
|
export const agentAppearanceSchema = z.object({
|
|
schemaVersion: z.literal(1),
|
|
characterVersion: z.literal("cap-v1"),
|
|
paletteId: z.enum(AGENT_PALETTE_IDS),
|
|
/** Company-scoped normalized image, set through the avatar upload service. */
|
|
customAvatarAssetId: z.uuid().optional(),
|
|
}).strict();
|
|
export type AgentAppearance = z.infer<typeof agentAppearanceSchema>;
|
|
|
|
export function appearanceForPalette(paletteId: AgentPaletteId): AgentAppearance {
|
|
return { schemaVersion: 1, characterVersion: "cap-v1", paletteId };
|
|
}
|
|
/** A persisted choice, never randomize while rendering. */
|
|
export function randomAgentAppearance(): AgentAppearance {
|
|
const bytes = new Uint32Array(1);
|
|
// Rejection sampling avoids modulo bias.
|
|
const limit = Math.floor(0x100000000 / AGENT_PALETTE_IDS.length) * AGENT_PALETTE_IDS.length;
|
|
do { globalThis.crypto.getRandomValues(bytes); } while (bytes[0] >= limit);
|
|
return appearanceForPalette(AGENT_PALETTE_IDS[bytes[0] % AGENT_PALETTE_IDS.length]);
|
|
}
|
|
/** Must match the migration: rolling base-31 hash, modulo 17 at every step. */
|
|
export function legacyAgentAppearance(id: string): AgentAppearance {
|
|
let hash = 0;
|
|
for (const char of id) hash = (hash * 31 + char.charCodeAt(0)) % AGENT_PALETTE_IDS.length;
|
|
return appearanceForPalette(AGENT_PALETTE_IDS[hash]);
|
|
}
|
|
export function resolveAgentAppearance(appearance: unknown, id = "agent"): AgentAppearance {
|
|
const parsed = agentAppearanceSchema.safeParse(appearance);
|
|
return parsed.success ? parsed.data : legacyAgentAppearance(id);
|
|
}
|
|
export function agentAvatarUrl(appearance: AgentAppearance, size: AgentAvatarSize = 512, scale: 1 | 2 = 1, pose: CharacterState = "rest", muted = false, background: AgentAvatarBackground = "transparent", view: AgentAvatarView = "front"): string {
|
|
if (appearance.customAvatarAssetId) return `/api/assets/${appearance.customAvatarAssetId}/content`;
|
|
return `/api/agent-avatars/${appearance.characterVersion}/${muted ? "muted-dream" : appearance.paletteId}/${pose}.png?size=${size}&scale=${scale}${background === "paperclip-dark" ? "&background=paperclip-dark" : ""}${view === "portrait" ? "&view=portrait" : ""}`;
|
|
}
|
|
export function characterStateForAgent(status: string): CharacterState {
|
|
if (status === "running") return "working";
|
|
if (status === "error") return "confused";
|
|
if (status === "paused" || status === "terminated" || status === "pending_approval") return "rest";
|
|
return "idle";
|
|
}
|
|
|
|
/** Hydrate compact agent projections without an additional per-agent request. */
|
|
export function withAgentAppearance<T extends { id: string; appearance?: AgentAppearance | null }>(agent: T) {
|
|
const appearance = resolveAgentAppearance(agent.appearance, agent.id);
|
|
return { ...agent, appearance, avatarUrl: agentAvatarUrl(appearance) };
|
|
}
|