Product name: Panel Agent
After the Smart Life OEM app and optional smart devices are ready, create an agent on the AI Agent Platform.








A product defines the data points (DPs) and agents carried by the panel and the device. Before you start panel development, create an AI plush-toy product, define its DPs, associate the required agents, and then implement these DPs in the panel.
First, sign in to the Tuya Developer Platform and create a product.










Panel miniapps are managed on the Smart MiniApp Developer Platform. Go to the developer platform, register, and sign in.
For step-by-step instructions, see Create panel miniapp.
Open Tuya MiniApp IDE and create a project with the AI Agent MiniApp template.
For detailed steps, see Initialize project.
After completing the steps above, the panel miniapp template is initialized. The project directory is organized as follows:
├── src
│ ├── api // Aggregate file for all cloud API requests
│ │ ├── atop.ts // ATOP request wrapper
│ │ ├── getCachedLaunchOptions.ts // Cached launch options
│ │ ├── getCachedSystemInfo.ts // Cached system info
│ │ ├── index_highway.ts // Agent-related requests
│ │ ├── panelAgent // Agent API module
│ │ │ ├── apis.ts // Agent API definitions
│ │ │ ├── index.ts // Agent API exports
│ │ │ ├── types.d.ts // Agent type definitions
│ │ │ ├── universal.ts // Common API helpers
│ │ │ ├── utils.ts // Agent utilities
│ │ ├── request.ts // Request helper
│ ├── components
│ │ ├── Avatar // Avatar component
│ │ ├── AvatarBar // Avatar bar
│ │ ├── Battery // Battery component
│ │ ├── BottomButton // Bottom button
│ │ ├── DialogConfirm // Confirmation dialog
│ │ ├── DialogInput // Text input dialog
│ │ ├── DialogPicker // DP picker dialog
│ │ ├── GridBattery // Battery grid
│ │ ├── icon-font // SVG icon container
│ │ ├── Modal // Generic modal
│ │ ├── NoData // Empty state component
│ │ ├── PickerItem // Generic picker item
│ │ ├── SearchBar // Search bar
│ │ ├── SubTopBar // Sub-page top bar
│ │ ├── Tag // Tag subcomponent
│ │ ├── TagBar // Tag bar
│ │ ├── Text // Text component
│ │ ├── TopBar // Top bar
│ │ ├── TouchableOpacity // Button component
│ │ ├── WifiSignal // Wi-Fi signal
│ │ ├── index.ts // Component exports
│ ├── constant
│ │ ├── dpCodes.ts // dpCode constants
│ │ ├── index.ts // Constant definitions
│ ├── devices // Device models
│ │ ├── index.ts // Device exports
│ │ ├── protocols // Device protocols
│ │ │ ├── index.ts // Protocol exports
│ │ ├── schema.ts // Schema definitions
│ ├── hooks // Hooks
│ │ ├── useAgentLanguages.ts // Agent language hook
│ │ ├── useChatEmotion.ts // Chat emotion hook
│ │ ├── usePanelConfig.ts // Panel config hook
│ │ ├── useSelectorWithEquality.ts // Redux selector hook
│ │ ├── useSystemInfo.tsx // System info hook
│ │ ├── useWakeWord.ts // Wake-word hook
│ ├── i18n // Internationalization
│ │ ├── index.ts // i18n exports
│ │ ├── strings.ts // Localized strings
│ ├── image // Image assets
│ │ ├── bg-light@3x.png // Background
│ ├── pages
│ │ ├── AvatarSelect // Avatar selection page
│ │ ├── CustomAgentEdit // Add/Edit role page
│ │ ├── DialogHistory // Chat history page
│ │ │ ├── DialogSingleContentNew // Conversation component
│ │ ├── HomeRole // Role homepage
│ │ │ ├── BaseInfoCard // Basic info card
│ │ │ ├── HomeTopBar // Homepage top bar
│ │ │ ├── RoleMemoryEntry // Memory entry
│ │ ├── RoleChange // Role switch page
│ │ │ ├── RoleItem // Role list item
│ │ ├── RoleMemory // Role memory page
│ │ │ ├── ChatSummary // Chat summary
│ │ │ ├── MemoryFormat // Memory format
│ │ ├── VoiceSquare // Voice square page
│ │ │ ├── SliderValueItem // Slider item
│ │ │ ├── VoiceItem // Voice item
│ ├── redux // Redux
│ │ ├── index.ts // Redux exports
│ │ ├── modules // Redux modules
│ ├── res // Assets & SVGs
│ │ ├── agent // Agent images
│ │ ├── iconfont // Icon font files
│ │ ├── signal // Signal icons
│ │ ├── iconsvg.ts // SVG definitions
│ │ ├── index.ts // Asset exports
│ ├── styles // Global styles
│ │ ├── index.less // Style entry
│ ├── types // Global types
│ │ ├── index.ts // Type exports
│ ├── utils // Utility helpers
│ │ ├── index.ts // Utility exports
│ │ ├── string.js // String helpers
│ │ ├── time.ts // Time helpers
│ ├── app.config.ts // App config
│ ├── app.less // App styles
│ ├── app.tsx // App entry
│ ├── composeLayout.tsx // Handle sub-device events and DP changes
│ ├── global.config.ts // Global config
│ ├── global.less // Global styles
│ ├── mixins.less // Less mixins
│ ├── routes.config.ts // Routes
│ ├── theme.json // Theme config
│ ├── variables.less // Less variables

