mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
77 lines
2.4 KiB
TypeScript
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>
|
|
)
|
|
}
|