feat(avatar): give mobile bots the same Oo face as web (#138)

Native avatars now draw the shared Oo rig with react-native-svg instead
of a visor with two equal eyes, so the Oo eyes look the same on every
surface. The robot/organic avatar setting is gone from web and mobile
because every bot is now an Oo; uploaded pictures show on native too.


Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-05 00:36:46 +02:00 committed by GitHub
parent 47988602e2
commit 7db3a56fe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 319 additions and 1425 deletions

View File

@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller"; import { KeyboardProvider } from "react-native-keyboard-controller";
import { AvatarStyleProvider } from "../components/avatar-style";
import { ComputerUpdateProgress } from "../components/computer-update-progress"; import { ComputerUpdateProgress } from "../components/computer-update-progress";
import { currentApiBase, loadApiBase, loadSessionToken, selectedSpaceId } from "../lib/api"; import { currentApiBase, loadApiBase, loadSessionToken, selectedSpaceId } from "../lib/api";
import { loadAppearancePreference, mobileTokens } from "../lib/appearance"; import { loadAppearancePreference, mobileTokens } from "../lib/appearance";
@ -57,7 +56,6 @@ export default function Layout() {
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<KeyboardProvider> <KeyboardProvider>
{ready ? ( {ready ? (
<AvatarStyleProvider>
<ThemeProvider value={navigationTheme}> <ThemeProvider value={navigationTheme}>
<StatusBar style={resolved === "light" ? "dark" : "light"} /> <StatusBar style={resolved === "light" ? "dark" : "light"} />
<Stack <Stack
@ -124,7 +122,6 @@ export default function Layout() {
</Stack> </Stack>
<ComputerUpdateProgress /> <ComputerUpdateProgress />
</ThemeProvider> </ThemeProvider>
</AvatarStyleProvider>
) : ( ) : (
<View style={{ flex: 1, backgroundColor: String(native.page) }} /> <View style={{ flex: 1, backgroundColor: String(native.page) }} />
)} )}

View File

@ -1,4 +1,3 @@
import type { AvatarStyle } from "@rakazo/contracts";
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
@ -15,8 +14,6 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; 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 type { MobileBot, MobileMe } from "../lib/api";
import { import {
currentApiBase, currentApiBase,
@ -61,8 +58,6 @@ export default function Account() {
const [localeSaving, setLocaleSaving] = useState(false); const [localeSaving, setLocaleSaving] = useState(false);
const [localeError, setLocaleError] = useState<string | null>(null); const [localeError, setLocaleError] = useState<string | null>(null);
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const [avatarPending, setAvatarPending] = useState(false);
const [avatarError, setAvatarError] = useState<string | null>(null);
const [notifications, setNotifications] = useState<LiveNotificationSettings>( const [notifications, setNotifications] = useState<LiveNotificationSettings>(
DEFAULT_LIVE_NOTIFICATION_SETTINGS, DEFAULT_LIVE_NOTIFICATION_SETTINGS,
); );
@ -76,7 +71,6 @@ export default function Account() {
inputTokens: number; inputTokens: number;
outputTokens: number; outputTokens: number;
} | null>(null); } | null>(null);
const { avatarStyle, updateAvatarStyle } = useAvatarStyle();
const appearance = getCachedAppearancePreference(); const appearance = getCachedAppearancePreference();
const styles = useThemedStyles(createAccountStyles); const styles = useThemedStyles(createAccountStyles);
const versionInfo = getAppVersionInfo(); 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() { async function handleSignOut() {
setPending(true); setPending(true);
setError(null); setError(null);
@ -260,7 +241,7 @@ export default function Account() {
<Text style={styles.chevron}>›</Text> <Text style={styles.chevron}>›</Text>
</Pressable> </Pressable>
<View accessibilityLabel={t("Appearance")} style={styles.avatarSection}> <View accessibilityLabel={t("Appearance")} style={styles.appearanceSection}>
<Text style={styles.settingsTitle}>{t("Appearance")}</Text> <Text style={styles.settingsTitle}>{t("Appearance")}</Text>
<View style={styles.appearanceOptions}> <View style={styles.appearanceOptions}>
{( {(
@ -292,40 +273,6 @@ export default function Account() {
</View> </View>
</View> </View>
<View accessibilityLabel={t("Avatar style")} style={styles.avatarSection}>
<Text style={styles.settingsTitle}>{t("Avatars")}</Text>
<View style={styles.avatarOptions}>
{(["robot", "organic"] as const).map((style) => {
const selected = avatarStyle === style;
const styleLabel = style === "robot" ? t("Robot") : t("Organic");
return (
<Pressable
key={style}
accessibilityLabel={t("{style} avatars", { style: styleLabel })}
accessibilityRole="button"
accessibilityState={{ selected, disabled: avatarPending }}
disabled={avatarPending}
onPress={() => void selectAvatarStyle(style)}
style={({ pressed }) => [
styles.avatarOption,
selected && styles.avatarOptionSelected,
pressed && styles.pressed,
]}
>
<BotAvatar
color={style === "robot" ? "#8B5CF6" : "#D62F8B"}
identity="avatar-preview"
size={42}
variant={style}
/>
<Text style={styles.avatarLabel}>{styleLabel}</Text>
</Pressable>
);
})}
</View>
{avatarError ? <Text style={styles.error}>{avatarError}</Text> : null}
</View>
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={t("Language")} accessibilityLabel={t("Language")}
@ -649,7 +596,7 @@ function createAccountStyles() {
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
}, },
avatarSection: { appearanceSection: {
borderRadius: 16, borderRadius: 16,
backgroundColor: native.fill, backgroundColor: native.fill,
padding: 18, padding: 18,
@ -677,29 +624,6 @@ function createAccountStyles() {
fontSize: 14, fontSize: 14,
fontWeight: "600", fontWeight: "600",
}, },
avatarOptions: {
flexDirection: "row",
gap: 12,
},
avatarOption: {
flex: 1,
minHeight: 86,
borderRadius: 14,
borderWidth: StyleSheet.hairlineWidth,
borderColor: native.tertiaryLabel,
alignItems: "center",
justifyContent: "center",
gap: 8,
},
avatarOptionSelected: {
borderColor: native.label,
backgroundColor: native.fillPressed,
},
avatarLabel: {
color: native.label,
fontSize: 14,
fontWeight: "600",
},
settingsTitle: { settingsTitle: {
color: native.label, color: native.label,
fontSize: 17, fontSize: 17,

View File

@ -1,52 +0,0 @@
import type { AvatarStyle, Me } from "@rakazo/contracts";
import { usePathname } from "expo-router";
import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from "react";
import { rpc } from "../lib/api";
const AvatarStyleContext = createContext<{
avatarStyle: AvatarStyle;
updateAvatarStyle: (avatarStyle: AvatarStyle) => Promise<void>;
}>({
avatarStyle: "robot",
updateAvatarStyle: async () => undefined,
});
export function AvatarStyleProvider({ children }: { children: ReactNode }) {
const [avatarStyle, setAvatarStyle] = useState<AvatarStyle>("robot");
const pathname = usePathname();
const requestIdRef = useRef(0);
const updatePromiseRef = useRef<Promise<void> | null>(null);
useEffect(() => {
const requestId = ++requestIdRef.current;
void rpc<Me>("me")
.then((me) => {
if (requestId !== requestIdRef.current) return;
setAvatarStyle(me.avatarStyle);
})
.catch(() => undefined);
}, [pathname]);
function updateAvatarStyle(next: AvatarStyle): Promise<void> {
if (updatePromiseRef.current) return updatePromiseRef.current;
const requestId = ++requestIdRef.current;
const update = rpc<Me>("preferences/update", { avatarStyle: next })
.then((me) => {
if (requestId !== requestIdRef.current) return;
setAvatarStyle(me.avatarStyle);
})
.finally(() => {
updatePromiseRef.current = null;
});
updatePromiseRef.current = update;
return update;
}
return (
<AvatarStyleContext value={{ avatarStyle, updateAvatarStyle }}>{children}</AvatarStyleContext>
);
}
export function useAvatarStyle() {
return useContext(AvatarStyleContext);
}

View File

@ -1,95 +1,58 @@
import type { AvatarStyle } from "@rakazo/contracts"; import type { OoMoment } from "@rakazo/core";
import { import {
ACTIVE_RUN_STATUSES, ACTIVE_RUN_STATUSES,
avatarIdentitySeed,
DEFAULT_OO_AVATAR, DEFAULT_OO_AVATAR,
ooAvatarColor, decodeOoAvatar,
organicAvatarPath, ooIdentityShapeIndex,
ooMomentForStatus,
resolveOoColor,
} from "@rakazo/core"; } from "@rakazo/core";
import { memo, useEffect } from "react"; import { memo, useMemo } from "react";
import { View } from "react-native"; import { Image, 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 { useI18n } from "../lib/i18n"; import { useI18n } from "../lib/i18n";
import { useAvatarStyle } from "./avatar-style";
import { NativeSymbol } from "./native-symbol"; import { NativeSymbol } from "./native-symbol";
import { OoFigure } from "./oo-figure";
const AnimatedRect = Animated.createAnimatedComponent(Rect);
export const BotAvatar = memo(function BotAvatar({ export const BotAvatar = memo(function BotAvatar({
color, color,
size = 54, size = 54,
status, status,
identity, identity,
variant, moment,
still,
muted = false, muted = false,
}: { }: {
color: string; color: string;
size?: number; size?: number;
status?: string; status?: string;
identity?: string; identity?: string;
variant?: AvatarStyle; /** Explicit moment; wins over `status`. */
moment?: OoMoment;
still?: boolean;
muted?: boolean; muted?: boolean;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const isWorking = ACTIVE_RUN_STATUSES.some((activeStatus) => activeStatus === status); const isWorking = ACTIVE_RUN_STATUSES.some((activeStatus) => activeStatus === status);
const { avatarStyle } = useAvatarStyle(); const picture = color.startsWith("data:image/") ? color : null;
const fill = ooAvatarColor(color) || DEFAULT_OO_AVATAR.color; const look = useMemo(() => decodeOoAvatar(color) ?? DEFAULT_OO_AVATAR, [color]);
const visorW = Math.round(size * 0.68); const seed = identity || (picture ? "agent" : color) || "agent";
const visorH = Math.round(size * 0.44); const body = resolveOoColor(seed, look.color).hex;
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));
return ( return (
<View style={{ width: size, height: size }}> <View style={{ width: size, height: size }}>
{(variant ?? avatarStyle) === "organic" ? ( {picture ? (
<OrganicAvatar color={fill} identity={identity} size={size} isWorking={isWorking} /> <Image
) : ( source={{ uri: picture }}
<View style={{ width: size, height: size, borderRadius: size / 2 }}
style={{ />
width: size, ) : (
height: size, <OoFigure
borderRadius: size / 2, avatar={look}
backgroundColor: fill, color={body}
alignItems: "center", fallbackShape={look.shape ?? ooIdentityShapeIndex(seed)}
justifyContent: "center", moment={moment ?? ooMomentForStatus(status)}
}} size={size}
> still={still}
<View
style={{
width: visorW,
height: visorH,
borderRadius: Math.round(visorH * 0.52),
backgroundColor: "#0C0C0E",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap,
}}
>
{[0, 1].map((eye) => (
<View
key={eye}
style={{
width: eyeW,
height: eyeH,
borderRadius: Math.max(2, Math.round(eyeW * 0.6)),
backgroundColor: "#fff",
}}
/> />
))}
</View>
</View>
)} )}
{isWorking ? ( {isWorking ? (
<View <View
@ -135,81 +98,3 @@ export const BotAvatar = memo(function BotAvatar({
</View> </View>
); );
}); });
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 (
<View style={{ width: size, height: size }}>
<Animated.View style={[{ width: size, height: size }, bodyStyle]}>
<Svg width={size} height={size} viewBox="-60 -60 120 120">
<Path d={organicAvatarPath(seed)} fill={color} />
<G transform={`rotate(${(seed % 9) - 4})`}>
<AnimatedRect
animatedProps={leftEyeProps}
width={7}
height={24}
rx={3.5}
fill="#101014"
/>
<AnimatedRect
animatedProps={rightEyeProps}
width={7}
height={24}
rx={3.5}
fill="#101014"
/>
</G>
</Svg>
</Animated.View>
</View>
);
}

View File

@ -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<Tick>();
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 <Path key={key} d={shape.d} {...paintProps(shape, gradient)} />;
case "circle":
return (
<Circle
key={key}
cx={shape.cx}
cy={shape.cy}
r={shape.r}
{...paintProps(shape, gradient)}
/>
);
case "ellipse":
return (
<Ellipse
key={key}
cx={shape.cx}
cy={shape.cy}
rx={shape.rx}
ry={shape.ry}
{...paintProps(shape, gradient)}
/>
);
case "rect":
return (
<Rect
key={key}
x={shape.x}
y={shape.y}
width={shape.width}
height={shape.height}
rx={shape.rx}
{...paintProps(shape, gradient)}
/>
);
case "group": {
const { part, fx } = shape;
if (part === "mouth") {
return (
<Path
key={key}
d={frame.mouth.d}
fill={frame.mouth.filled ? mouthColor : "none"}
stroke={mouthColor}
strokeWidth={3.2}
strokeLinecap="round"
strokeLinejoin="round"
/>
);
}
if (part === "tongue") {
return frame.tongue ? <Path key={key} d={frame.tongue} fill="#FF7FA6" /> : 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 (
<G key={key} transform={state?.transform ?? shape.transform} opacity={state?.opacity}>
{shape.children.map((child, index) => draw(child, index, gradient, frame, mouthColor))}
</G>
);
}
}
}
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<OoLife | null>(null);
const [frame, setFrame] = useState<OoFrame | null>(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 (
<Svg width={size} height={size} viewBox={VIEWBOX}>
<Defs>
<LinearGradient id={`${gradient}-body`} x1="0" y1="0" x2="1" y2="1">
<Stop offset="0" stopColor={colors.light} />
<Stop offset="0.45" stopColor={colors.base} />
<Stop offset="1" stopColor={colors.dark} />
</LinearGradient>
<RadialGradient id={`${gradient}-shine`} cx="0.3" cy="0.22" r="0.55">
<Stop offset="0" stopColor="#FFFFFF" stopOpacity="0.55" />
<Stop offset="1" stopColor="#FFFFFF" stopOpacity="0" />
</RadialGradient>
</Defs>
{rig.shapes.map((shape, index) => draw(shape, index, gradient, shown, rig.mouthColor))}
</Svg>
);
}

View File

@ -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);
});
});

View File

@ -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";

View File

@ -75,7 +75,6 @@ export const RU_MESSAGES: Record<string, string> = {
"{name} is working": "{name} работает", "{name} is working": "{name} работает",
"{name}’s computer": "Компьютер {name}", "{name}’s computer": "Компьютер {name}",
"{runs} runs · {tokens} tokens": "Запусков: {runs} · токенов: {tokens}", "{runs} runs · {tokens} tokens": "Запусков: {runs} · токенов: {tokens}",
"{style} avatars": "Аватары: {style}",
"@{name}": "@{name}", "@{name}": "@{name}",
"API key": "API-ключ", "API key": "API-ключ",
Account: "Аккаунт", Account: "Аккаунт",
@ -122,8 +121,6 @@ export const RU_MESSAGES: Record<string, string> = {
"Authorization timed out. Please try again.": "Authorization timed out. Please try again.":
"Время авторизации истекло. Пожалуйста, попробуйте еще раз.", "Время авторизации истекло. Пожалуйста, попробуйте еще раз.",
"Authorize {name}": "Авторизовать {name}", "Authorize {name}": "Авторизовать {name}",
Avatars: "Аватары",
"Avatar style": "Стиль аватара",
"Bearer token": "Bearer-токен", "Bearer token": "Bearer-токен",
"Back to sign in": "Вернуться ко входу", "Back to sign in": "Вернуться ко входу",
Back: "Назад", Back: "Назад",
@ -233,7 +230,6 @@ export const RU_MESSAGES: Record<string, string> = {
"Could not update computer": "Не удалось обновить компьютер", "Could not update computer": "Не удалось обновить компьютер",
"Could not update notifications": "Не удалось обновить уведомления.", "Could not update notifications": "Не удалось обновить уведомления.",
"Could not update reaction": "Не удалось обновить реакцию", "Could not update reaction": "Не удалось обновить реакцию",
"Couldn't update avatars": "Не удалось обновить аватары",
Create: "Создать", Create: "Создать",
"Create group": "Создать группу", "Create group": "Создать группу",
"Create section": "Создать раздел", "Create section": "Создать раздел",
@ -366,7 +362,6 @@ export const RU_MESSAGES: Record<string, string> = {
"OpenAI-compatible server URL": "URL-адрес сервера, совместимого с OpenAI", "OpenAI-compatible server URL": "URL-адрес сервера, совместимого с OpenAI",
Optional: "Необязательно", Optional: "Необязательно",
"Or connect an API key": "Или подключите API-ключ", "Or connect an API key": "Или подключите API-ключ",
Organic: "Органичный",
"Other model…": "Другая модель…", "Other model…": "Другая модель…",
Password: "Пароль", Password: "Пароль",
"Password recovery is not configured for this server": "Password recovery is not configured for this server":
@ -414,7 +409,6 @@ export const RU_MESSAGES: Record<string, string> = {
"Восстановить последнее сохранённое рабочее пространство. Несохранённые данные на компьютере будут потеряны.", "Восстановить последнее сохранённое рабочее пространство. Несохранённые данные на компьютере будут потеряны.",
"Resume notifications": "Возобновить уведомления", "Resume notifications": "Возобновить уведомления",
"Return to inbox": "Вернуться во входящие", "Return to inbox": "Вернуться во входящие",
Robot: "Робот",
Routine: "Задача", Routine: "Задача",
"Routine link is incomplete": "Ссылка на задачу неполная", "Routine link is incomplete": "Ссылка на задачу неполная",
Running: "Выполняется", Running: "Выполняется",

View File

@ -63,7 +63,6 @@ export const ZH_MESSAGES: Record<string, string> = {
"{name} is working": "{name} 正在工作", "{name} is working": "{name} 正在工作",
"{name}’s computer": "{name} 的电脑", "{name}’s computer": "{name} 的电脑",
"{runs} runs · {tokens} tokens": "{runs} 次运行 · {tokens} 个 token", "{runs} runs · {tokens} tokens": "{runs} 次运行 · {tokens} 个 token",
"{style} avatars": "{style} 头像",
"@{name}": "@{name}", "@{name}": "@{name}",
"API key": "API 密钥", "API key": "API 密钥",
Account: "账户", Account: "账户",
@ -110,8 +109,6 @@ export const ZH_MESSAGES: Record<string, string> = {
"Authorization code": "授权码", "Authorization code": "授权码",
"Authorization timed out. Please try again.": "授权超时,请重试。", "Authorization timed out. Please try again.": "授权超时,请重试。",
"Authorize {name}": "授权 {name}", "Authorize {name}": "授权 {name}",
Avatars: "头像",
"Avatar style": "头像风格",
"Bearer token": "Bearer 令牌", "Bearer token": "Bearer 令牌",
"Back to sign in": "返回登录", "Back to sign in": "返回登录",
Back: "返回", Back: "返回",
@ -218,7 +215,6 @@ export const ZH_MESSAGES: Record<string, string> = {
"Could not update computer": "无法更新电脑", "Could not update computer": "无法更新电脑",
"Could not update notifications": "无法更新通知", "Could not update notifications": "无法更新通知",
"Could not update reaction": "无法更新反应", "Could not update reaction": "无法更新反应",
"Couldn't update avatars": "无法更新头像",
Create: "创建", Create: "创建",
"Create group": "创建群组", "Create group": "创建群组",
"Create section": "创建分组", "Create section": "创建分组",
@ -350,7 +346,6 @@ export const ZH_MESSAGES: Record<string, string> = {
"OpenAI-compatible server URL": "OpenAI 兼容服务器地址", "OpenAI-compatible server URL": "OpenAI 兼容服务器地址",
Optional: "可选", Optional: "可选",
"Or connect an API key": "或连接 API 密钥", "Or connect an API key": "或连接 API 密钥",
Organic: "有机",
"Other model…": "其他模型…", "Other model…": "其他模型…",
Password: "密码", Password: "密码",
"Password recovery is not configured for this server": "此服务器未配置密码找回", "Password recovery is not configured for this server": "此服务器未配置密码找回",
@ -395,7 +390,6 @@ export const ZH_MESSAGES: Record<string, string> = {
"恢复上次保存的工作区。电脑上未保存的工作将会丢失。", "恢复上次保存的工作区。电脑上未保存的工作将会丢失。",
"Resume notifications": "恢复通知", "Resume notifications": "恢复通知",
"Return to inbox": "返回收件箱", "Return to inbox": "返回收件箱",
Robot: "机器人",
Routine: "例行任务", Routine: "例行任务",
"Routine link is incomplete": "例行任务链接不完整", "Routine link is incomplete": "例行任务链接不完整",
Running: "运行中", Running: "运行中",

View File

@ -756,10 +756,6 @@ msgstr "Verfügbar, sobald die Routine gespeichert ist"
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "Avatar-Studio" msgstr "Avatar-Studio"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr "Avatare"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr "Messaging-Einstellungen konnten nicht geladen werden"
msgid "Couldn't save" msgid "Couldn't save"
msgstr "Konnte nicht gespeichert werden" msgstr "Konnte nicht gespeichert werden"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr "Avatare konnten nicht aktualisiert werden"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "Messaging-Einstellungen konnten nicht aktualisiert werden" msgstr "Messaging-Einstellungen konnten nicht aktualisiert werden"
@ -3510,10 +3502,6 @@ msgstr "Optionaler Header-Wert"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "Oder einen API-Schlüssel verbinden" msgstr "Oder einen API-Schlüssel verbinden"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr "Organisch"
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "Organisations-API-Schlüssel" msgstr "Organisations-API-Schlüssel"
@ -4025,10 +4013,6 @@ msgstr "Widerrufen"
msgid "Riffing" msgid "Riffing"
msgstr "Am Riffen" msgstr "Am Riffen"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr "Roboter"
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "Hinweise für diesen Raum" msgstr "Hinweise für diesen Raum"

View File

@ -756,10 +756,6 @@ msgstr "Available after the routine is saved"
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "Avatar Studio" msgstr "Avatar Studio"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr "Avatars"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr "Couldn't load messaging settings"
msgid "Couldn't save" msgid "Couldn't save"
msgstr "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 #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "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" msgid "Or connect an API key"
msgstr "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 #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "Organization API key" msgstr "Organization API key"
@ -4025,10 +4013,6 @@ msgstr "Revoke"
msgid "Riffing" msgid "Riffing"
msgstr "Riffing" msgstr "Riffing"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr "Robot"
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "Room guidance" msgstr "Room guidance"

View File

@ -756,10 +756,6 @@ msgstr "Disponible después de guardar la rutina"
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "Estudio de avatares" msgstr "Estudio de avatares"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr "Avatares"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr "No se pudo cargar la configuración de mensajería"
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr "No se pudieron actualizar los avatares"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "No se pudo actualizar la configuración de mensajería" 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" msgid "Or connect an API key"
msgstr "O conecta una clave de API" msgstr "O conecta una clave de API"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr "Orgánico"
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "Clave de API de organización" msgstr "Clave de API de organización"
@ -4025,10 +4013,6 @@ msgstr "Revocar"
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr "Robot"
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "" msgstr ""

View File

@ -756,10 +756,6 @@ msgstr ""
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "अवतार स्टूडियो" msgstr "अवतार स्टूडियो"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr ""
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr ""
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "" msgstr ""
@ -3510,10 +3502,6 @@ msgstr "वैकल्पिक हेडर मान"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "या एक API कुंजी कनेक्ट करें" msgstr "या एक API कुंजी कनेक्ट करें"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr ""
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "संगठन API कुंजी" msgstr "संगठन API कुंजी"
@ -4025,10 +4013,6 @@ msgstr ""
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr ""
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "" msgstr ""

View File

@ -756,10 +756,6 @@ msgstr "루틴을 저장한 후 사용할 수 있습니다"
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "아바타 스튜디오" msgstr "아바타 스튜디오"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr "아바타"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr "메시징 설정을 불러올 수 없습니다"
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr "아바타를 업데이트할 수 없습니다"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "메시징 설정을 업데이트할 수 없습니다" msgstr "메시징 설정을 업데이트할 수 없습니다"
@ -3510,10 +3502,6 @@ msgstr "선택 사항인 헤더 값"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "또는 API 키 연결" msgstr "또는 API 키 연결"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr "자연스럽게"
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "조직 API 키" msgstr "조직 API 키"
@ -4025,10 +4013,6 @@ msgstr "연결 해제"
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr "로봇"
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "" msgstr ""

View File

@ -756,10 +756,6 @@ msgstr ""
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "Estúdio de avatar" msgstr "Estúdio de avatar"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr ""
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr ""
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "" msgstr ""
@ -3510,10 +3502,6 @@ msgstr "Valor de cabeçalho opcional"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "Ou conecte uma chave de API" msgstr "Ou conecte uma chave de API"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr ""
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "Chave da API da organização" msgstr "Chave da API da organização"
@ -4025,10 +4013,6 @@ msgstr ""
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr ""
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "" msgstr ""

View File

@ -756,10 +756,6 @@ msgstr "Доступно после сохранения задачи"
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "Студия аватаров" msgstr "Студия аватаров"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr "Аватары"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr "Не удалось загрузить настройки обмена
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr "Не удалось обновить аватары"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "Не удалось обновить настройки обмена сообщениями." msgstr "Не удалось обновить настройки обмена сообщениями."
@ -3510,10 +3502,6 @@ msgstr "Необязательное значение заголовка"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "Или подключить API-ключ" msgstr "Или подключить API-ключ"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr "Органичный"
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "Ключ API организации" msgstr "Ключ API организации"
@ -4025,10 +4013,6 @@ msgstr "Отозвать"
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr "Робот"
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "Инструкции для комнаты" msgstr "Инструкции для комнаты"

View File

@ -756,10 +756,6 @@ msgstr ""
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "Avatar Stüdyosu" msgstr "Avatar Stüdyosu"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr ""
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr ""
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "" msgstr ""
@ -3510,10 +3502,6 @@ msgstr "İsteğe bağlı üstbilgi değeri"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "Veya bir API anahtarı bağla" msgstr "Veya bir API anahtarı bağla"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr ""
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "Kuruluş API anahtarı" msgstr "Kuruluş API anahtarı"
@ -4025,10 +4013,6 @@ msgstr "İptal et"
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr ""
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "" msgstr ""

View File

@ -756,10 +756,6 @@ msgstr "保存例行任务后可用"
msgid "Avatar Studio" msgid "Avatar Studio"
msgstr "头像工作室" msgstr "头像工作室"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Avatars"
msgstr "头像"
#: src/components/browser/InAppBrowser.tsx #: src/components/browser/InAppBrowser.tsx
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
#: src/pages/Onboarding.tsx #: src/pages/Onboarding.tsx
@ -1748,10 +1744,6 @@ msgstr "无法加载消息设置"
msgid "Couldn't save" msgid "Couldn't save"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Couldn't update avatars"
msgstr "无法更新头像"
#: src/pages/MessagingSettingsOverlay.tsx #: src/pages/MessagingSettingsOverlay.tsx
msgid "Couldn't update messaging settings" msgid "Couldn't update messaging settings"
msgstr "无法更新消息设置" msgstr "无法更新消息设置"
@ -3510,10 +3502,6 @@ msgstr "可选请求头值"
msgid "Or connect an API key" msgid "Or connect an API key"
msgstr "或连接 API 密钥" msgstr "或连接 API 密钥"
#: src/pages/AccountSettingsOverlay.tsx
msgid "Organic"
msgstr "自然"
#: src/pages/memory-providers/SupermemorySettingsForm.tsx #: src/pages/memory-providers/SupermemorySettingsForm.tsx
msgid "Organization API key" msgid "Organization API key"
msgstr "组织 API 密钥" msgstr "组织 API 密钥"
@ -4025,10 +4013,6 @@ msgstr "撤销"
msgid "Riffing" msgid "Riffing"
msgstr "" msgstr ""
#: src/pages/AccountSettingsOverlay.tsx
msgid "Robot"
msgstr "机器人"
#: src/pages/ExternalConversationSettings.tsx #: src/pages/ExternalConversationSettings.tsx
msgid "Room guidance" msgid "Room guidance"
msgstr "" msgstr ""

View File

@ -1,6 +1,5 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import type { AvatarStyle } from "@rakazo/contracts"; import { Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web";
import { BotAvatar, Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { import {
type KeyboardEvent as ReactKeyboardEvent, type KeyboardEvent as ReactKeyboardEvent,
@ -28,8 +27,6 @@ import { UI_LOCALE_LABELS, UI_LOCALES, type UiLocale } from "../lib/ui-locale";
export type SettingsGeneralProps = { export type SettingsGeneralProps = {
email?: string | null; email?: string | null;
name: string; name: string;
avatarStyle: AvatarStyle;
onAvatarStyleChange: (style: AvatarStyle) => Promise<void>;
messagingEnabled?: boolean; messagingEnabled?: boolean;
onOpenMessaging?: () => void; onOpenMessaging?: () => void;
isDeploymentOwner?: boolean; isDeploymentOwner?: boolean;
@ -38,20 +35,15 @@ export type SettingsGeneralProps = {
export function GeneralSettingsPanels({ export function GeneralSettingsPanels({
email, email,
name, name,
avatarStyle,
onAvatarStyleChange,
messagingEnabled = false, messagingEnabled = false,
onOpenMessaging, onOpenMessaging,
isDeploymentOwner = false, isDeploymentOwner = false,
}: SettingsGeneralProps) { }: SettingsGeneralProps) {
const { t } = useLingui();
const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale()); const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale());
const localeRequestRef = useRef(0); const localeRequestRef = useRef(0);
const [appearance, setAppearance] = useState<AppearancePreference>(() => const [appearance, setAppearance] = useState<AppearancePreference>(() =>
getUiAppearancePreference(), getUiAppearancePreference(),
); );
const [avatarPending, setAvatarPending] = useState(false);
const [avatarError, setAvatarError] = useState<string | null>(null);
function chooseLocale(next: UiLocale) { function chooseLocale(next: UiLocale) {
if (next === locale) return; 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 ( return (
<div className="space-y-5"> <div className="space-y-5">
<section className="rounded-xl border border-border px-4 py-4"> <section className="rounded-xl border border-border px-4 py-4">
@ -122,37 +101,6 @@ export function GeneralSettingsPanels({
<UiLocalePicker value={locale} onChange={chooseLocale} /> <UiLocalePicker value={locale} onChange={chooseLocale} />
</section> </section>
<section className="rounded-xl border border-border px-4 py-4">
<h3 className="text-ui-body font-medium text-foreground">
<Trans>Avatars</Trans>
</h3>
<div className="mt-3 grid grid-cols-2 gap-3">
{(["robot", "organic"] as const).map((style) => (
<Toggle
key={style}
variant="outline"
pressed={style === avatarStyle}
disabled={avatarPending}
onPressedChange={() => void chooseAvatarStyle(style)}
className="h-auto justify-start gap-3 px-3.5 py-3 text-ui-body font-normal"
>
<BotAvatar
color="#D9508A"
identity="avatar-style-preview"
size={32}
variant={style}
/>
<span>{style === "robot" ? <Trans>Robot</Trans> : <Trans>Organic</Trans>}</span>
</Toggle>
))}
</div>
{avatarError ? (
<p role="alert" className="mt-3 text-ui-body-sm text-destructive">
{avatarError}
</p>
) : null}
</section>
{isDeploymentOwner ? ( {isDeploymentOwner ? (
<Button variant="outline" render={<Link to="/integrations/setup" />}> <Button variant="outline" render={<Link to="/integrations/setup" />}>
<Trans>Server integrations</Trans> <Trans>Server integrations</Trans>

View File

@ -1,5 +1,5 @@
import { useLingui } from "@lingui/react/macro"; import { useLingui } from "@lingui/react/macro";
import type { AvatarStyle, SpaceMemoryConfig } from "@rakazo/contracts"; import type { SpaceMemoryConfig } from "@rakazo/contracts";
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web"; import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
import { import {
Bell, Bell,
@ -58,8 +58,6 @@ export function SettingsOverlay({
name, name,
usage, usage,
initialSection = "general", initialSection = "general",
avatarStyle,
onAvatarStyleChange,
isDeploymentOwner = false, isDeploymentOwner = false,
sandboxProvider, sandboxProvider,
messagingEnabled = false, messagingEnabled = false,
@ -75,8 +73,6 @@ export function SettingsOverlay({
name: string; name: string;
usage?: { runs: number; inputTokens: number; outputTokens: number } | null; usage?: { runs: number; inputTokens: number; outputTokens: number } | null;
initialSection?: SettingsSection; initialSection?: SettingsSection;
avatarStyle: AvatarStyle;
onAvatarStyleChange: (style: AvatarStyle) => Promise<void>;
isDeploymentOwner?: boolean; isDeploymentOwner?: boolean;
sandboxProvider?: string | null; sandboxProvider?: string | null;
messagingEnabled?: boolean; messagingEnabled?: boolean;
@ -236,8 +232,6 @@ export function SettingsOverlay({
<GeneralSettingsPanels <GeneralSettingsPanels
email={email} email={email}
name={name} name={name}
avatarStyle={avatarStyle}
onAvatarStyleChange={onAvatarStyleChange}
messagingEnabled={messagingEnabled} messagingEnabled={messagingEnabled}
onOpenMessaging={ onOpenMessaging={
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined

View File

@ -51,7 +51,6 @@ import {
type WorkspaceFocus, type WorkspaceFocus,
} from "@rakazo/core"; } from "@rakazo/core";
import { import {
AvatarStyleProvider,
BotAvatar, BotAvatar,
Button, Button,
DropdownMenuItem, DropdownMenuItem,
@ -4127,7 +4126,6 @@ export function ShellPage() {
email={session.data?.user.email} email={session.data?.user.email}
usage={usage} usage={usage}
initialSection={settingsSection} initialSection={settingsSection}
avatarStyle={bootstrapMe?.avatarStyle ?? "robot"}
isDeploymentOwner={bootstrapMe?.isDeploymentOwner === true} isDeploymentOwner={bootstrapMe?.isDeploymentOwner === true}
sandboxProvider={bootstrapMe?.sandboxProvider} sandboxProvider={bootstrapMe?.sandboxProvider}
messagingEnabled={messagingSurfaceEnabled} messagingEnabled={messagingSurfaceEnabled}
@ -4135,10 +4133,6 @@ export function ShellPage() {
setSettingsOpen(false); setSettingsOpen(false);
setMessagingSettingsOpen(true); setMessagingSettingsOpen(true);
}} }}
onAvatarStyleChange={async (avatarStyle) => {
const nextMe = await rpc.preferences.update({ avatarStyle });
setBootstrapMe(nextMe);
}}
memoryConfig={memoryProviderConfig} memoryConfig={memoryProviderConfig}
onMemoryConfigChange={(config) => { onMemoryConfigChange={(config) => {
memoryProviderConfigRevision.current += 1; memoryProviderConfigRevision.current += 1;
@ -4236,5 +4230,5 @@ export function ShellPage() {
</div> </div>
); );
return <AvatarStyleProvider value="organic">{shell}</AvatarStyleProvider>; return shell;
} }

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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);
});
});

View File

@ -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`;
}

View File

@ -4,8 +4,6 @@ export * from "./ai-consent.js";
export * from "./answerable-ask.js"; export * from "./answerable-ask.js";
export * from "./async.js"; export * from "./async.js";
export * from "./attachments.js"; export * from "./attachments.js";
export * from "./avatar-motion.js";
export * from "./avatar-shape.js";
export * from "./bot-messages.js"; export * from "./bot-messages.js";
export * from "./bot-reply-sanitize.js"; export * from "./bot-reply-sanitize.js";
export * from "./bot-sections.js"; export * from "./bot-sections.js";

View File

@ -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);
}
});
});

View File

@ -1,19 +0,0 @@
import { createContext, type ReactNode, useContext } from "react";
export type AvatarStyle = "robot" | "organic";
const AvatarStyleContext = createContext<AvatarStyle>("robot");
export function AvatarStyleProvider({
children,
value,
}: {
children: ReactNode;
value: AvatarStyle;
}) {
return <AvatarStyleContext value={value}>{children}</AvatarStyleContext>;
}
export function useAvatarStyle(): AvatarStyle {
return useContext(AvatarStyleContext);
}

View File

@ -67,7 +67,6 @@ export interface BotAvatarProps {
/** Draw one still frame. */ /** Draw one still frame. */
still?: boolean; still?: boolean;
className?: string; className?: string;
variant?: unknown;
} }
function WorkingRing({ size, color }: { size: number; color: string }) { function WorkingRing({ size, color }: { size: number; color: string }) {

View File

@ -1,4 +1,3 @@
export { type AvatarStyle, AvatarStyleProvider, useAvatarStyle } from "./avatar-style.js";
export { export {
BotAvatar, BotAvatar,
type BotAvatarProps, type BotAvatarProps,

View File

@ -106,445 +106,8 @@
opacity: 1; 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) { @media (prefers-reduced-motion: reduce) {
.rakazo-bot-avatar-ring, .rakazo-bot-avatar-ring {
.rakazo-bot-avatar-eyes {
animation: none; animation: none;
transition: none; transition: none;
} }
@ -552,19 +115,4 @@
.rakazo-bot-avatar { .rakazo-bot-avatar {
transition: none; 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;
}
} }