Call the getAIAgentRoles API to obtain the role currently bound to the device.
// Request the currently bound role
const getBindAgentRoleData = async () => {
getBindAgentRoleLocal()
.then((res: any) => {
dispatch(initBindAgentRole(res));
if (res?.bindRoleType === 1) {
dispatch(updateRoleInfo({ roleTemplateId: res?.templateId }));
}
// bindRoleType indicates whether multi-role is supported:
// 0 custom, 1 template, 2 single-role only
dispatch(initSupportMultiRole(res?.bindRoleType !== 2));
initLoading && setInitLoading(false);
setIsUnconfigured(false);
})
.catch((err) => {
console.log("getBindAgentRoleLocal::err::", err);
setInitLoading(false);
setIsUnconfigured(true);
});
};
By listening for device messages, you can keep track of the emotional state of the current role.
useEffect(() => {
registerMQTTDeviceListener({ deviceId: getDevInfo().devId });
const handleCallback = async ({ messageData }) => {
try {
const { bizType, data } = messageData;
const { code, custom } = data;
if (bizType === "SKILL" && code === "emo") {
setEmojiUrl(custom?.url);
// Refresh the chat info at the top
getBindAgentRoleData();
setChatEmotion((state) => ({
...state,
disableTime: true,
}));
}
} catch (err) {
console.log(err);
}
};
onMqttMessageReceived(handleCallback);
return () => {
console.log("============================");
unregisterMQTTDeviceListener({ deviceId: getDevInfo().devId });
offMqttMessageReceived(handleCallback);
};
}, []);

