Files
social-app/apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart
T
zl-q 3ac09475ad feat(agent): add voice input capability and standardize tool naming
- Add voice recording with transcribe endpoint (ASR) for multimodal input
- Android: add RECORD_AUDIO and INTERNET permissions
- Refactor tool naming: frontend tools use 'front.' prefix, backend tools use 'back.'
- Migrate calendar tools: create_calendar_event -> back.mutate/list/delete events
- Add calendar_event_list.v1 and calendar_operation.v1 UI card types
- Update all Flutter and Python tests to match new tool naming conventions
- Add record package dependency for voice recording
2026-03-09 00:10:09 +08:00

340 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:social_app/core/theme/design_tokens.dart';
import '../../data/models/tool_result.dart';
/// 卡片类型常量
const _calendarCardType = 'calendar_card.v1';
const _calendarListType = 'calendar_event_list.v1';
const _calendarOperationType = 'calendar_operation.v1';
const _errorCardType = 'error_card.v1';
const _aiGeneratedSource = 'ai_generated';
const _agentGeneratedSource = 'agent_generated';
const _primaryActionType = 'primary';
class UiSchemaRenderer {
static Widget render(UiCard card) {
return switch (card.cardType) {
_calendarCardType => _renderCalendarCard(card),
_calendarListType => _renderCalendarList(card),
_calendarOperationType => _renderCalendarOperation(card),
_errorCardType => _renderErrorCard(card),
_ => _renderUnknownCard(card),
};
}
static Widget _renderCalendarCard(UiCard card) {
final data = CalendarCardData.fromJson(card.data);
final color = data.color != null
? Color(int.parse(data.color!.replaceFirst('#', '0xFF')))
: AppColors.blue500;
final isAiGenerated =
data.sourceType == _aiGeneratedSource ||
data.sourceType == _agentGeneratedSource;
return Container(
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.messageCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: AppSpacing.sm,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(AppRadius.lg),
topRight: Radius.circular(AppRadius.lg),
),
),
),
Padding(
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isAiGenerated) ...[
_buildAiTag(),
SizedBox(height: AppSpacing.sm),
],
Text(
_formatTime(data.startAt, data.endAt),
style: TextStyle(fontSize: 12, color: AppColors.slate500),
),
SizedBox(height: AppSpacing.sm),
Text(
data.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
if (data.description != null) ...[
SizedBox(height: AppSpacing.xs),
Text(
data.description!,
style: TextStyle(fontSize: 14, color: AppColors.slate600),
),
],
if (data.location != null) ...[
SizedBox(height: AppSpacing.sm),
_buildLocation(data.location!),
],
if (card.actions != null && card.actions!.isNotEmpty) ...[
SizedBox(height: AppSpacing.md),
_buildActions(card.actions!),
],
],
),
),
],
),
);
}
static Widget _buildAiTag() {
return Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: AppColors.messageTagBg,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'AI生成',
style: TextStyle(fontSize: 10, color: AppColors.blue600),
),
);
}
static Widget _buildLocation(String location) {
return Row(
children: [
Icon(Icons.location_on_outlined, size: 16, color: AppColors.slate500),
SizedBox(width: AppSpacing.xs),
Text(
location,
style: TextStyle(fontSize: 12, color: AppColors.slate500),
),
],
);
}
static Widget _buildActions(List<CardAction> actions) {
return Wrap(
spacing: AppSpacing.sm,
children: actions.map((action) => _buildActionButton(action)).toList(),
);
}
static Widget _buildActionButton(CardAction action) {
final isPrimary = action.type == _primaryActionType;
return GestureDetector(
onTap: () => _handleAction(action),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: isPrimary ? AppColors.blue500 : AppColors.messageBtnWrap,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(
color: isPrimary ? AppColors.blue500 : AppColors.messageBtnBorder,
),
),
child: Text(
action.label,
style: TextStyle(
fontSize: 14,
color: isPrimary ? AppColors.white : AppColors.slate600,
),
),
),
);
}
static Widget _renderCalendarList(UiCard card) {
final rawItems = card.data['items'];
final items = rawItems is List ? rawItems : const [];
final paginationRaw = card.data['pagination'];
final pagination = paginationRaw is Map<String, dynamic>
? paginationRaw
: const <String, dynamic>{};
final page = pagination['page'];
final total = pagination['total'];
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.messageCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日程列表',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
if (page != null || total != null) ...[
SizedBox(height: AppSpacing.xs),
Text(
'${page ?? '-'}页 · 共${total ?? '-'}',
style: TextStyle(fontSize: 12, color: AppColors.slate500),
),
],
SizedBox(height: AppSpacing.sm),
if (items.isEmpty)
Text(
'暂无日程',
style: TextStyle(fontSize: 14, color: AppColors.slate500),
),
for (final item in items)
if (item is Map<String, dynamic>)
Padding(
padding: EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
item['title']?.toString() ?? '未命名日程',
style: TextStyle(fontSize: 14, color: AppColors.slate700),
),
),
],
),
);
}
static Widget _renderCalendarOperation(UiCard card) {
final ok = card.data['ok'] == true;
final operation = card.data['operation']?.toString() ?? 'operation';
final message = card.data['message']?.toString() ?? (ok ? '操作成功' : '操作失败');
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: ok ? AppColors.messageCardBg : AppColors.warningBackground,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: ok ? AppColors.messageCardBorder : AppColors.red400,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日程$operation结果',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ok ? AppColors.slate900 : AppColors.red600,
),
),
SizedBox(height: AppSpacing.xs),
Text(
message,
style: TextStyle(
fontSize: 13,
color: ok ? AppColors.slate600 : AppColors.red600,
),
),
if (card.actions != null && card.actions!.isNotEmpty) ...[
SizedBox(height: AppSpacing.md),
_buildActions(card.actions!),
],
],
),
);
}
static Widget _renderErrorCard(UiCard card) {
final message = card.data['message'] as String? ?? '发生错误';
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.warningBackground,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.red400),
),
child: Row(
children: [
Icon(Icons.error_outline, size: 20, color: AppColors.red600),
SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
message,
style: TextStyle(fontSize: 14, color: AppColors.red600),
),
),
],
),
);
}
static Widget _renderUnknownCard(UiCard card) {
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'未知卡片类型: ${card.cardType}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.slate600,
),
),
SizedBox(height: AppSpacing.sm),
Text(
card.data.toString(),
style: TextStyle(fontSize: 12, color: AppColors.slate500),
),
],
),
);
}
static String _formatTime(String startAt, String? endAt) {
try {
final start = DateTime.parse(startAt);
final buffer = StringBuffer();
buffer.write('${start.month}${start.day}');
buffer.write(
'${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}',
);
if (endAt != null) {
final end = DateTime.parse(endAt);
buffer.write(
' - ${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}',
);
}
return buffer.toString();
} catch (e) {
return startAt;
}
}
static void _handleAction(CardAction action) {
// TODO: 实现 action 处理
}
}