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:
parent
47988602e2
commit
7db3a56fe8
@ -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,74 +56,72 @@ 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
|
screenOptions={{
|
||||||
screenOptions={{
|
headerStyle: { backgroundColor: navigationTheme.colors.background },
|
||||||
headerStyle: { backgroundColor: navigationTheme.colors.background },
|
headerTintColor: navigationTheme.colors.text,
|
||||||
headerTintColor: navigationTheme.colors.text,
|
headerShadowVisible: false,
|
||||||
headerShadowVisible: false,
|
headerBackButtonDisplayMode: "minimal",
|
||||||
headerBackButtonDisplayMode: "minimal",
|
contentStyle: { backgroundColor: String(native.page) },
|
||||||
contentStyle: { backgroundColor: String(native.page) },
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen name="index" options={{ headerShown: false, title: "Rakazo" }} />
|
||||||
|
<Stack.Screen name="sign-in" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen
|
||||||
|
name="integration-setup"
|
||||||
|
options={{ title: t("Server integrations") }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name="ai-data-sharing" options={{ title: "AI data sharing" }} />
|
||||||
|
<Stack.Screen name="account" options={{ title: t("Account") }} />
|
||||||
|
<Stack.Screen
|
||||||
|
name="change-password"
|
||||||
|
options={{
|
||||||
|
title: t("Change password"),
|
||||||
|
presentation: "formSheet",
|
||||||
|
sheetAllowedDetents: [0.6, 1],
|
||||||
|
sheetGrabberVisible: true,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Stack.Screen name="index" options={{ headerShown: false, title: "Rakazo" }} />
|
<Stack.Screen name="models" options={{ title: t("Models") }} />
|
||||||
<Stack.Screen name="sign-in" options={{ headerShown: false }} />
|
<Stack.Screen name="voice" options={{ title: t("Voice") }} />
|
||||||
<Stack.Screen
|
<Stack.Screen name="integrations" options={{ title: t("Integrations") }} />
|
||||||
name="integration-setup"
|
<Stack.Screen
|
||||||
options={{ title: t("Server integrations") }}
|
name="new"
|
||||||
/>
|
options={{
|
||||||
<Stack.Screen name="ai-data-sharing" options={{ title: "AI data sharing" }} />
|
title: t("New bot"),
|
||||||
<Stack.Screen name="account" options={{ title: t("Account") }} />
|
presentation: "modal",
|
||||||
<Stack.Screen
|
gestureEnabled: true,
|
||||||
name="change-password"
|
headerBackVisible: false,
|
||||||
options={{
|
}}
|
||||||
title: t("Change password"),
|
/>
|
||||||
presentation: "formSheet",
|
<Stack.Screen
|
||||||
sheetAllowedDetents: [0.6, 1],
|
name="new-group"
|
||||||
sheetGrabberVisible: true,
|
options={{
|
||||||
}}
|
title: t("New group"),
|
||||||
/>
|
presentation: "modal",
|
||||||
<Stack.Screen name="models" options={{ title: t("Models") }} />
|
gestureEnabled: true,
|
||||||
<Stack.Screen name="voice" options={{ title: t("Voice") }} />
|
}}
|
||||||
<Stack.Screen name="integrations" options={{ title: t("Integrations") }} />
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="new"
|
name="new-space"
|
||||||
options={{
|
options={{
|
||||||
title: t("New bot"),
|
title: t("New space"),
|
||||||
presentation: "modal",
|
presentation: "modal",
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
headerBackVisible: false,
|
headerBackVisible: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen name="group-thread" options={{ title: t("Group") }} />
|
||||||
name="new-group"
|
<Stack.Screen name="group-settings" options={{ title: t("Group settings") }} />
|
||||||
options={{
|
<Stack.Screen name="bot-settings" options={{ title: t("Chat settings") }} />
|
||||||
title: t("New group"),
|
<Stack.Screen name="thread" options={{ title: t("Thread") }} />
|
||||||
presentation: "modal",
|
<Stack.Screen name="routine" options={{ title: t("Routine") }} />
|
||||||
gestureEnabled: true,
|
<Stack.Screen name="computer" options={{ title: t("Computer") }} />
|
||||||
}}
|
</Stack>
|
||||||
/>
|
<ComputerUpdateProgress />
|
||||||
<Stack.Screen
|
</ThemeProvider>
|
||||||
name="new-space"
|
|
||||||
options={{
|
|
||||||
title: t("New space"),
|
|
||||||
presentation: "modal",
|
|
||||||
gestureEnabled: true,
|
|
||||||
headerBackVisible: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name="group-thread" options={{ title: t("Group") }} />
|
|
||||||
<Stack.Screen name="group-settings" options={{ title: t("Group settings") }} />
|
|
||||||
<Stack.Screen name="bot-settings" options={{ title: t("Chat settings") }} />
|
|
||||||
<Stack.Screen name="thread" options={{ title: t("Thread") }} />
|
|
||||||
<Stack.Screen name="routine" options={{ title: t("Routine") }} />
|
|
||||||
<Stack.Screen name="computer" options={{ title: t("Computer") }} />
|
|
||||||
</Stack>
|
|
||||||
<ComputerUpdateProgress />
|
|
||||||
</ThemeProvider>
|
|
||||||
</AvatarStyleProvider>
|
|
||||||
) : (
|
) : (
|
||||||
<View style={{ flex: 1, backgroundColor: String(native.page) }} />
|
<View style={{ flex: 1, backgroundColor: String(native.page) }} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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 }}
|
||||||
|
style={{ width: size, height: size, borderRadius: size / 2 }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<OoFigure
|
||||||
style={{
|
avatar={look}
|
||||||
width: size,
|
color={body}
|
||||||
height: size,
|
fallbackShape={look.shape ?? ooIdentityShapeIndex(seed)}
|
||||||
borderRadius: size / 2,
|
moment={moment ?? ooMomentForStatus(status)}
|
||||||
backgroundColor: fill,
|
size={size}
|
||||||
alignItems: "center",
|
still={still}
|
||||||
justifyContent: "center",
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
218
apps/mobile/components/oo-figure.tsx
Normal file
218
apps/mobile/components/oo-figure.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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";
|
|
||||||
@ -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: "Выполняется",
|
||||||
|
|||||||
@ -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: "运行中",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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 "Инструкции для комнаты"
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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`;
|
|
||||||
}
|
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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 }) {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export { type AvatarStyle, AvatarStyleProvider, useAvatarStyle } from "./avatar-style.js";
|
|
||||||
export {
|
export {
|
||||||
BotAvatar,
|
BotAvatar,
|
||||||
type BotAvatarProps,
|
type BotAvatarProps,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user