diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index dcd75f3..5a5d0ee 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from "react"; import { View } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; -import { AvatarStyleProvider } from "../components/avatar-style"; import { ComputerUpdateProgress } from "../components/computer-update-progress"; import { currentApiBase, loadApiBase, loadSessionToken, selectedSpaceId } from "../lib/api"; import { loadAppearancePreference, mobileTokens } from "../lib/appearance"; @@ -57,74 +56,72 @@ export default function Layout() { {ready ? ( - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + /> + + + + + + + + + + + + + + + ) : ( )} diff --git a/apps/mobile/app/account.tsx b/apps/mobile/app/account.tsx index 7c10439..c549143 100644 --- a/apps/mobile/app/account.tsx +++ b/apps/mobile/app/account.tsx @@ -1,4 +1,3 @@ -import type { AvatarStyle } from "@rakazo/contracts"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; import { @@ -15,8 +14,6 @@ import { View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { useAvatarStyle } from "../components/avatar-style"; -import { BotAvatar } from "../components/bot-avatar"; import type { MobileBot, MobileMe } from "../lib/api"; import { currentApiBase, @@ -61,8 +58,6 @@ export default function Account() { const [localeSaving, setLocaleSaving] = useState(false); const [localeError, setLocaleError] = useState(null); const [pending, setPending] = useState(false); - const [avatarPending, setAvatarPending] = useState(false); - const [avatarError, setAvatarError] = useState(null); const [notifications, setNotifications] = useState( DEFAULT_LIVE_NOTIFICATION_SETTINGS, ); @@ -76,7 +71,6 @@ export default function Account() { inputTokens: number; outputTokens: number; } | null>(null); - const { avatarStyle, updateAvatarStyle } = useAvatarStyle(); const appearance = getCachedAppearancePreference(); const styles = useThemedStyles(createAccountStyles); const versionInfo = getAppVersionInfo(); @@ -127,19 +121,6 @@ export default function Account() { } } - async function selectAvatarStyle(next: AvatarStyle) { - if (next === avatarStyle) return; - setAvatarPending(true); - setAvatarError(null); - try { - await updateAvatarStyle(next); - } catch { - setAvatarError(t("Couldn't update avatars")); - } finally { - setAvatarPending(false); - } - } - async function handleSignOut() { setPending(true); setError(null); @@ -260,7 +241,7 @@ export default function Account() { › - + {t("Appearance")} {( @@ -292,40 +273,6 @@ export default function Account() { - - {t("Avatars")} - - {(["robot", "organic"] as const).map((style) => { - const selected = avatarStyle === style; - const styleLabel = style === "robot" ? t("Robot") : t("Organic"); - return ( - void selectAvatarStyle(style)} - style={({ pressed }) => [ - styles.avatarOption, - selected && styles.avatarOptionSelected, - pressed && styles.pressed, - ]} - > - - {styleLabel} - - ); - })} - - {avatarError ? {avatarError} : null} - - Promise; -}>({ - avatarStyle: "robot", - updateAvatarStyle: async () => undefined, -}); - -export function AvatarStyleProvider({ children }: { children: ReactNode }) { - const [avatarStyle, setAvatarStyle] = useState("robot"); - const pathname = usePathname(); - const requestIdRef = useRef(0); - const updatePromiseRef = useRef | null>(null); - - useEffect(() => { - const requestId = ++requestIdRef.current; - void rpc("me") - .then((me) => { - if (requestId !== requestIdRef.current) return; - setAvatarStyle(me.avatarStyle); - }) - .catch(() => undefined); - }, [pathname]); - - function updateAvatarStyle(next: AvatarStyle): Promise { - if (updatePromiseRef.current) return updatePromiseRef.current; - const requestId = ++requestIdRef.current; - const update = rpc("preferences/update", { avatarStyle: next }) - .then((me) => { - if (requestId !== requestIdRef.current) return; - setAvatarStyle(me.avatarStyle); - }) - .finally(() => { - updatePromiseRef.current = null; - }); - updatePromiseRef.current = update; - return update; - } - - return ( - {children} - ); -} - -export function useAvatarStyle() { - return useContext(AvatarStyleContext); -} diff --git a/apps/mobile/components/bot-avatar.tsx b/apps/mobile/components/bot-avatar.tsx index a6c1ca7..4e3eb43 100644 --- a/apps/mobile/components/bot-avatar.tsx +++ b/apps/mobile/components/bot-avatar.tsx @@ -1,95 +1,58 @@ -import type { AvatarStyle } from "@rakazo/contracts"; +import type { OoMoment } from "@rakazo/core"; import { ACTIVE_RUN_STATUSES, - avatarIdentitySeed, DEFAULT_OO_AVATAR, - ooAvatarColor, - organicAvatarPath, + decodeOoAvatar, + ooIdentityShapeIndex, + ooMomentForStatus, + resolveOoColor, } from "@rakazo/core"; -import { memo, useEffect } from "react"; -import { View } from "react-native"; -import Animated, { - cancelAnimation, - Easing, - useAnimatedProps, - useAnimatedStyle, - useReducedMotion, - useSharedValue, - withRepeat, - withTiming, -} from "react-native-reanimated"; -import Svg, { G, Path, Rect } from "react-native-svg"; -import { workingAvatarDuration, workingAvatarFrame } from "../lib/avatar-motion"; +import { memo, useMemo } from "react"; +import { Image, View } from "react-native"; import { useI18n } from "../lib/i18n"; -import { useAvatarStyle } from "./avatar-style"; import { NativeSymbol } from "./native-symbol"; - -const AnimatedRect = Animated.createAnimatedComponent(Rect); +import { OoFigure } from "./oo-figure"; export const BotAvatar = memo(function BotAvatar({ color, size = 54, status, identity, - variant, + moment, + still, muted = false, }: { color: string; size?: number; status?: string; identity?: string; - variant?: AvatarStyle; + /** Explicit moment; wins over `status`. */ + moment?: OoMoment; + still?: boolean; muted?: boolean; }) { const { t } = useI18n(); const isWorking = ACTIVE_RUN_STATUSES.some((activeStatus) => activeStatus === status); - const { avatarStyle } = useAvatarStyle(); - const fill = ooAvatarColor(color) || DEFAULT_OO_AVATAR.color; - const visorW = Math.round(size * 0.68); - const visorH = Math.round(size * 0.44); - const eyeW = Math.max(3, Math.round(size * 0.11)); - const eyeH = Math.max(4, Math.round(size * 0.17)); - const gap = Math.max(3, Math.round(size * 0.11)); + const picture = color.startsWith("data:image/") ? color : null; + const look = useMemo(() => decodeOoAvatar(color) ?? DEFAULT_OO_AVATAR, [color]); + const seed = identity || (picture ? "agent" : color) || "agent"; + const body = resolveOoColor(seed, look.color).hex; return ( - {(variant ?? avatarStyle) === "organic" ? ( - + {picture ? ( + ) : ( - - - {[0, 1].map((eye) => ( - - ))} - - + )} {isWorking ? ( ); }); - -function OrganicAvatar({ - color, - identity, - size, - isWorking, -}: { - color: string; - identity?: string; - size: number; - isWorking: boolean; -}) { - const seed = avatarIdentitySeed(identity || color || "#8B5CF6"); - const progress = useSharedValue(0); - const reducedMotion = useReducedMotion(); - - useEffect(() => { - cancelAnimation(progress); - progress.value = 0; - if (isWorking && !reducedMotion) { - progress.value = withRepeat( - withTiming(1, { - duration: workingAvatarDuration(seed), - easing: Easing.linear, - }), - -1, - ); - } - return () => cancelAnimation(progress); - }, [isWorking, progress, reducedMotion, seed]); - - const bodyStyle = useAnimatedStyle(() => { - const frame = workingAvatarFrame(seed, progress.value); - return { - transform: [ - { translateX: (frame.translationX * size) / 120 }, - { translateY: (frame.translationY * size) / 120 }, - { rotate: `${frame.rotation}deg` }, - { scaleX: frame.scaleX }, - { scaleY: frame.scaleY }, - ], - }; - }); - const leftEyeProps = useAnimatedProps(() => { - const frame = workingAvatarFrame(seed, progress.value); - return { x: -14 + frame.eyeOffsetX, y: -12 + frame.eyeOffsetY }; - }); - const rightEyeProps = useAnimatedProps(() => { - const frame = workingAvatarFrame(seed, progress.value); - return { x: 7 + frame.eyeOffsetX, y: -12 + frame.eyeOffsetY }; - }); - - return ( - - - - - - - - - - - - ); -} diff --git a/apps/mobile/components/oo-figure.tsx b/apps/mobile/components/oo-figure.tsx new file mode 100644 index 0000000..9adcda8 --- /dev/null +++ b/apps/mobile/components/oo-figure.tsx @@ -0,0 +1,218 @@ +import type { OoAvatar, OoFrame, OoLife, OoMoment, OoPaint, OoShape } from "@rakazo/core"; +import { + buildOoRig, + createOoLife, + OO_LIST_MOMENTS, + OO_RIG_INSET, + OO_RIG_SIZE, + OO_STILL_TIME, + ooFrame, + ooLodForSize, + ooSeededRandom, +} from "@rakazo/core"; +import type { ReactNode } from "react"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; +import { useReducedMotion } from "react-native-reanimated"; +import Svg, { + Circle, + Defs, + Ellipse, + G, + LinearGradient, + Path, + RadialGradient, + Rect, + Stop, +} from "react-native-svg"; + +const VIEWBOX = `${OO_RIG_INSET} ${OO_RIG_INSET + 4} ${OO_RIG_SIZE - 2 * OO_RIG_INSET} ${OO_RIG_SIZE - 2 * OO_RIG_INSET}`; +/** Native frames redraw the SVG tree, so they run at half the display rate. */ +const FRAME_SECONDS = 1 / 30; +/** Above this size an Oo always moves; smaller ones only for list moments. */ +const LIVELY_SIZE = 56; + +type Tick = (now: number) => void; +const ticks = new Set(); +let frameRequest = 0; + +function loop(now: number) { + for (const tick of ticks) tick(now / 1000); + frameRequest = ticks.size ? requestAnimationFrame(loop) : 0; +} + +function subscribe(tick: Tick): () => void { + ticks.add(tick); + if (!frameRequest) frameRequest = requestAnimationFrame(loop); + return () => { + ticks.delete(tick); + }; +} + +function paintProps(paint: OoPaint, gradient: string) { + const fill = + paint.fill === "$body" + ? `url(#${gradient}-body)` + : paint.fill === "$shine" + ? `url(#${gradient}-shine)` + : paint.fill; + return { + fill: fill ?? (paint.stroke ? "none" : undefined), + stroke: paint.stroke, + strokeWidth: paint.strokeWidth, + opacity: paint.opacity, + fillOpacity: paint.fillOpacity, + strokeOpacity: paint.strokeOpacity, + strokeLinecap: paint.round ? ("round" as const) : undefined, + strokeLinejoin: paint.round ? ("round" as const) : undefined, + strokeDasharray: paint.dash, + }; +} + +function draw( + shape: OoShape, + key: number, + gradient: string, + frame: OoFrame, + mouthColor: string, +): ReactNode { + switch (shape.type) { + case "path": + return ; + case "circle": + return ( + + ); + case "ellipse": + return ( + + ); + case "rect": + return ( + + ); + case "group": { + const { part, fx } = shape; + if (part === "mouth") { + return ( + + ); + } + if (part === "tongue") { + return frame.tongue ? : null; + } + const state = fx + ? frame.fx(fx.kind, fx.index) + : part && part !== "fx" + ? frame.parts[part] + : undefined; + if (state?.hidden ?? shape.hidden) return null; + return ( + + {shape.children.map((child, index) => draw(child, index, gradient, frame, mouthColor))} + + ); + } + } +} + +export function OoFigure({ + avatar, + color, + fallbackShape, + moment, + size, + still = false, +}: { + avatar: OoAvatar; + color: string; + fallbackShape: number; + moment: OoMoment; + size: number; + still?: boolean; +}) { + const gradient = `oo${useId().replace(/[^a-zA-Z0-9-_]/g, "")}`; + const lod = ooLodForSize(size); + const rig = useMemo( + () => buildOoRig({ avatar, color, fallbackShape, moment, lod }), + [avatar, color, fallbackShape, moment, lod], + ); + const reducedMotion = useReducedMotion(); + const lively = + !still && !reducedMotion && (size > LIVELY_SIZE || OO_LIST_MOMENTS.includes(moment)); + const lifeRef = useRef(null); + const [frame, setFrame] = useState(null); + + useEffect(() => { + setFrame(null); + if (!lively) return; + if (!lifeRef.current) lifeRef.current = createOoLife(); + const life = lifeRef.current; + const start = performance.now() / 1000 - Math.random() * 10; + let last = 0; + return subscribe((now) => { + if (now - last < FRAME_SECONDS) return; + last = now; + setFrame(ooFrame({ rig, avatar, moment, t: now - start, life })); + }); + }, [rig, avatar, moment, lively]); + + const stillFrame = useMemo( + () => + ooFrame({ + rig, + avatar, + moment, + t: OO_STILL_TIME, + life: createOoLife(ooSeededRandom(1)), + still: true, + }), + [rig, avatar, moment], + ); + const shown = frame ?? stillFrame; + const { colors } = rig; + return ( + + + + + + + + + + + + + {rig.shapes.map((shape, index) => draw(shape, index, gradient, shown, rig.mouthColor))} + + ); +} diff --git a/apps/mobile/lib/avatar-motion.test.ts b/apps/mobile/lib/avatar-motion.test.ts deleted file mode 100644 index e0fed1c..0000000 --- a/apps/mobile/lib/avatar-motion.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { workingAvatarDuration, workingAvatarFrame } from "@rakazo/core"; -import { describe, expect, it } from "vitest"; - -describe("working avatar motion", () => { - it("reuses shared core choreography", () => { - const start = workingAvatarFrame(0, 0); - const end = workingAvatarFrame(0, 1); - expect(end.translationY).toBeCloseTo(start.translationY); - expect(end.scaleX).toBeCloseTo(start.scaleX); - expect(end.eyeOffsetX).toBeCloseTo(start.eyeOffsetX); - expect(end.eyeOffsetY).toBeCloseTo(start.eyeOffsetY); - expect(workingAvatarFrame(0, 0.5)).not.toEqual(workingAvatarFrame(0, 0)); - expect(workingAvatarFrame(2, 0.5)).not.toEqual(workingAvatarFrame(0, 0.5)); - expect(workingAvatarDuration(6)).toBe(1100); - }); -}); diff --git a/apps/mobile/lib/avatar-motion.ts b/apps/mobile/lib/avatar-motion.ts deleted file mode 100644 index 56eb6a4..0000000 --- a/apps/mobile/lib/avatar-motion.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Re-export shared choreography for Reanimated worklets. - * Source of truth: `@rakazo/core` (`avatar-motion.ts`), mirrored by web CSS. - */ -export { - WORKING_AVATAR_DURATIONS_MS, - type WorkingAvatarFrame, - workingAvatarDuration, - workingAvatarFrame, -} from "@rakazo/core"; diff --git a/apps/mobile/lib/locales/ru.ts b/apps/mobile/lib/locales/ru.ts index 50d8864..4e19a40 100644 --- a/apps/mobile/lib/locales/ru.ts +++ b/apps/mobile/lib/locales/ru.ts @@ -75,7 +75,6 @@ export const RU_MESSAGES: Record = { "{name} is working": "{name} работает", "{name}’s computer": "Компьютер {name}", "{runs} runs · {tokens} tokens": "Запусков: {runs} · токенов: {tokens}", - "{style} avatars": "Аватары: {style}", "@{name}": "@{name}", "API key": "API-ключ", Account: "Аккаунт", @@ -122,8 +121,6 @@ export const RU_MESSAGES: Record = { "Authorization timed out. Please try again.": "Время авторизации истекло. Пожалуйста, попробуйте еще раз.", "Authorize {name}": "Авторизовать {name}", - Avatars: "Аватары", - "Avatar style": "Стиль аватара", "Bearer token": "Bearer-токен", "Back to sign in": "Вернуться ко входу", Back: "Назад", @@ -233,7 +230,6 @@ export const RU_MESSAGES: Record = { "Could not update computer": "Не удалось обновить компьютер", "Could not update notifications": "Не удалось обновить уведомления.", "Could not update reaction": "Не удалось обновить реакцию", - "Couldn't update avatars": "Не удалось обновить аватары", Create: "Создать", "Create group": "Создать группу", "Create section": "Создать раздел", @@ -366,7 +362,6 @@ export const RU_MESSAGES: Record = { "OpenAI-compatible server URL": "URL-адрес сервера, совместимого с OpenAI", Optional: "Необязательно", "Or connect an API key": "Или подключите API-ключ", - Organic: "Органичный", "Other model…": "Другая модель…", Password: "Пароль", "Password recovery is not configured for this server": @@ -414,7 +409,6 @@ export const RU_MESSAGES: Record = { "Восстановить последнее сохранённое рабочее пространство. Несохранённые данные на компьютере будут потеряны.", "Resume notifications": "Возобновить уведомления", "Return to inbox": "Вернуться во входящие", - Robot: "Робот", Routine: "Задача", "Routine link is incomplete": "Ссылка на задачу неполная", Running: "Выполняется", diff --git a/apps/mobile/lib/locales/zh.ts b/apps/mobile/lib/locales/zh.ts index 03f37fe..771c5b8 100644 --- a/apps/mobile/lib/locales/zh.ts +++ b/apps/mobile/lib/locales/zh.ts @@ -63,7 +63,6 @@ export const ZH_MESSAGES: Record = { "{name} is working": "{name} 正在工作", "{name}’s computer": "{name} 的电脑", "{runs} runs · {tokens} tokens": "{runs} 次运行 · {tokens} 个 token", - "{style} avatars": "{style} 头像", "@{name}": "@{name}", "API key": "API 密钥", Account: "账户", @@ -110,8 +109,6 @@ export const ZH_MESSAGES: Record = { "Authorization code": "授权码", "Authorization timed out. Please try again.": "授权超时,请重试。", "Authorize {name}": "授权 {name}", - Avatars: "头像", - "Avatar style": "头像风格", "Bearer token": "Bearer 令牌", "Back to sign in": "返回登录", Back: "返回", @@ -218,7 +215,6 @@ export const ZH_MESSAGES: Record = { "Could not update computer": "无法更新电脑", "Could not update notifications": "无法更新通知", "Could not update reaction": "无法更新反应", - "Couldn't update avatars": "无法更新头像", Create: "创建", "Create group": "创建群组", "Create section": "创建分组", @@ -350,7 +346,6 @@ export const ZH_MESSAGES: Record = { "OpenAI-compatible server URL": "OpenAI 兼容服务器地址", Optional: "可选", "Or connect an API key": "或连接 API 密钥", - Organic: "有机", "Other model…": "其他模型…", Password: "密码", "Password recovery is not configured for this server": "此服务器未配置密码找回", @@ -395,7 +390,6 @@ export const ZH_MESSAGES: Record = { "恢复上次保存的工作区。电脑上未保存的工作将会丢失。", "Resume notifications": "恢复通知", "Return to inbox": "返回收件箱", - Robot: "机器人", Routine: "例行任务", "Routine link is incomplete": "例行任务链接不完整", Running: "运行中", diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index 8b3b5e0..80ae231 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -756,10 +756,6 @@ msgstr "Verfügbar, sobald die Routine gespeichert ist" msgid "Avatar Studio" msgstr "Avatar-Studio" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "Avatare" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "Messaging-Einstellungen konnten nicht geladen werden" msgid "Couldn't save" msgstr "Konnte nicht gespeichert werden" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "Avatare konnten nicht aktualisiert werden" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "Messaging-Einstellungen konnten nicht aktualisiert werden" @@ -3510,10 +3502,6 @@ msgstr "Optionaler Header-Wert" msgid "Or connect an API key" msgstr "Oder einen API-Schlüssel verbinden" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "Organisch" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "Organisations-API-Schlüssel" @@ -4025,10 +4013,6 @@ msgstr "Widerrufen" msgid "Riffing" msgstr "Am Riffen" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "Roboter" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "Hinweise für diesen Raum" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index e3f5100..a1f4f1c 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -756,10 +756,6 @@ msgstr "Available after the routine is saved" msgid "Avatar Studio" msgstr "Avatar Studio" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "Avatars" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "Couldn't load messaging settings" msgid "Couldn't save" msgstr "Couldn't save" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "Couldn't update avatars" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "Couldn't update messaging settings" @@ -3510,10 +3502,6 @@ msgstr "Optional header value" msgid "Or connect an API key" msgstr "Or connect an API key" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "Organic" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "Organization API key" @@ -4025,10 +4013,6 @@ msgstr "Revoke" msgid "Riffing" msgstr "Riffing" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "Robot" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "Room guidance" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index ab5d3b3..8adf09d 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -756,10 +756,6 @@ msgstr "Disponible después de guardar la rutina" msgid "Avatar Studio" msgstr "Estudio de avatares" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "Avatares" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "No se pudo cargar la configuración de mensajería" msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "No se pudieron actualizar los avatares" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "No se pudo actualizar la configuración de mensajería" @@ -3510,10 +3502,6 @@ msgstr "Valor de encabezado opcional" msgid "Or connect an API key" msgstr "O conecta una clave de API" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "Orgánico" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "Clave de API de organización" @@ -4025,10 +4013,6 @@ msgstr "Revocar" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "Robot" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index 33700b3..0b578b6 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -756,10 +756,6 @@ msgstr "" msgid "Avatar Studio" msgstr "अवतार स्टूडियो" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "" msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "" @@ -3510,10 +3502,6 @@ msgstr "वैकल्पिक हेडर मान" msgid "Or connect an API key" msgstr "या एक API कुंजी कनेक्ट करें" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "संगठन API कुंजी" @@ -4025,10 +4013,6 @@ msgstr "" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index 8d02744..5ba9c66 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -756,10 +756,6 @@ msgstr "루틴을 저장한 후 사용할 수 있습니다" msgid "Avatar Studio" msgstr "아바타 스튜디오" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "아바타" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "메시징 설정을 불러올 수 없습니다" msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "아바타를 업데이트할 수 없습니다" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "메시징 설정을 업데이트할 수 없습니다" @@ -3510,10 +3502,6 @@ msgstr "선택 사항인 헤더 값" msgid "Or connect an API key" msgstr "또는 API 키 연결" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "자연스럽게" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "조직 API 키" @@ -4025,10 +4013,6 @@ msgstr "연결 해제" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "로봇" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index b48fefd..b305d71 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -756,10 +756,6 @@ msgstr "" msgid "Avatar Studio" msgstr "Estúdio de avatar" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "" msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "" @@ -3510,10 +3502,6 @@ msgstr "Valor de cabeçalho opcional" msgid "Or connect an API key" msgstr "Ou conecte uma chave de API" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "Chave da API da organização" @@ -4025,10 +4013,6 @@ msgstr "" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 860d4b7..f42fe89 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -756,10 +756,6 @@ msgstr "Доступно после сохранения задачи" msgid "Avatar Studio" msgstr "Студия аватаров" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "Аватары" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "Не удалось загрузить настройки обмена msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "Не удалось обновить аватары" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "Не удалось обновить настройки обмена сообщениями." @@ -3510,10 +3502,6 @@ msgstr "Необязательное значение заголовка" msgid "Or connect an API key" msgstr "Или подключить API-ключ" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "Органичный" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "Ключ API организации" @@ -4025,10 +4013,6 @@ msgstr "Отозвать" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "Робот" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "Инструкции для комнаты" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index 635d728..45a906b 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -756,10 +756,6 @@ msgstr "" msgid "Avatar Studio" msgstr "Avatar Stüdyosu" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "" msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "" @@ -3510,10 +3502,6 @@ msgstr "İsteğe bağlı üstbilgi değeri" msgid "Or connect an API key" msgstr "Veya bir API anahtarı bağla" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "Kuruluş API anahtarı" @@ -4025,10 +4013,6 @@ msgstr "İptal et" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index 4ab22f8..7251cb0 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -756,10 +756,6 @@ msgstr "保存例行任务后可用" msgid "Avatar Studio" msgstr "头像工作室" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Avatars" -msgstr "头像" - #: src/components/browser/InAppBrowser.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx #: src/pages/Onboarding.tsx @@ -1748,10 +1744,6 @@ msgstr "无法加载消息设置" msgid "Couldn't save" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Couldn't update avatars" -msgstr "无法更新头像" - #: src/pages/MessagingSettingsOverlay.tsx msgid "Couldn't update messaging settings" msgstr "无法更新消息设置" @@ -3510,10 +3502,6 @@ msgstr "可选请求头值" msgid "Or connect an API key" msgstr "或连接 API 密钥" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Organic" -msgstr "自然" - #: src/pages/memory-providers/SupermemorySettingsForm.tsx msgid "Organization API key" msgstr "组织 API 密钥" @@ -4025,10 +4013,6 @@ msgstr "撤销" msgid "Riffing" msgstr "" -#: src/pages/AccountSettingsOverlay.tsx -msgid "Robot" -msgstr "机器人" - #: src/pages/ExternalConversationSettings.tsx msgid "Room guidance" msgstr "" diff --git a/apps/web/src/pages/AccountSettingsOverlay.tsx b/apps/web/src/pages/AccountSettingsOverlay.tsx index d222a72..c6dd604 100644 --- a/apps/web/src/pages/AccountSettingsOverlay.tsx +++ b/apps/web/src/pages/AccountSettingsOverlay.tsx @@ -1,6 +1,5 @@ import { Trans, useLingui } from "@lingui/react/macro"; -import type { AvatarStyle } from "@rakazo/contracts"; -import { BotAvatar, Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web"; +import { Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web"; import { ChevronDown } from "lucide-react"; import { type KeyboardEvent as ReactKeyboardEvent, @@ -28,8 +27,6 @@ import { UI_LOCALE_LABELS, UI_LOCALES, type UiLocale } from "../lib/ui-locale"; export type SettingsGeneralProps = { email?: string | null; name: string; - avatarStyle: AvatarStyle; - onAvatarStyleChange: (style: AvatarStyle) => Promise; messagingEnabled?: boolean; onOpenMessaging?: () => void; isDeploymentOwner?: boolean; @@ -38,20 +35,15 @@ export type SettingsGeneralProps = { export function GeneralSettingsPanels({ email, name, - avatarStyle, - onAvatarStyleChange, messagingEnabled = false, onOpenMessaging, isDeploymentOwner = false, }: SettingsGeneralProps) { - const { t } = useLingui(); const [locale, setLocale] = useState(() => getActiveUiLocale()); const localeRequestRef = useRef(0); const [appearance, setAppearance] = useState(() => getUiAppearancePreference(), ); - const [avatarPending, setAvatarPending] = useState(false); - const [avatarError, setAvatarError] = useState(null); function chooseLocale(next: UiLocale) { if (next === locale) return; @@ -63,19 +55,6 @@ export function GeneralSettingsPanels({ }); } - async function chooseAvatarStyle(next: AvatarStyle) { - if (avatarPending || next === avatarStyle) return; - setAvatarPending(true); - setAvatarError(null); - try { - await onAvatarStyleChange(next); - } catch { - setAvatarError(t`Couldn't update avatars`); - } finally { - setAvatarPending(false); - } - } - return (
@@ -122,37 +101,6 @@ export function GeneralSettingsPanels({
-
-

- Avatars -

-
- {(["robot", "organic"] as const).map((style) => ( - void chooseAvatarStyle(style)} - className="h-auto justify-start gap-3 px-3.5 py-3 text-ui-body font-normal" - > - - {style === "robot" ? Robot : Organic} - - ))} -
- {avatarError ? ( -

- {avatarError} -

- ) : null} -
- {isDeploymentOwner ? (
); - return {shell}; + return shell; } diff --git a/packages/core/src/avatar-motion.test.ts b/packages/core/src/avatar-motion.test.ts deleted file mode 100644 index adfc12e..0000000 --- a/packages/core/src/avatar-motion.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { workingAvatarDuration, workingAvatarFrame } from "./avatar-motion.js"; - -describe("working avatar motion", () => { - it("loops cleanly while keeping identity-specific choreography", () => { - const start = workingAvatarFrame(0, 0); - const end = workingAvatarFrame(0, 1); - expect(end.translationY).toBeCloseTo(start.translationY); - expect(end.scaleX).toBeCloseTo(start.scaleX); - expect(end.eyeOffsetX).toBeCloseTo(start.eyeOffsetX); - expect(end.eyeOffsetY).toBeCloseTo(start.eyeOffsetY); - expect(workingAvatarFrame(0, 0.5)).not.toEqual(workingAvatarFrame(0, 0)); - expect(workingAvatarFrame(2, 0.5)).not.toEqual(workingAvatarFrame(0, 0.5)); - expect(workingAvatarDuration(6)).toBe(1100); - }); -}); diff --git a/packages/core/src/avatar-motion.ts b/packages/core/src/avatar-motion.ts deleted file mode 100644 index b57d55f..0000000 --- a/packages/core/src/avatar-motion.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Shared working-avatar choreography used by mobile (Reanimated) and mirrored - * by web CSS in `@rakazo/ui-web` (`styles.css` organic working keyframes). - * - * Duration families and transform midpoints must stay aligned across surfaces. - */ - -export interface WorkingAvatarFrame { - translationX: number; - translationY: number; - scaleX: number; - scaleY: number; - rotation: number; - eyeOffsetX: number; - eyeOffsetY: number; -} - -/** Per shape-family loop length in ms — keep in sync with web CSS animation durations. */ -export const WORKING_AVATAR_DURATIONS_MS = [ - 1800, 1350, 1600, 2400, 2400, 1350, 1100, 1350, 1600, 1350, -] as const; - -export function workingAvatarDuration(seed: number): number { - "worklet"; - return WORKING_AVATAR_DURATIONS_MS[seed % 10] ?? 1800; -} - -export function workingAvatarFrame(seed: number, progress: number): WorkingAvatarFrame { - "worklet"; - const middle = (1 - Math.cos(progress * Math.PI * 2)) / 2; - const frame: WorkingAvatarFrame = { - translationX: 0, - translationY: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, - eyeOffsetX: 0, - eyeOffsetY: 0, - }; - - switch (seed % 10) { - case 0: - frame.translationY = 2 - 5 * middle; - frame.scaleX = 1.02 - 0.04 * middle; - break; - case 1: - frame.translationY = 2 - 5 * middle; - frame.scaleX = 1.04 - 0.08 * middle; - frame.scaleY = 0.96 + 0.09 * middle; - break; - case 2: - case 8: - frame.translationX = -1 + 2 * middle; - frame.rotation = -3 + 6 * middle; - break; - case 3: - case 4: - frame.scaleX = 0.98 + 0.06 * middle; - frame.scaleY = 0.98 + 0.06 * middle; - frame.rotation = -4 + 8 * middle; - break; - case 5: - case 9: - frame.scaleX = 1.04 - 0.08 * middle; - frame.scaleY = 0.96 + 0.08 * middle; - break; - case 6: - frame.scaleX = 0.96 + 0.1 * middle; - frame.scaleY = 0.96 + 0.1 * middle; - break; - default: - frame.rotation = -4 + 9 * middle; - } - - const angle = progress * Math.PI * 2; - switch (seed % 4) { - case 0: - frame.eyeOffsetX = Math.sin(angle) * 9; - frame.eyeOffsetY = Math.cos(angle) * 2; - break; - case 1: - frame.eyeOffsetX = Math.cos(angle) * 7; - frame.eyeOffsetY = Math.sin(angle) * 4; - break; - case 2: - frame.eyeOffsetX = Math.cos(angle) * 8; - frame.eyeOffsetY = Math.sin(angle) * 3; - break; - default: - frame.eyeOffsetX = Math.sin(angle * 2) * 6; - frame.eyeOffsetY = Math.cos(angle * 2) * 3; - } - return frame; -} diff --git a/packages/core/src/avatar-shape.test.ts b/packages/core/src/avatar-shape.test.ts deleted file mode 100644 index c9b5108..0000000 --- a/packages/core/src/avatar-shape.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { avatarIdentitySeed, organicAvatarPath } from "./avatar-shape.js"; - -describe("organic avatar geometry", () => { - it("is stable for an identity and changes across identities", () => { - const first = organicAvatarPath(avatarIdentitySeed("research")); - expect(first).toBe(organicAvatarPath(avatarIdentitySeed("research"))); - expect(first).not.toBe(organicAvatarPath(avatarIdentitySeed("health"))); - }); - - it("emits only path geometry from a numeric seed", () => { - const path = organicAvatarPath(42); - expect(path).toMatch(/^M[-0-9. ]+(?:C[-0-9. ]+)+Z$/); - expect(path).not.toMatch(/<|>|javascript:|url\(/i); - }); - - it("produces ten distinct smooth shape families", () => { - const families = Array.from({ length: 10 }, (_, seed) => - organicAvatarPath(seed, -((seed % 360) * Math.PI) / 180), - ); - expect(new Set(families)).toHaveLength(10); - expect(families.every((path) => (path.match(/C/g) ?? []).length >= 12)).toBe(true); - }); -}); diff --git a/packages/core/src/avatar-shape.ts b/packages/core/src/avatar-shape.ts deleted file mode 100644 index fccce7f..0000000 --- a/packages/core/src/avatar-shape.ts +++ /dev/null @@ -1,187 +0,0 @@ -type AvatarPoint = readonly [number, number]; - -let organicAvatarTemplates: readonly (readonly AvatarPoint[])[] | undefined; - -export function avatarIdentitySeed(identity: string): number { - let hash = 0; - for (let index = 0; index < identity.length; index++) { - hash = (hash << 5) - hash + identity.charCodeAt(index); - hash |= 0; - } - return Math.abs(hash); -} - -export function organicAvatarPath(seed: number, phaseOffset = 0): string { - const phase = ((seed % 360) * Math.PI) / 180 + phaseOffset; - const family = seed % 10; - if (!organicAvatarTemplates) { - organicAvatarTemplates = [ - [ - [-43, 22], - [-51, 16], - [-53, 6], - [-49, -4], - [-40, -12], - [-30, -13], - [-27, -24], - [-18, -34], - [-6, -36], - [4, -32], - [12, -42], - [25, -45], - [37, -38], - [43, -27], - [42, -17], - [51, -11], - [56, 0], - [54, 12], - [46, 20], - [30, 24], - [0, 25], - [-29, 25], - ], - [ - [0, -52], - [12, -35], - [28, -17], - [39, 2], - [42, 21], - [34, 38], - [19, 49], - [0, 52], - [-19, 49], - [-34, 38], - [-42, 21], - [-39, 2], - [-28, -17], - [-12, -35], - ], - [ - [-42, -23], - [-27, -40], - [-7, -46], - [14, -40], - [33, -27], - [46, -8], - [46, 14], - [35, 34], - [14, 46], - [-8, 46], - [-29, 38], - [-43, 21], - [-48, 0], - ], - Array.from({ length: 40 }, (_, index) => { - const angle = (index / 40) * Math.PI * 2; - const radius = 44 + Math.cos(angle * 5) * 5; - return [Math.cos(angle) * radius, Math.sin(angle) * radius]; - }), - Array.from({ length: 36 }, (_, index) => { - const angle = (index / 36) * Math.PI * 2; - const radius = 43 + Math.cos(angle * 6) * 7; - return [Math.cos(angle) * radius, Math.sin(angle) * radius]; - }), - Array.from({ length: 32 }, (_, index) => { - const angle = (index / 32) * Math.PI * 2; - const x = Math.cos(angle); - const y = Math.sin(angle); - return [ - Math.sign(x) * Math.sqrt(Math.abs(x)) * 46, - Math.sign(y) * Math.sqrt(Math.abs(y)) * 43, - ]; - }), - [ - [0, 48], - [-14, 36], - [-32, 20], - [-45, 0], - [-43, -20], - [-29, -36], - [-10, -39], - [0, -27], - [10, -39], - [29, -36], - [43, -20], - [45, 0], - [32, 20], - [14, 36], - ], - [ - [-48, 35], - [-38, 22], - [-34, -8], - [-27, -31], - [-12, -44], - [0, -48], - [12, -44], - [27, -31], - [34, -8], - [38, 22], - [48, 35], - [26, 42], - [0, 44], - [-26, 42], - ], - [ - [-45, -19], - [-28, -38], - [-4, -46], - [22, -41], - [42, -25], - [49, -2], - [42, 23], - [22, 42], - [-4, 47], - [-29, 38], - [-46, 18], - [-50, 0], - ], - [ - [-50, 0], - [-45, -20], - [-30, -38], - [0, -48], - [30, -38], - [45, -20], - [50, 0], - [35, 10], - [18, 12], - [16, 38], - [0, 46], - [-16, 38], - [-18, 12], - [-35, 10], - ], - ] as const; - } - const templates = organicAvatarTemplates; - const xScale = 1 + Math.sin(phase) * 0.035; - const yScale = 1 + Math.cos(phase) * 0.025; - const shear = Math.sin(phase * 1.7) * 0.025; - let points = (templates[family] ?? templates[5]!).map(([x, y]) => ({ - x: x * xScale + y * shear, - y: y * yScale, - })); - const smoothingPasses = family >= 3 && family <= 5 ? 1 : 2; - for (let pass = 0; pass < smoothingPasses; pass++) { - points = points.flatMap((point, index) => { - const next = points[(index + 1) % points.length]!; - return [ - { x: point.x * 0.75 + next.x * 0.25, y: point.y * 0.75 + next.y * 0.25 }, - { x: point.x * 0.25 + next.x * 0.75, y: point.y * 0.25 + next.y * 0.75 }, - ]; - }); - } - const pointCount = points.length; - const round = (value: number) => Math.round(value * 100) / 100; - const first = points[0]!; - let path = `M${round(first.x)} ${round(first.y)}`; - for (let index = 0; index < pointCount; index++) { - const before = points[(index - 1 + pointCount) % pointCount]!; - const current = points[index]!; - const next = points[(index + 1) % pointCount]!; - const after = points[(index + 2) % pointCount]!; - path += `C${round(current.x + (next.x - before.x) / 6)} ${round(current.y + (next.y - before.y) / 6)} ${round(next.x - (after.x - current.x) / 6)} ${round(next.y - (after.y - current.y) / 6)} ${round(next.x)} ${round(next.y)}`; - } - return `${path}Z`; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5ad8ea1..62fcfd7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,8 +4,6 @@ export * from "./ai-consent.js"; export * from "./answerable-ask.js"; export * from "./async.js"; export * from "./attachments.js"; -export * from "./avatar-motion.js"; -export * from "./avatar-shape.js"; export * from "./bot-messages.js"; export * from "./bot-reply-sanitize.js"; export * from "./bot-sections.js"; diff --git a/packages/ui-web/src/avatar-motion-sync.test.ts b/packages/ui-web/src/avatar-motion-sync.test.ts deleted file mode 100644 index afaf91e..0000000 --- a/packages/ui-web/src/avatar-motion-sync.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { readFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { WORKING_AVATAR_DURATIONS_MS } from "@rakazo/core"; -import { describe, expect, it } from "vitest"; - -/** CSS `data-shape-family` → expected duration seconds (mirrors styles.css). */ -const FAMILY_DURATION_SECONDS = [1.8, 1.35, 1.6, 2.4, 2.4, 1.35, 1.1, 1.35, 1.6, 1.35] as const; - -function workingDurationSecondsForFamily(css: string, family: number): number | null { - // Split on rule closers so a wrong duration cannot match a later family's token. - for (const chunk of css.split("}")) { - if (!chunk.includes(`data-shape-family="${family}"]`)) continue; - if (!chunk.includes(".rakazo-organic-avatar-body-working")) continue; - const match = chunk.match(/animation:\s*[^;]*?\s([\d.]+)s\b/); - if (match?.[1]) return Number(match[1]); - } - return null; -} - -describe("organic working avatar CSS", () => { - it("keeps each shape-family duration aligned with shared core choreography", () => { - const css = readFileSync( - resolve(dirname(fileURLToPath(import.meta.url)), "styles.css"), - "utf8", - ); - expect(WORKING_AVATAR_DURATIONS_MS).toHaveLength(FAMILY_DURATION_SECONDS.length); - for (let family = 0; family < FAMILY_DURATION_SECONDS.length; family += 1) { - const seconds = FAMILY_DURATION_SECONDS[family]!; - const sharedMs = WORKING_AVATAR_DURATIONS_MS[family]; - expect(sharedMs).toBeTypeOf("number"); - expect(sharedMs! / 1000).toBe(seconds); - expect(workingDurationSecondsForFamily(css, family)).toBe(seconds); - } - }); -}); diff --git a/packages/ui-web/src/avatar-style.tsx b/packages/ui-web/src/avatar-style.tsx deleted file mode 100644 index a747a09..0000000 --- a/packages/ui-web/src/avatar-style.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createContext, type ReactNode, useContext } from "react"; - -export type AvatarStyle = "robot" | "organic"; - -const AvatarStyleContext = createContext("robot"); - -export function AvatarStyleProvider({ - children, - value, -}: { - children: ReactNode; - value: AvatarStyle; -}) { - return {children}; -} - -export function useAvatarStyle(): AvatarStyle { - return useContext(AvatarStyleContext); -} diff --git a/packages/ui-web/src/bot-avatar.tsx b/packages/ui-web/src/bot-avatar.tsx index 441a43e..48ca7c9 100644 --- a/packages/ui-web/src/bot-avatar.tsx +++ b/packages/ui-web/src/bot-avatar.tsx @@ -67,7 +67,6 @@ export interface BotAvatarProps { /** Draw one still frame. */ still?: boolean; className?: string; - variant?: unknown; } function WorkingRing({ size, color }: { size: number; color: string }) { diff --git a/packages/ui-web/src/index.ts b/packages/ui-web/src/index.ts index cbbd90e..5754d8c 100644 --- a/packages/ui-web/src/index.ts +++ b/packages/ui-web/src/index.ts @@ -1,4 +1,3 @@ -export { type AvatarStyle, AvatarStyleProvider, useAvatarStyle } from "./avatar-style.js"; export { BotAvatar, type BotAvatarProps, diff --git a/packages/ui-web/src/styles.css b/packages/ui-web/src/styles.css index 9d1289c..987f549 100644 --- a/packages/ui-web/src/styles.css +++ b/packages/ui-web/src/styles.css @@ -106,445 +106,8 @@ opacity: 1; } -.rakazo-bot-avatar-eyes { - animation: var(--rakazo-eye-animation-name) var(--rakazo-eye-animation-duration) - var(--rakazo-eye-animation-easing) var(--rakazo-eye-animation-delay) infinite; - transition: opacity 180ms cubic-bezier(0.4, 0, 0.2, 1); -} - -.rakazo-bot-avatar-eyes-working { - opacity: 0; -} - -.rakazo-bot-avatar[data-working="true"] .rakazo-bot-avatar-eyes-idle { - opacity: 0; -} - -.rakazo-bot-avatar[data-working="true"] .rakazo-bot-avatar-eyes-working { - opacity: 1; -} - -.rakazo-organic-avatar-body { - transform-origin: center; - transition: opacity 180ms cubic-bezier(0.4, 0, 0.2, 1); -} - -.rakazo-organic-avatar-body-idle { - animation: rakazo-organic-idle 5.6s ease-in-out infinite; -} - -.rakazo-organic-avatar-body-working { - opacity: 0; -} - -.rakazo-organic-avatar[data-working="true"] .rakazo-organic-avatar-body-idle { - opacity: 0; -} - -.rakazo-organic-avatar[data-working="true"] .rakazo-organic-avatar-body-working { - opacity: 1; -} - -.rakazo-organic-avatar-eyes { - transition: opacity 180ms cubic-bezier(0.4, 0, 0.2, 1); -} - -.rakazo-organic-avatar-eyes-working { - opacity: 0; -} - -.rakazo-organic-avatar[data-working="true"] .rakazo-organic-avatar-eyes-idle { - opacity: 0; -} - -.rakazo-organic-avatar[data-working="true"] .rakazo-organic-avatar-eyes-working { - opacity: 1; -} - -.rakazo-organic-avatar[data-eye-pattern="0"] .rakazo-organic-avatar-eyes-idle { - animation: rakazo-eyes-idle-0 5.8s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="1"] .rakazo-organic-avatar-eyes-idle { - animation: rakazo-eyes-idle-1 6.4s ease-in-out -1.7s infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="2"] .rakazo-organic-avatar-eyes-idle { - animation: rakazo-eyes-idle-2 5.2s ease-in-out -3.1s infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="3"] .rakazo-organic-avatar-eyes-idle { - animation: rakazo-eyes-idle-3 6.8s ease-in-out -2.3s infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="0"] .rakazo-organic-avatar-eyes-working { - animation: rakazo-organic-eyes-loop-0 6.6s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="1"] .rakazo-organic-avatar-eyes-working { - animation: rakazo-organic-eyes-loop-1 7.2s ease-in-out -1.4s infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="2"] .rakazo-organic-avatar-eyes-working { - animation: rakazo-organic-eyes-loop-2 6.1s ease-in-out -3.2s infinite; -} - -.rakazo-organic-avatar[data-eye-pattern="3"] .rakazo-organic-avatar-eyes-working { - animation: rakazo-organic-eyes-loop-3 7.8s ease-in-out -2.1s infinite; -} - -/* Durations mirror @rakazo/core WORKING_AVATAR_DURATIONS_MS (avatar-motion). */ -.rakazo-organic-avatar[data-shape-family="0"] .rakazo-organic-avatar-body-working { - animation: rakazo-organic-float 1.8s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-shape-family="1"] .rakazo-organic-avatar-body-working { - animation: rakazo-organic-stretch 1.35s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-shape-family="2"] .rakazo-organic-avatar-body-working, -.rakazo-organic-avatar[data-shape-family="8"] .rakazo-organic-avatar-body-working { - animation: rakazo-organic-sway 1.6s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-shape-family="3"] .rakazo-organic-avatar-body-working, -.rakazo-organic-avatar[data-shape-family="4"] .rakazo-organic-avatar-body-working { - animation: rakazo-organic-turn 2.4s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-shape-family="5"] .rakazo-organic-avatar-body-working, -.rakazo-organic-avatar[data-shape-family="9"] .rakazo-organic-avatar-body-working { - animation: rakazo-organic-squash 1.35s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-shape-family="6"] .rakazo-organic-avatar-body-working { - animation: rakazo-organic-pulse 1.1s ease-in-out infinite; -} - -.rakazo-organic-avatar[data-shape-family="7"] .rakazo-organic-avatar-body-working { - transform-origin: center 20%; - animation: rakazo-organic-ring 1.35s ease-in-out infinite; -} - -@keyframes rakazo-organic-float { - 0%, - 100% { - transform: translateY(2px) scaleX(1.02); - } - 50% { - transform: translateY(-3px) scaleX(0.98); - } -} - -@keyframes rakazo-organic-idle { - 0%, - 100% { - transform: translateY(1.2px) scale(1.012, 0.988); - } - 50% { - transform: translateY(-1.4px) scale(0.988, 1.012); - } -} - -@keyframes rakazo-organic-stretch { - 0%, - 100% { - transform: translateY(2px) scale(1.04, 0.96); - } - 50% { - transform: translateY(-3px) scale(0.96, 1.05); - } -} - -@keyframes rakazo-organic-sway { - 0%, - 100% { - transform: rotate(-3deg) translateX(-1px); - } - 50% { - transform: rotate(3deg) translateX(1px); - } -} - -@keyframes rakazo-organic-turn { - 0%, - 100% { - transform: rotate(-4deg) scale(0.98); - } - 50% { - transform: rotate(4deg) scale(1.04); - } -} - -@keyframes rakazo-organic-squash { - 0%, - 100% { - transform: scale(1.04, 0.96); - } - 50% { - transform: scale(0.96, 1.04); - } -} - -@keyframes rakazo-organic-pulse { - 0%, - 100% { - transform: scale(0.96); - } - 45% { - transform: scale(1.06); - } -} - -@keyframes rakazo-organic-ring { - 0%, - 100% { - transform: rotate(-4deg); - } - 35% { - transform: rotate(5deg); - } - 65% { - transform: rotate(-3deg); - } -} - -@keyframes rakazo-organic-eyes-loop-0 { - 0%, - 100% { - transform: translate(-9%, 1%) scaleY(0.94); - } - 18% { - transform: translate(9%, -2%) scaleY(1.04); - } - 34% { - transform: translate(0, -8%) scale(1.08); - } - 48% { - transform: scale(1.12, 0.82); - } - 51% { - transform: scale(1.05, 0.08); - } - 54% { - transform: scale(1.12, 0.9); - } - 72% { - transform: translate(-5%, 7%) scaleY(0.9); - } - 86% { - transform: translate(7%, 3%); - } -} - -@keyframes rakazo-organic-eyes-loop-1 { - 0%, - 100% { - transform: translate(0, -8%) scale(1.06); - } - 16% { - transform: translate(8%, 2%); - } - 31% { - transform: translate(0, 8%) scaleY(0.9); - } - 46% { - transform: translate(-8%, 2%); - } - 61% { - transform: scale(0.88, 1.1); - } - 76% { - transform: translateY(-7%) scaleY(1.08); - } - 88% { - transform: translateY(6%) scale(1.08, 0.86); - } -} - -@keyframes rakazo-organic-eyes-loop-2 { - 0%, - 100% { - transform: scale(0.9, 1.08); - } - 20% { - transform: scale(1.14, 0.84); - } - 24% { - transform: scale(1.08, 0.08); - } - 28% { - transform: scale(1.02); - } - 45% { - transform: translate(-9%, -3%) scaleY(0.94); - } - 62% { - transform: translate(9%, 2%) scaleY(1.05); - } - 80% { - transform: translate(0, -8%) scale(1.06); - } -} - -@keyframes rakazo-organic-eyes-loop-3 { - 0%, - 100% { - transform: translateY(7%) scaleY(0.9); - } - 14% { - transform: translateY(-9%) scaleY(1.08); - } - 30% { - transform: translate(8%, -2%); - } - 47% { - transform: translate(-8%, 3%) scaleY(0.94); - } - 63% { - transform: scale(1.12, 0.82); - } - 78% { - transform: translate(0, -7%) scale(0.9, 1.08); - } - 91% { - transform: translate(0, 5%) scale(1.06, 0.9); - } -} - -@keyframes rakazo-eyes-idle-0 { - 0%, - 32%, - 100% { - transform: translate(0, 0) scaleY(1); - } - 35% { - transform: translate(0, 0) scaleY(0.08); - } - 38% { - transform: translate(0, 0) scaleY(1); - } - 48%, - 62% { - transform: translate(3px, -0.5px) scaleY(1); - } - 70%, - 84% { - transform: translate(-2.5px, 0.5px) scaleY(1); - } - 89% { - transform: translate(0, 0) scaleY(0.12); - } - 91% { - transform: translate(0, 0) scaleY(1); - } -} - -@keyframes rakazo-eyes-idle-1 { - 0%, - 25%, - 100% { - transform: translate(0, 0) scaleY(1); - } - 32%, - 48% { - transform: translate(-3px, -1px) scaleY(1); - } - 54% { - transform: translate(0, 0) scaleY(0.08); - } - 56% { - transform: translate(0, 0) scaleY(1); - } - 59% { - transform: translate(0, 0) scaleY(0.08); - } - 61% { - transform: translate(0, 0) scaleY(1); - } - 68%, - 82% { - transform: translate(2.5px, 0.5px) scaleY(1); - } -} - -@keyframes rakazo-eyes-idle-2 { - 0%, - 28%, - 100% { - transform: translate(0, 0) scaleY(1); - } - 36%, - 52% { - transform: translate(0.5px, -1.8px) scaleY(1.05); - } - 58% { - transform: translate(0, 0) scaleY(0.08); - } - 60% { - transform: translate(0, 0) scaleY(1); - } - 68%, - 85% { - transform: translate(3px, 0) scaleY(1); - } - 92% { - transform: translate(0, 0) scaleY(0.1); - } - 94% { - transform: translate(0, 0) scaleY(1); - } -} - -@keyframes rakazo-eyes-idle-3 { - 0%, - 24%, - 100% { - transform: translate(0, 0) scaleY(1); - } - 30%, - 46% { - transform: translate(-2.5px, 1.2px) scaleY(0.95); - } - 52% { - transform: translate(0, 0) scaleY(0.08); - } - 54% { - transform: translate(0, 0) scaleY(1); - } - 62%, - 78% { - transform: translate(2.8px, -1px) scaleY(1); - } - 86% { - transform: translate(0, 0) scaleY(0.08); - } - 88% { - transform: translate(0, 0) scaleY(1); - } -} - -@keyframes rakazo-eyes-working { - 0% { - transform: translate(-4px, 0) scale(0.92, 0.95); - } - 25% { - transform: translate(0, -2px) scale(1.08, 1.08); - } - 50% { - transform: translate(4px, 0) scale(0.92, 0.95); - } - 75% { - transform: translate(0, 2px) scale(1, 0.9); - } - 100% { - transform: translate(-4px, 0) scale(0.92, 0.95); - } -} - @media (prefers-reduced-motion: reduce) { - .rakazo-bot-avatar-ring, - .rakazo-bot-avatar-eyes { + .rakazo-bot-avatar-ring { animation: none; transition: none; } @@ -552,19 +115,4 @@ .rakazo-bot-avatar { transition: none; } - - .rakazo-bot-avatar-visor { - transition: none; - } - - .rakazo-organic-avatar[data-shape-family] .rakazo-organic-avatar-body { - animation: none; - d: var(--rakazo-organic-path); - transition: none; - } - - .rakazo-organic-avatar[data-eye-pattern] .rakazo-organic-avatar-eyes { - animation: none; - transition: none; - } }