Retrieve both the recommended roles and the custom roles.
const getCustomRoleFunc = (params: GetListParams) => {
return new Promise((resolve, reject) => {
getCustomRoleListLocal({})
.then((res: VoiceRes) => {
const { totalPage, list = [] } = res;
// Avoid flashing "no role"
setTimeout(() => {
setLoading(false);
}, 200);
resolve({
total: totalPage,
list,
});
})
.catch((error) => {
setLoading(false);
reject(error);
});
});
};
const initRoleList = () => {
getRecommendRoleListLocal()
.then((res) => {
setResList(res);
setTimeout(() => {
setLoading(false);
}, 200);
})
.catch((error) => {
setLoading(false);
});
};
Tap an item in the list to switch the current role.
const handleItemChecked = async (roleId: string, item: any) => {
// bindRoleType: 0 custom, 1 template, 2 single-role
const params = { roleId, bindRoleType: tag === "custom" ? 0 : 1 };
postBindRoleLocal(params)
.then(() => {
dispatch(updateRoleInfo({ roleId }));
// Refresh the homepage after switching
emitter.emit("refreshDialogData", "");
navigateBack({ delta: 1 });
})
.catch((error) => {
console.log(error);
});
};
const deleteAIRole = (roleId: string) => {
showLoading({
title: "",
});
deleteAIAgentRoles(roleId)
.then(() => {
hideLoading();
showToast({
title: Strings.getLang("dsc_delete_success"),
icon: "success",
});
// Refresh the custom list after deletion
initData("");
})
.catch((err) => {
hideLoading();
console.log("deleteAIAgentRoles::err::", err);
showToast({
title: Strings.getLang("dsc_delete_fail"),
icon: "error",
});
});
setUnbindId("-1");
setIsShowUnbindConfirm(false);
};

Use this page to update role information (avatar, name, introduction, and memory), and select the timbre, language, and large model.
You can pre-configure templates for B-end users, so that they can quickly pick a baseline role.
// Get the role template list
const getTemplateList = async () => {
if (source === "createRole") {
return;
}
getAIAgentRolesTemplateList()
.then((res: any) => {
console.log("==getTemplateList", res);
setTemplateList(res);
if (res?.length > 0) {
setTemplateRoleId(res[0].roleId);
}
})
.catch((err) => {
console.log("getTemplateList::err::", err);
});
};
// Get template details
const getTemplateListDetail = async (
roleId: string,
onlyReduxUpdate?: boolean
) => {
if (CustomAgentEditForbiddenGetDetail.current) return;
getCommonRoleDetailLocal(roleId)
.then((res: any) => {
console.log("==getTemplateListDetail", res);
if (onlyReduxUpdate) {
dispatch(initRoleDetail(res));
} else {
initRoleData(res);
initRoleInfoData();
}
})
.catch((err) => {
console.log("getTemplateListDetail::err::", err, roleId);
});
};
When editing a role, retrieve its full details.
const getAgentRoleDetail = async (
roleId: string,
onlyReduxUpdate?: boolean
) => {
if (CustomAgentEditForbiddenGetDetail.current) return;
getCustomRoleDetailLocal(roleId)
.then((res: any) => {
console.log("==getCustomRoleDetailLocal", res);
if (onlyReduxUpdate) {
dispatch(initRoleDetail(res));
} else {
initRoleData(res);
initRoleInfoData();
}
})
.catch((err) => {
console.log("getCustomRoleDetailLocal::err::", err);
});
};
Handle the logic for creating or editing roles.
// After creating or editing a role, refresh the custom list
/* Editing flows:
* 1. Custom roles: updateCustomRole -> refresh list ✅
* 2. Recommended roles: createFromTemplate -> jump to custom tab ✅
* From homepage:
* 1. Custom roles: update -> refresh homepage ✅
* 2. Recommended roles: create from template -> jump to custom tab ✅
* Creating roles:
* 1. Default values are pre-filled ✅
* 2. Tag determines whether to call template or custom API ✅
*/
try {
showLoading({
title: "",
});
setLoading(true);
if (roleId) {
params.roleId = roleId;
params.speed = roleDetail?.speed || 33;
params.tone = roleDetail?.tone || 33;
console.log("roleDetail::1", roleDetail);
await postUpdateCustomRoleLocal(params);
setTimeout(() => {
emitter.emit("refreshRoleList", "");
}, 500);
} else if (templateRoleId && source !== "createRole") {
params.roleId = templateRoleId;
params.speed = roleDetail?.speed || 33;
params.tone = roleDetail?.tone || 33;
console.log("roleDetail::2", roleDetail);
if (needBind === "true") {
params.needBind = true;
}
await postCreateRoleFromTemplateLocal(params);
setTimeout(() => {
emitter.emit("refreshRoleList", { toCustomTab: true });
}, 500);
} else if (source === "createRole") {
params.speed = roleDetail?.speed || 33;
params.tone = roleDetail?.tone || 33;
console.log("roleDetail::3", roleDetail);
await postCreateCustomRoleLocal(params);
setTimeout(() => {
emitter.emit("refreshRoleList", { toCustomTab: true });
}, 500);
}
showToast({
title: Strings.getLang("save_success"),
icon: "success",
});
// Refresh the homepage after editing from the homepage
emitter.emit("refreshDialogData", "");
navigateBack({ delta: 1 });
setTimeout(() => {
hideLoading();
setLoading(false);
}, 500);
} catch (error) {
hideLoading();
setLoading(false);
if (platform === "android") {
showToast({
title: error?.innerError?.errorMsg,
icon: "error",
});
} else if (platform === "ios") {
showToast({
title: iOSExtractErrorMessage(error?.innerError?.errorMsg),
icon: "error",
});
}
}
Provide pre-configured avatars for users to choose from.
// Avatars
const getBoundAgentsFunc = async () => {
getAvatarListLocal()
.then((res) => {
console.log("getAIAvatars::", res);
setAvatarList(res);
})
.catch((err) => {
console.log("getAIAvatars::err::", err);
});
};
useEffect(() => {
getBoundAgentsFunc();
}, []);
Fetch the supported languages.
// Get supported languages
const useAgentLanguages = (id: number) => {
const [langRangeList, setSupportLangs] = useState<Array<{ key: string; dataString: string }>>([]);
useEffect(() => {
const fetchSupportedLangs = async () => {
try {
const response = await getLanguageListLocal();
if (response) {
setSupportLangs(
response?.map(item => ({
dataString: item?.langName,
key: item?.langCode,
}))
);
}
} catch (error) {
console.error('Failed to fetch supported languages:', error);
}
};
fetchSupportedLangs();
}, []);
return { langRangeList };
};
import useAgentLanguages from '@/hooks/useAgentLanguages';
const { langRangeList } = useAgentLanguages(id);
Retrieve the list of large language models.
// Display the latest model list
const response = await getModelListLocal();
let models = [];
if (response) {
models = response?.map((model) => ({
key: model.llmId,
dataString: model.llmName,
}));
}

