mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-28 03:22:39 +00:00
6314188835
- align Android/iOS native bridge implementations and audio handling paths - improve Bluetooth provisioning/verification flow and related error handling - refactor WebSocket, music, and device utility logic for more stable behavior - clean up noisy debug logs and normalize comments across Flutter and native code - update AR view, dance/agent/device pages, and platform integration details
508 lines
15 KiB
Dart
508 lines
15 KiB
Dart
/*
|
|
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
|
SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:stack_chan/app_state.dart';
|
|
import 'package:stack_chan/view/popup/edit_agent.dart';
|
|
|
|
import '../../model/XiaoZhi/agent.dart';
|
|
import '../../model/XiaoZhi/agent_template.dart';
|
|
import '../../model/XiaoZhi/device.dart';
|
|
import '../../util/XiaoZhi_util.dart';
|
|
|
|
//System1UIConstant - Can
|
|
const double kCardRadius = 16.0;
|
|
const double kDefaultPadding = 16.0;
|
|
const double kDefaultSpacing = 16.0;
|
|
const Color kPrimaryTintColor = CupertinoColors.systemBlue;
|
|
const double kElevation = 2.0;
|
|
|
|
class AgentConfiguration extends StatefulWidget {
|
|
const AgentConfiguration({super.key});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _AgentConfigurationState();
|
|
}
|
|
|
|
class AgentConfigurationModel extends GetxController {
|
|
Rxn<Device> device = Rxn(null);
|
|
RxList<AgentTemplate> agentTemplatesList = RxList([]);
|
|
Rxn<Agent> currentBindAgent = Rxn(null);
|
|
|
|
//search
|
|
int agentListPage = 1;
|
|
RxBool isLoading = false.obs;
|
|
RxBool isDialogLoading = false.obs;
|
|
RxBool isListLoading = false.obs;
|
|
bool hasMoreList = true;
|
|
|
|
Future<void> loadAllData() async {
|
|
isLoading.value = true;
|
|
try {
|
|
await Future.wait([loadDevice(), loadAgentTemplates()]);
|
|
} catch (e) {
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
//loaddeviceinfo
|
|
Future<void> loadDevice() async {
|
|
final devices = await XiaoZhiUtil.shared.getDevice(
|
|
AppState.shared.deviceMac,
|
|
);
|
|
if (devices.isNotEmpty) {
|
|
device.value = devices.first;
|
|
if (device.value?.agent_id != null) {
|
|
await getBindAgent(device.value!.agent_id!);
|
|
} else {
|
|
currentBindAgent.value = null;
|
|
}
|
|
} else {
|
|
device.value = null;
|
|
currentBindAgent.value = null;
|
|
}
|
|
}
|
|
|
|
//Getdeviceboundagentdetails
|
|
Future<void> getBindAgent(int agentId) async {
|
|
final agent = await XiaoZhiUtil.shared.getAgentDetail(agentId);
|
|
currentBindAgent.value = agent;
|
|
}
|
|
|
|
//switchbindagent
|
|
Future<bool> switchBindAgent(
|
|
Agent targetAgent,
|
|
String verificationCode,
|
|
) async {
|
|
if (device.value == null) {
|
|
AppState.shared.showToast("No device detected, cannot bind AI Agent");
|
|
return false;
|
|
}
|
|
if (targetAgent.id == null) {
|
|
AppState.shared.showToast("Invalid AI Agent ID, cannot bind");
|
|
return false;
|
|
}
|
|
if (verificationCode.isEmpty) {
|
|
AppState.shared.showToast("Please enter device verification code");
|
|
return false;
|
|
}
|
|
if (currentBindAgent.value?.id == targetAgent.id) {
|
|
AppState.shared.showToast("Device is already bound to this AI Agent");
|
|
return true;
|
|
}
|
|
|
|
isLoading.value = true;
|
|
try {
|
|
//First / Previouslyunbindcurrentdevice
|
|
if (device.value?.device_id != null) {
|
|
await XiaoZhiUtil.shared.unbindDevice(device.value!.device_id!);
|
|
}
|
|
//bindtonewagent
|
|
final bindSuccess = await XiaoZhiUtil.shared.bindDeviceToAgent(
|
|
targetAgent.id!,
|
|
verificationCode,
|
|
);
|
|
if (!bindSuccess) {
|
|
throw Exception("Failed to bind device to AI Agent");
|
|
}
|
|
//refreshdata
|
|
await loadDevice();
|
|
AppState.shared.showToast("Successfully switched AI Agent");
|
|
return true;
|
|
} catch (e) {
|
|
AppState.shared.showToast(e.toString().replaceAll("Exception: ", ""));
|
|
return false;
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
//loadagent
|
|
Future<void> loadAgentTemplates() async {
|
|
final templates = await XiaoZhiUtil.shared.agentTemplatesList(
|
|
agentListPage,
|
|
20,
|
|
);
|
|
agentTemplatesList.assignAll(templates);
|
|
}
|
|
|
|
//ReplaceDescriptionin
|
|
String replacePlaceholdersInCharacter(Agent agent) {
|
|
if (agent.character == null || agent.character!.isEmpty) {
|
|
return "";
|
|
}
|
|
String characterText = agent.character!;
|
|
if (agent.assistant_name != null && agent.assistant_name!.isNotEmpty) {
|
|
characterText = characterText.replaceAll(
|
|
"{{assistant_name}}",
|
|
agent.assistant_name!,
|
|
);
|
|
}
|
|
if (agent.user_name != null && agent.user_name!.isNotEmpty) {
|
|
characterText = characterText.replaceAll(
|
|
"{{user_name}}",
|
|
agent.user_name!,
|
|
);
|
|
}
|
|
return characterText;
|
|
}
|
|
}
|
|
|
|
class _AgentConfigurationState extends State<AgentConfiguration> {
|
|
late AgentConfigurationModel model = Get.put(AgentConfigurationModel());
|
|
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
@override
|
|
void dispose() {
|
|
scrollController.dispose();
|
|
Get.delete<AgentConfigurationModel>();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
model.loadAllData();
|
|
scrollController.addListener(() {
|
|
if (scrollController.position.pixels >=
|
|
scrollController.position.maxScrollExtent - 200) {
|
|
if (!model.isListLoading.value && model.hasMoreList) {
|
|
model.loadAgentTemplates();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
///showCreate agentpopup
|
|
void goToEditAgentPage({bool isCreate = false, Agent? editAgent}) {
|
|
Navigator.of(context)
|
|
.push(
|
|
CupertinoPageRoute(
|
|
builder: (context) {
|
|
return EditAgent(agent: editAgent);
|
|
},
|
|
),
|
|
)
|
|
.then((_) {
|
|
model.loadAllData();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
//GetCupertinodynamicTheme
|
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
|
final Color surfaceColor = CupertinoColors.systemGroupedBackground
|
|
.resolveFrom(context);
|
|
final Color secondaryTextColor = CupertinoColors.secondaryLabel.resolveFrom(
|
|
context,
|
|
);
|
|
final Color primaryColor = cupertinoTheme.primaryColor;
|
|
|
|
return CupertinoPageScaffold(
|
|
backgroundColor: surfaceColor,
|
|
navigationBar: CupertinoNavigationBar.large(
|
|
largeTitle: Obx(
|
|
() => Text(
|
|
model.currentBindAgent.value?.agent_name ?? "AI Agent",
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
leading: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: const Icon(CupertinoIcons.xmark, size: 24),
|
|
onPressed: () => CupertinoSheetRoute.popSheet(context),
|
|
),
|
|
backgroundColor: surfaceColor,
|
|
trailing: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
onPressed: () => goToEditAgentPage(
|
|
isCreate: false,
|
|
editAgent: model.currentBindAgent.value,
|
|
),
|
|
child: Icon(
|
|
CupertinoIcons.pencil_circle,
|
|
size: 26,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
border: const Border(bottom: BorderSide.none), //remove
|
|
),
|
|
child: Obx(() {
|
|
//Initialloadstate
|
|
if (model.isLoading.value) {
|
|
return const Center(
|
|
child: CupertinoActivityIndicator(radius: 16), //increaseloadindicator
|
|
);
|
|
}
|
|
//Nodevicestate
|
|
if (model.device.value == null) {
|
|
return _buildNoDeviceWidget(secondaryTextColor, primaryColor);
|
|
}
|
|
//MainLayout
|
|
return ListView(
|
|
controller: scrollController,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: kDefaultPadding,
|
|
vertical: 24,
|
|
),
|
|
children: [
|
|
//currentbindagent
|
|
_buildCurrentBindCard(
|
|
cupertinoTheme,
|
|
primaryColor,
|
|
secondaryTextColor,
|
|
),
|
|
const SizedBox(height: 32),
|
|
//increase
|
|
//listtitle
|
|
// const Text(
|
|
// "AI Agent Templates",
|
|
// style: TextStyle(
|
|
// fontSize: 20,
|
|
// fontWeight: FontWeight.w600,
|
|
//letterSpacing: -0.5, // Zoom out
|
|
// ),
|
|
// ),
|
|
// const SizedBox(height: 16),
|
|
//// Agent templatelist
|
|
// _buildAgentTemplatesList(cupertinoTheme, primaryColor),
|
|
//// loadmoreindicator
|
|
// if (model.isListLoading.value)
|
|
// const Padding(
|
|
// padding: EdgeInsets.symmetric(vertical: 24),
|
|
// child: Center(child: CupertinoActivityIndicator(radius: 14)),
|
|
// ),
|
|
const SizedBox(height: 40),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
//currentbindagent - optimize
|
|
Widget _buildCurrentBindCard(
|
|
CupertinoThemeData theme,
|
|
Color primaryColor,
|
|
Color secondaryTextColor,
|
|
) {
|
|
final cardRadius = 40.0;
|
|
|
|
final currentAgent = model.currentBindAgent.value;
|
|
if (currentAgent == null) {
|
|
return ClipRSuperellipse(
|
|
borderRadius: .circular(cardRadius),
|
|
child: Container(
|
|
padding: .all(cardRadius / 2),
|
|
color: CupertinoColors.systemBackground.resolveFrom(context),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.person_badge_plus, //useicon
|
|
size: 56,
|
|
color: secondaryTextColor.withValues(alpha: 0.7),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
"No Agent Bound",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
"Select an Agent template below to bind, or create a custom AI Agent by tapping the edit button in the upper right corner.",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
height: 1.5, //translated comment
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String processedCharacter = model.replacePlaceholdersInCharacter(
|
|
currentAgent,
|
|
);
|
|
|
|
return ClipRSuperellipse(
|
|
borderRadius: .circular(cardRadius),
|
|
child: Container(
|
|
padding: .all(cardRadius / 2),
|
|
color: CupertinoColors.systemBackground.resolveFrom(context),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: .min,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
"Current AI Agent",
|
|
style: TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
Text(
|
|
"ID: ${currentAgent.id}",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: secondaryTextColor.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInfoItem(
|
|
"AI Model",
|
|
currentAgent.llm_model ?? "Not configured",
|
|
secondaryTextColor,
|
|
),
|
|
_buildInfoItem(
|
|
"Voice Tone",
|
|
currentAgent.tts_voice ?? "Not configured",
|
|
secondaryTextColor,
|
|
),
|
|
_buildInfoItem(
|
|
"Speech Speed",
|
|
currentAgent.tts_speech_speed ?? "Normal",
|
|
secondaryTextColor,
|
|
),
|
|
_buildInfoItem(
|
|
"Pitch",
|
|
currentAgent.tts_pitch?.toString() ?? "0",
|
|
secondaryTextColor,
|
|
),
|
|
if (processedCharacter.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Character Profile",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ClipRSuperellipse(
|
|
borderRadius: .circular(cardRadius / 2),
|
|
child: Container(
|
|
padding: .all(cardRadius / 4),
|
|
color: CupertinoColors.systemGroupedBackground
|
|
.resolveFrom(context),
|
|
child: Text(
|
|
processedCharacter,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: secondaryTextColor,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
//Nodevicehintcomponent - optimize
|
|
Widget _buildNoDeviceWidget(Color secondaryTextColor, Color primaryColor) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.nosign,
|
|
size: 72,
|
|
color: secondaryTextColor.withValues(alpha: 0.6),
|
|
),
|
|
const SizedBox(height: 24),
|
|
const Text(
|
|
"No Device Detected",
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.5,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
"We couldn't find any connected devices. Please check your device connection and try again.",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
color: secondaryTextColor,
|
|
height: 1.5,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 32),
|
|
CupertinoButton.filled(
|
|
borderRadius: BorderRadius.circular(12),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
|
onPressed: model.loadDevice,
|
|
child: const Text(
|
|
"Retry Detection",
|
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
//infoItemcomponent - optimize
|
|
Widget _buildInfoItem(String title, String value, Color secondaryTextColor) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: -0.2,
|
|
),
|
|
),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
color: secondaryTextColor,
|
|
letterSpacing: -0.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|