import type { AvatarShape, ComputerStatus } from "@rakazo/contracts"; import { AVATAR_SHAPES, BOT_COLORS, BOT_DESCRIPTION_MAX_LENGTH, BOT_NAME_MAX_LENGTH, BOT_TITLE_MAX_LENGTH, type ComputerMode, normalizeCreateBotProfile, } from "@rakazo/contracts"; import { botAvatarImageSrc, nextGeneratedAvatarFace } from "@rakazo/core"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; import { Pressable, ScrollView, Text, TextInput, View } from "react-native"; import { BotAvatar } from "../components/bot-avatar"; import { ComputerMaintenanceActions } from "../components/computer-maintenance-actions"; import { ComputerModePicker } from "../components/computer-mode-picker"; import { type MobileBot, rpc } from "../lib/api"; import { pickFromLibrary } from "../lib/pick-attachments"; type BotSettingsRecord = MobileBot & { description?: string; }; const AVATAR_SHAPE_LABELS: Record = { circle: "Circle", oval: "Oval", "rounded-square": "Rounded square", pill: "Pill", triangle: "Triangle", hexagon: "Hexagon", cloud: "Cloud", teardrop: "Teardrop", }; export default function BotSettingsScreen() { const router = useRouter(); const { botId } = useLocalSearchParams<{ botId: string }>(); const [bot, setBot] = useState(null); const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [computerMode, setComputerMode] = useState("team"); const [computer, setComputer] = useState(null); const [error, setError] = useState(null); const [pending, setPending] = useState(false); useEffect(() => { if (!botId) return; void Promise.all([ rpc("bots/get", { botId }), rpc("computer/status", { botId }).catch(() => null), ]) .then(([next, status]) => { setBot(next); setName(next.name); setTitle(next.title); setDescription(next.description ?? ""); setComputerMode(next.computerMode); setComputer(status); }) .catch((err) => setError(err instanceof Error ? err.message : "Could not load bot")); }, [botId]); async function applyAvatar(patch: { color?: string; avatarShape?: AvatarShape | null; avatarImageArtifactId?: string | null; }) { if (!botId || !bot || pending) return; const previous = bot; setBot({ ...bot, ...(patch.color !== undefined ? { color: patch.color } : {}), ...(patch.avatarShape !== undefined ? { avatarShape: patch.avatarShape } : {}), ...(patch.avatarImageArtifactId !== undefined ? { hasAvatarImage: patch.avatarImageArtifactId !== null } : {}), }); setPending(true); setError(null); try { const next = await rpc("bots/update", { botId, ...patch }); setBot(next); } catch (err) { setBot(previous); setError(err instanceof Error ? err.message : "Could not update avatar"); } finally { setPending(false); } } async function uploadAvatar() { if (!botId || pending) return; const picked = await pickFromLibrary(0); const file = picked.attachments[0]; if (!file) return; setPending(true); setError(null); const previous = bot; try { const artifact = await rpc<{ id: string }>("artifacts/create", { botId, name: file.name, mimeType: file.mimeType, contentBase64: file.contentBase64, }); if (bot) setBot({ ...bot, hasAvatarImage: true }); const next = await rpc("bots/update", { botId, avatarImageArtifactId: artifact.id, }); setBot(next); } catch (err) { if (previous) setBot(previous); setError(err instanceof Error ? err.message : "Could not update avatar"); } finally { setPending(false); } } async function save() { if (!botId || !bot || pending) return; setPending(true); setError(null); try { const profile = normalizeCreateBotProfile({ name, title, description }); const input: { botId: string; name?: string; title?: string; description?: string; instructions?: string; } = { botId }; if (profile.name !== bot.name) input.name = profile.name; if (profile.title !== bot.title) input.title = profile.title; if (profile.description !== (bot.description ?? "")) { input.description = profile.description; // Keep instructions in sync with description (same as web BotSettings). input.instructions = profile.instructions; } if (computerMode !== bot.computerMode) { await rpc("bots/setComputer", { botId, mode: computerMode }); } // Use key presence so clearing title/description to "" still persists. if (Object.keys(input).length > 1) { await rpc("bots/update", input); } router.back(); } catch (err) { setError(err instanceof Error ? err.message : "Could not save bot"); } finally { setPending(false); } } return ( <> {bot ? ( {AVATAR_SHAPES.map((shape) => { const selected = (bot.avatarShape ?? "circle") === shape && !bot.hasAvatarImage; return ( void applyAvatar({ avatarShape: shape, avatarImageArtifactId: null }) } style={{ width: 52, height: 68, borderRadius: 12, borderWidth: 1, borderColor: selected ? "#5A5A62" : "#26262A", backgroundColor: selected ? "#1A1A1D" : "transparent", alignItems: "center", justifyContent: "center", }} > {AVATAR_SHAPE_LABELS[shape]} ); })} {BOT_COLORS.map((swatch) => { const selected = bot.color.toLowerCase() === swatch.toLowerCase(); return ( void applyAvatar({ color: swatch })} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: swatch, borderWidth: selected ? 2 : 0, borderColor: "#ECECEE", }} /> ); })} { const generated = nextGeneratedAvatarFace({ shape: bot.avatarShape, color: bot.color, }); void applyAvatar({ avatarShape: generated.shape, color: generated.color, avatarImageArtifactId: null, }); }} > Generate void uploadAvatar()}> Upload void applyAvatar({ avatarShape: null, avatarImageArtifactId: null })} > Reset ) : null} Name Title Description { const status = await rpc("computer/status", { botId }); setComputer(status); }} /> {error ? {error} : null} void save()} disabled={!name.trim() || pending || !bot} style={{ marginTop: 24, backgroundColor: "#F1F1EF", borderRadius: 11, padding: 16, alignItems: "center", opacity: !name.trim() || pending || !bot ? 0.4 : 1, }} > {pending ? "Saving…" : "Save"} ); }