Switch the timbre associated with the agent role and manage the system-default timbres.
Display the default timbres and allow end users to pick the best match. Filtering by category and searching are supported.
// Request the timbre list
const getVoiceListFunc = (params: GetListParams) => {
return new Promise((resolve, reject) => {
getStandardVoiceList({
pageNo: params.current,
pageSize: params.pageSize,
tag,
agentId,
keyWord: params.searchText,
lang: selectedLang,
})
.then((res: VoiceRes) => {
const { totalPage, list = [] } = res;
hideLoading();
setLoading(false);
resolve({
total: totalPage,
list,
});
})
.catch((error) => {
hideLoading();
setLoading(false);
reject(error);
});
});
};
// Manage the pagination requests
const { pagination, data, run } = usePagination(
({ current, pageSize, searchText }) =>
getVoiceListFunc({
current,
pageSize,
searchText,
}) as Promise<GetStandardVoice>,
{
manual: true,
}
);
// Lazy load updates
useEffect(() => {
if (data?.list) {
pagination.current > 1
? dispatch(updateVoiceList([...data?.list]))
: dispatch(initVoiceList([...data?.list]));
}
}, [data]);
// Keep the same request flow; searchText determines whether it's a search
const { pagination, data, run } = usePagination(
({ current, pageSize, searchText }) =>
getVoiceListFunc({ current, pageSize, searchText }) as Promise<GetStandardVoice>,
{
manual: true,
}
);
// Switch the timbre when editing role details
const handleItemChecked = async (idKey: string, item: any) => {
try {
playVoiceAudio(item?.demoUrl);
dispatch(
updateRoleInfo({
voiceId: idKey,
voiceName: item?.voiceName,
supportLangs: item?.supportLangs,
})
);
// When creating, block template detail updates triggered by roleDetail changes
emitter.emit("CustomAgentEditForbiddenGetDetail", true);
setTimeout(() => {
dispatch(initRoleDetail({ ...roleDetail, speed: item?.speed, tone: item?.tone }));
}, 50);
} catch (err) {
hideLoading();
}
};
// Edit the speech speed in role details
const onChangeToneOrSpeed = (sv: number, tv: number) => {
if (!isEditMode) {
// Prevent template detail updates triggered by roleDetail changes
emitter.emit("CustomAgentEditForbiddenGetDetail", true);
setTimeout(() => {
dispatch(initRoleDetail({ ...roleDetail, speed: sv }));
}, 50);
return;
}
showLoading({
title: "",
});
postUpdateCustomRoleLocal({
roleId,
[speedKey]: `${sv}`,
// [toneKey]: `${tv}`,
})
.then(async (res) => {
sv >= 0 && dispatch(initRoleDetail({ ...roleDetail, speed: sv }));
// tv >= 0 && setToneValue(tv);
hideLoading();
showToast({
title: Strings.getLang("dsc_edit_success"),
icon: "success",
});
})
.catch(() => {
hideLoading();
showToast({
title: Strings.getLang("dsc_edit_fail"),
icon: "error",
});
});
};

