Files
hanzo.ai/components/settings/Memoji.tsx
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

77 lines
2.4 KiB
TypeScript

'use client'
/**
* A memoji of yourself: the image model draws it from a description, the same
* way the new-agent sheet draws an agent's face. It is a paid plan's and opens
* only on "Create a memoji", so General loads this on that press, and the AI
* client it builds stays off the settings page until someone asks for it.
*/
import { useState } from 'react'
import { Button, Input, XStack, YStack, toast } from '@hanzo/ui'
import { useAi } from '@hanzo/app/lib/ai'
import { mark } from '@hanzo/app/lib/mark'
import { say } from '@hanzo/app/lib/org'
export default function Memoji({
busy,
onDrawn,
onCancel,
}: {
/** The profile is saving, so nothing else may start. */
busy: boolean
/** The drawn picture, marked and ready to save as the avatar. */
onDrawn: (image: string) => Promise<void>
onCancel: () => void
}) {
const { client } = useAi()
const [look, setLook] = useState('')
const [drawing, setDrawing] = useState(false)
const held = busy || drawing
const draw = async () => {
if (!client || !look.trim()) return
setDrawing(true)
try {
const out = await client.http.json<{ data?: { b64_json?: string }[] }>({
method: 'POST',
path: '/v1/images/generations',
body: {
prompt: `${look.trim()}, memoji style 3D emoji portrait, head and shoulders, centered, plain background`,
n: 1,
size: '512x512',
response_format: 'b64_json',
},
})
const b64 = out?.data?.[0]?.b64_json
if (!b64) throw new Error('The image model answered with no picture.')
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
await onDrawn(await mark(new File([bytes], 'memoji.png', { type: 'image/png' })))
} catch (e) {
toast.error(say(e))
} finally {
setDrawing(false)
}
}
return (
<XStack gap="$2" items="center" flexWrap="wrap" maxW={576}>
<YStack flex={1} minW={240}>
<Input
aria-label="Describe your memoji"
value={look}
onChange={(e) => setLook(e.target.value)}
placeholder="Your look: hair, glasses, clothes, colours"
autoFocus
/>
</YStack>
<Button size="sm" disabled={held || !look.trim()} onClick={() => void draw()}>
{held ? 'Creating…' : 'Create'}
</Button>
<Button variant="ghost" size="sm" disabled={held} onClick={onCancel}>
Cancel
</Button>
</XStack>
)
}