Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
yilsem committed 2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 files changed
+3442369

No files matched your search

@@ -0,0 +1,276 @@
/**
* The Pet tab: browse the petdex gallery and use a companion's first sprite
* frame as the bot's profile picture.
*/
import { Button, cn, GlyphSpinner, host, Input, LruCache, RowButton, useQuery } from '@hermes/plugin-sdk'
import { useEffect, useState } from 'react'
import { useBots } from './i18n'
import { ID } from './shared'
// ── pet tab: attach a petdex companion that lives beside the avatar ─────────
// A petdex "spritesheet" is the FULL animation sheet (1536×1872 webp, ~2MB;
// 8×9 grid of 192×208 frames). Using it as an <img> both downloads megabytes
// per tile and shows the whole sheet squashed. Extract frame 0 once per slug
// via canvas, downscale to 96px, and cache the data URL. Concurrency-capped
// so opening the tab doesn't fire dozens of 2MB fetches at once.
const PET_FRAME_W = 192
const PET_FRAME_H = 208
// The gallery is 4500+ pets browsed 24 at a time, and each entry is a decoded
// PNG data URL — an unbounded cache holds every pet the user ever scrolled
// past for the life of the window. Five pages' worth keeps scrolling back up
// instant; past that a revisit pays the fetch and crop again.
const PET_FRAME_CACHE_MAX = 120
const petFrameCache = new LruCache<string, Promise<null | string>>(PET_FRAME_CACHE_MAX)
let petFetchActive = 0
const petFetchQueue: Array<() => Promise<void>> = []
function pumpPetQueue() {
while (petFetchActive < 4 && petFetchQueue.length) {
const job = petFetchQueue.shift()!
petFetchActive++
job().finally(() => {
petFetchActive--
pumpPetQueue()
})
}
}
function petFrameIcon(spriteUrl: null | string | undefined): Promise<null | string> {
if (!spriteUrl) {
return Promise.resolve(null)
}
if (!petFrameCache.has(spriteUrl)) {
petFrameCache.set(
spriteUrl,
new Promise(resolve => {
petFetchQueue.push(async () => {
try {
const resp = await fetch(spriteUrl, {
signal: AbortSignal.timeout(15000)
})
const blob = await resp.blob()
// Crop frame 0 during decode — never materialize the full sheet.
const bitmap = await createImageBitmap(blob, 0, 0, PET_FRAME_W, PET_FRAME_H)
const canvas = document.createElement('canvas')
canvas.width = 96
canvas.height = 104
canvas.getContext('2d')!.drawImage(bitmap, 0, 0, 96, 104)
bitmap.close()
resolve(canvas.toDataURL('image/png'))
} catch {
petFrameCache.delete(spriteUrl)
resolve(null)
}
})
pumpPetQueue()
})
)
}
return petFrameCache.get(spriteUrl)!
}
interface PetThumbProps {
size?: number
spriteUrl?: null | string
}
/** One pet tile image: frame 0 only, resolved lazily through the cache. */
function PetThumb({ spriteUrl, size = 40 }: PetThumbProps) {
const [icon, setIcon] = useState<null | string>(null)
useEffect(() => {
let alive = true
petFrameIcon(spriteUrl).then(url => {
if (alive) {
setIcon(url)
}
})
return () => {
alive = false
}
}, [spriteUrl])
if (!icon) {
return (
<div
style={{
width: size,
height: size,
borderRadius: 6,
background: 'var(--chrome-action-hover, rgba(255,255,255,0.06))'
}}
/>
)
}
return (
<img
alt=""
src={icon}
style={{
width: size,
height: size,
objectFit: 'contain',
imageRendering: 'pixelated',
borderRadius: 6
}}
/>
)
}
/** One petdex companion as `pet.gallery` reports it. */
interface PetGalleryEntry {
curated?: boolean
displayName?: string
installed?: boolean
slug: string
/** Full animation sheet (1536×1872 webp); frame 0 is cropped out of it. */
spritesheetUrl?: null | string
}
interface PetTabProps {
image: null | string
onImage: (image: null | string) => void
}
export function PetTab({ image, onImage }: PetTabProps) {
const b = useBots()
// Selection is dialog-local: committed by the dialog's Save like any
// uploaded/generated image (a direct meta write here gets clobbered by
// Save's own image state).
const [selectedSlug, setSelectedSlug] = useState<null | string>(null)
const { data, isLoading } = useQuery({
queryKey: [ID, 'pet-gallery'],
queryFn: () => host.request<{ pets?: PetGalleryEntry[] }>('pet.gallery', {}),
staleTime: 300000
})
const [query, setQuery] = useState('')
// Windowed rendering: the gallery is 4500+ pets — mounting an <img> per pet
// froze the dialog. Render `limit` at a time and grow on scroll-to-bottom.
const [limit, setLimit] = useState(24)
const pets = data?.pets ?? []
if (isLoading) {
return (
<div className="flex justify-center py-4">
<GlyphSpinner className="text-(--ui-text-tertiary)" spinner="breathe" />
</div>
)
}
if (!pets.length) {
return (
<div className="px-2 py-3 text-center text-xs text-(--ui-text-tertiary)">
No pets in the petdex gallery. Run `hermes pets` to explore.
</div>
)
}
const q = query.trim().toLowerCase()
const filtered = q
? pets.filter(pet => (pet.displayName || '').toLowerCase().includes(q) || (pet.slug || '').includes(q))
: pets
// Installed and curated pets surface first — they're the likeliest picks.
const ranked = filtered.slice().sort((a, b) => {
const rank = (pet: PetGalleryEntry) => (pet.installed ? 0 : pet.curated ? 1 : 2)
return rank(a) - rank(b)
})
const visible = ranked.slice(0, limit)
const onScroll = (event: { currentTarget: HTMLDivElement }) => {
const el = event.currentTarget
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 120 && limit < ranked.length) {
setLimit(prev => Math.min(prev + 24, ranked.length))
}
}
return (
<div className="grid w-full gap-2">
<div className="text-center text-[0.65rem] text-(--ui-text-quaternary)">{b.avatar.pickPet}</div>
<Input
className="h-7 text-xs"
onChange={event => {
setQuery(event.target.value)
setLimit(24)
}}
placeholder={`Search ${pets.length} pets…`}
value={query}
/>
{image && selectedSlug ? (
<Button
className="justify-center"
onClick={() => {
setSelectedSlug(null)
onImage(null)
}}
size="sm"
type="button"
variant="ghost"
>
{b.avatar.removeBackToShape}
</Button>
) : null}
{filtered.length === 0 ? (
<div className="py-3 text-center text-xs text-(--ui-text-quaternary)">No pets match.</div>
) : (
<div
className="overflow-y-auto"
onScroll={onScroll}
style={{
maxHeight: 220
}}
>
<div className="grid grid-cols-3 gap-1.5">
{visible.map(pet => (
<RowButton
className={cn(
'grid justify-items-center gap-1 rounded-md p-1.5 transition-colors hover:bg-(--chrome-action-hover)',
selectedSlug === pet.slug && 'ring-1 ring-(--ui-accent)'
)}
key={pet.slug}
onClick={() => {
// The pet IS the profile picture: extract frame 0
// and hand it to the dialog as the avatar image.
// Persisted when the user hits Save.
setSelectedSlug(pet.slug)
void petFrameIcon(pet.spritesheetUrl).then(icon => {
if (icon) {
onImage(icon)
} else {
setSelectedSlug(null)
host.notify({
kind: 'error',
message: b.avatar.petLoadFailed
})
}
})
}}
>
<PetThumb size={40} spriteUrl={pet.spritesheetUrl} />
<span className="w-full truncate text-center text-[0.6rem] text-(--ui-text-tertiary)">
{pet.displayName}
</span>
</RowButton>
))}
</div>
{limit < ranked.length ? (
<div className="py-2 text-center text-[0.65rem] text-(--ui-text-quaternary)">{`Scroll for more (${limit} of ${ranked.length})`}</div>
) : null}
</div>
)}
</div>
)
}