Display the conversations between the end user and the agent role. If there is no conversation yet, the template shows an empty placeholder.
// Fetch the chat history
const fetchHistoryData = useCallback(
async (gmtEnd: number, isRefresh = false) => {
if ((loadingUpper && !isRefresh) || (loadingLower && isRefresh)) return;
try {
if (isRefresh) {
setLoadingLower(true);
} else {
setLoadingUpper(true);
}
const response = await fetchPanelAgentChatHistory({
devId: getDevInfo().devId,
bindRoleType: bindAgentRole?.bindRoleType,
roleId: bindAgentRole?.roleId,
gmtEnd,
fetchSize: 10,
timeAsc: false,
});
setRefresherTriggeredState(false);
if (response && response.length > 0) {
// Find the oldest timestamp
const oldestTime = Math.min(
...response.map((item) => item.gmtCreate)
);
oldestGmtCreate.current = oldestTime;
// Add parsedTime for display
const processedList = response
.map((item) => ({
...item,
parsedTime: formatTimeByTimezone(item.gmtCreate, timezoneId),
}))
.filter((item) => item.parsedTime !== null);
if (isRefresh) {
// Append to the end (latest data) and sort chronologically
setHistoryList((prev) => {
const combined = [...prev, ...processedList];
const uniqList = _.uniqBy(combined, "requestId");
const sortedList = uniqList.sort(
(a, b) => a.gmtCreate - b.gmtCreate
);
if (sortedList.length > 0) {
oldestGmtCreate.current = sortedList[0].gmtCreate;
}
return sortedList;
});
} else {
// Prepend older data when loading more
setHistoryList((prev) => {
const combined = [...processedList, ...prev];
const uniqList = _.uniqBy(combined, "requestId");
return uniqList.sort((a, b) => a.gmtCreate - b.gmtCreate);
});
}
setTimeout(() => {
setEmptyIdState("bottomView");
}, 500);
} else {
// No more data
}
} catch (error: any) {
setRefresherTriggeredState(false);
if (platform === "android") {
showToast({
title: error?.innerError?.errorMsg || error?.errorMsg,
icon: "error",
});
} else if (platform === "ios") {
showToast({
title: error?.innerError?.errorMsg
? iOSExtractErrorMessage(error?.innerError?.errorMsg)
: error?.errorMsg,
icon: "error",
});
}
} finally {
setLoadingUpper(false);
setLoadingLower(false);
}
},
[loadingUpper, loadingLower, bindAgentRole, platform, timezoneId]
);
Delete one or more chat messages for the role.
// clearAgentHistoryMessage
const handleDeleteMessage = async (requestIds?: string) => {
try {
showLoading({ title: "" });
await clearAgentHistoryMessage({
bindRoleType: bindAgentRole?.bindRoleType,
roleId: bindAgentRole?.roleId,
requestIds: requestIds || selectedItems?.join(","),
clearAllHistory: false,
});
const newList = historyList.filter(
(item) =>
!selectedItems.includes(item.requestId) &&
item.requestId !== requestIds
);
setHistoryList(newList);
hideLoading();
showToast({
title: Strings.getLang("dsc_delete_chat_history_tip_new"),
icon: "success",
});
} catch (err) {
hideLoading();
showToast({
title: Strings.getLang("dsc_delete_chat_history_failed"),
icon: "error",
});
}
};

