Auto-mode commit 7aa33cd accidentally deleted 78 files (14,814 lines) during M005
execution. Subsequent commits rebuilt some frontend files but backend/, alembic/,
tests/, whisper/, docker configs, and prompts were never restored in this repo.
This commit restores the full project tree by syncing from ub01's working directory,
which has all M001-M007 features running in production containers.
Restored: backend/ (config, models, routers, database, redis, search_service, worker),
alembic/ (6 migrations), docker/ (Dockerfiles, nginx, compose), prompts/ (4 stages),
tests/, whisper/, README.md, .env.example, chrysopedia-spec.md
113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
/**
|
|
* Deterministic generative avatar for creators.
|
|
*
|
|
* Bars arranged as a waveshape across 3 phases:
|
|
* 1. Q1 (top-left): hash-generated bars above center (left half)
|
|
* 2. Flip Q1 vertically → bottom-left (same order, below center)
|
|
* 3. Flip that horizontally → bottom-right (reversed order, below center)
|
|
* 4. Remove the bottom-left, keeping only Q1 + bottom-right
|
|
*
|
|
* Result: top-left positive bars flow into bottom-right negative bars
|
|
* (reversed), producing a single oscillator cycle across the full width.
|
|
*/
|
|
|
|
interface CreatorAvatarProps {
|
|
creatorId: string;
|
|
name: string;
|
|
imageUrl?: string | null;
|
|
size?: number;
|
|
}
|
|
|
|
function hashBytes(str: string, count: number): number[] {
|
|
const bytes: number[] = [];
|
|
let h = 0x811c9dc5;
|
|
for (let i = 0; i < str.length; i++) {
|
|
h ^= str.charCodeAt(i);
|
|
h = Math.imul(h, 0x01000193);
|
|
}
|
|
for (let i = 0; i < count; i++) {
|
|
h = Math.imul(h, 0x01000193) ^ i;
|
|
bytes.push(((h >>> 0) % 256));
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
export default function CreatorAvatar({ creatorId, name, imageUrl, size = 32 }: CreatorAvatarProps) {
|
|
if (imageUrl) {
|
|
return (
|
|
<img
|
|
src={imageUrl}
|
|
alt={name}
|
|
className="creator-avatar creator-avatar--img"
|
|
style={{ width: size, height: size }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const b = hashBytes(creatorId, 32);
|
|
const g = (i: number) => b[i] ?? 0;
|
|
|
|
const hue1 = g(0) * 1.41;
|
|
const hue2 = (hue1 + 35 + g(1) * 0.4) % 360;
|
|
const sat = 58 + (g(2) % 25);
|
|
const lum = 48 + (g(3) % 18);
|
|
const c1 = `hsl(${hue1}, ${sat}%, ${lum}%)`;
|
|
const c2 = `hsl(${hue2}, ${sat - 8}%, ${lum - 8}%)`;
|
|
|
|
// Half the bars from hash — this is Q1 (positive, left side)
|
|
const half = 11;
|
|
const total = half * 2;
|
|
const pad = 1.2;
|
|
const usable = 24 - pad * 2;
|
|
const step = usable / total;
|
|
const barW = step * 0.65;
|
|
const maxAmp = 11;
|
|
|
|
const q1: number[] = [];
|
|
for (let i = 0; i < half; i++) {
|
|
q1.push(1 + (g(4 + i) / 255) * (maxAmp - 1));
|
|
}
|
|
|
|
// Bottom-right: Q1 flipped vertically then flipped horizontally
|
|
// = reversed order, negated heights
|
|
const br = [...q1].reverse().map((h) => -h);
|
|
|
|
// Final: Q1 (positive, left) then BR (negative, right)
|
|
const allHeights = [...q1, ...br];
|
|
|
|
const gradId = `ag-${creatorId.slice(0, 8)}`;
|
|
|
|
return (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
className="creator-avatar creator-avatar--gen"
|
|
style={{ width: size, height: size }}
|
|
role="img"
|
|
aria-label={`Avatar for ${name}`}
|
|
>
|
|
<defs>
|
|
<linearGradient id={gradId} x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0%" stopColor={c1} />
|
|
<stop offset="100%" stopColor={c2} />
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="24" height="24" rx="4" fill={`url(#${gradId})`} opacity="0.1" />
|
|
{allHeights.map((h, i) => {
|
|
const x = pad + i * step + (step - barW) / 2;
|
|
const absH = Math.abs(h);
|
|
const y = h >= 0 ? 12 - absH : 12;
|
|
return (
|
|
<rect
|
|
key={i}
|
|
x={x}
|
|
y={y}
|
|
width={barW}
|
|
height={absH}
|
|
rx={barW / 2}
|
|
fill={`url(#${gradId})`}
|
|
/>
|
|
);
|
|
})}
|
|
</svg>
|
|
);
|
|
}
|