Manage role memories, including clearing chat history, removing temporary memories, structured memories, conversation summaries, and long-term memories.
const handleClickDialog = (message: string, type: string) => {
DialogInstance.confirm({
selector: `#smart-dialog-edit`,
message,
cancelButtonText: Strings.getLang("cancel"),
confirmButtonText: Strings.getLang("confirm"),
})
.then(() => {
// Clear chat history
if (type === "clear_chat_history_all") {
clearingHistoryRecord({
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
clearAllHistory: true,
})
.then(() => {
showToast({
title: Strings.getLang("dsc_delete_chat_history_tip"),
icon: "success",
});
})
.catch(() => {
showToast({
title: Strings.getLang("dsc_delete_chat_history_failed"),
icon: "error",
});
});
}
// Clear temporary context
if (type === "clear_context") {
clearingContext({
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
})
.then(() => {
showToast({
title: Strings.getLang("clear_context_success"),
icon: "success",
});
})
.catch(() => {
showToast({
title: Strings.getLang("clear_context_failed"),
icon: "error",
});
});
}
// Clear all long-term memories
if (type === "clear_memory") {
clearingMemory({
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
clearAllMemory: true,
})
.then(() => {
showToast({
title: Strings.getLang("clear_memory_success"),
icon: "success",
});
})
.catch(() => {
showToast({
title: Strings.getLang("clear_memory_failed"),
icon: "error",
});
});
}
})
.catch(() => {
console.log("cancel");
});
};

Display role memories and shared household memories, and allow deletions.
// Fetch memories
const getPanelMemoryListData = async () => {
const params = {
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
};
getPanelMemoryListLocal(params)
.then((res: any) => {
setMemoryList(res);
})
.catch((err) => {
console.log("getMemorySwitchLocal::err::", err);
});
};
// Delete memories
const handleDeleteMemory = (memoryKey) => {
clearingMemory({
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
memoryKeys: memoryKey,
clearAllMemory: false,
})
.then(() => {
// Refresh after deletion
getPanelMemoryListData();
})
.catch((error) => {
showToast({
title: error?.innerError?.errorMsg,
icon: "error",
});
});
};

Show and delete conversation summaries.
// Fetch conversation summaries
const getChatMemoryListData = async () => {
const params = {
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
};
getChatMemoryListLocal(params)
.then((res: any) => {
setSummaryList(res);
})
.catch((err) => {
console.log("getMemorySwitchLocal::err::", err);
});
};
// Delete conversation summaries
const handleDeleteMemory = (memoryKey) => {
const leftMemoryList = summaryList?.filter((item) => item !== memoryKey);
const params = {
bindRoleType:
roleMemoryEntry === "custom" ? 0 : bindAgentRole?.bindRoleType,
roleId: roleDetail?.roleId || bindAgentRole?.roleId,
summaryItems: leftMemoryList,
};
postUpdateChatMemoryListLocal(params).then(() => {
// Refresh after deletion
getChatMemoryListData();
});
};