feat(chat): add ChatBubble widget and mock data for home screen

- Add ChatBubble reusable widget for chat messages
- Add HomeMockData for chat list mock data
- Add HomeScreen widget tests
- Add AG-UI chat design and implementation plan docs
- Add friendship design docs
- Ignore backend/logs directory
This commit is contained in:
qzl
2026-02-28 14:47:33 +08:00
parent 534efd182b
commit c3192a2431
8 changed files with 4475 additions and 0 deletions
@@ -0,0 +1,295 @@
import 'package:flutter/material.dart';
import '../../../shared/widgets/chat_bubble.dart';
enum ChatItemType { message, schedule }
abstract class ChatListItem {
String get id;
DateTime get timestamp;
ChatItemType get type;
MessageSender get sender;
}
class HomeMockData {
static List<ChatListItem> getTodayItems() {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
return _getMockItems().where((item) {
final itemDate = DateTime(
item.timestamp.year,
item.timestamp.month,
item.timestamp.day,
);
return itemDate == today;
}).toList();
}
static Future<List<ChatListItem>> loadMoreItems(DateTime beforeDate) async {
return _getOlderMockItems(beforeDate);
}
static List<ChatListItem> _getMockItems() {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final todayStart = DateTime(today.year, today.month, today.day);
return [
ChatMessageItem(
id: 'm4',
content: '明天提醒我开会',
timestamp: todayStart.add(const Duration(hours: 14)),
sender: MessageSender.user,
),
ScheduleItemWrapper(
id: 's1',
scheduleItem: ScheduleItemModel(
id: 's1',
title: '产品评审会议',
description: '讨论Q2产品路线图',
startAt: todayStart.add(const Duration(days: 1, hours: 10)),
endAt: todayStart.add(const Duration(days: 1, hours: 11)),
timezone: 'Asia/Shanghai',
sourceType: ScheduleSourceType.agentGenerated,
status: ScheduleStatus.active,
metadata: ScheduleMetadata(
color: '#4F46E5',
location: '会议室A / 在线',
notes: '需要提前准备Q2数据',
attachments: [
Attachment(
name: 'Q2路线图.pdf',
type: AttachmentType.document,
url: 'https://example.com/q2.pdf',
),
],
),
createdAt: todayStart.subtract(const Duration(hours: 5)),
),
timestamp: todayStart.add(const Duration(hours: 14, minutes: 2)),
sender: MessageSender.ai,
),
ChatMessageItem(
id: 'm5',
content: '已为你创建日程"产品评审会议",明天上午10:00。我还会提前15分钟提醒你。',
timestamp: todayStart.add(const Duration(hours: 14, minutes: 2)),
sender: MessageSender.ai,
),
];
}
static List<ChatListItem> _getOlderMockItems(DateTime beforeDate) {
final before = DateTime(beforeDate.year, beforeDate.month, beforeDate.day);
final dayBefore = before.subtract(const Duration(days: 1));
return [
ChatMessageItem(
id: 'm1',
content: '你好,我有什么可以帮你的?',
timestamp: dayBefore.add(const Duration(hours: 10)),
sender: MessageSender.ai,
),
ChatMessageItem(
id: 'm2',
content: '下周一之前提交项目报告',
timestamp: dayBefore.add(const Duration(hours: 9, minutes: 55)),
sender: MessageSender.user,
),
ScheduleItemWrapper(
id: 's0',
scheduleItem: ScheduleItemModel(
id: 's0',
title: '提交项目报告',
description: '完成并提交Q2项目报告',
startAt: before.subtract(const Duration(days: 3)),
endAt: null,
timezone: 'Asia/Shanghai',
sourceType: ScheduleSourceType.agentGenerated,
status: ScheduleStatus.active,
metadata: ScheduleMetadata(
color: '#F59E0B',
location: null,
notes: '记得附上数据附件',
attachments: [],
),
createdAt: dayBefore.add(const Duration(hours: 9, minutes: 50)),
),
timestamp: dayBefore.add(const Duration(hours: 9, minutes: 50)),
sender: MessageSender.ai,
),
ChatMessageItem(
id: 'm3',
content: '好的,我已帮你创建待办事项"提交项目报告",截止日期为下周一。我还会提醒你完成这项任务。',
timestamp: dayBefore.add(const Duration(hours: 9, minutes: 50)),
sender: MessageSender.ai,
),
];
}
}
class ChatMessageItem extends ChatListItem {
@override
final String id;
final String content;
@override
final DateTime timestamp;
@override
final MessageSender sender;
ChatMessageItem({
required this.id,
required this.content,
required this.timestamp,
required this.sender,
});
@override
ChatItemType get type => ChatItemType.message;
}
class ScheduleItemWrapper extends ChatListItem {
@override
final String id;
final ScheduleItemModel scheduleItem;
@override
final DateTime timestamp;
@override
final MessageSender sender;
ScheduleItemWrapper({
required this.id,
required this.scheduleItem,
required this.timestamp,
required this.sender,
});
@override
ChatItemType get type => ChatItemType.schedule;
}
enum ScheduleSourceType { manual, imported, agentGenerated }
enum ScheduleStatus { active, completed, canceled, archived }
class ScheduleItemModel {
final String id;
final String title;
final String? description;
final DateTime startAt;
final DateTime? endAt;
final String timezone;
final ScheduleSourceType sourceType;
final ScheduleStatus status;
final ScheduleMetadata? metadata;
final DateTime createdAt;
ScheduleItemModel({
required this.id,
required this.title,
this.description,
required this.startAt,
this.endAt,
required this.timezone,
required this.sourceType,
required this.status,
this.metadata,
required this.createdAt,
});
factory ScheduleItemModel.fromJson(Map<String, dynamic> json) {
return ScheduleItemModel(
id: json['id'],
title: json['title'],
description: json['description'],
startAt: DateTime.parse(json['start_at']),
endAt: json['end_at'] != null ? DateTime.parse(json['end_at']) : null,
timezone: json['timezone'] ?? 'UTC',
sourceType: ScheduleSourceType.values.firstWhere(
(e) => e.name == json['source_type'],
orElse: () => ScheduleSourceType.manual,
),
status: ScheduleStatus.values.firstWhere(
(e) => e.name == json['status'],
orElse: () => ScheduleStatus.active,
),
metadata: json['metadata'] != null
? ScheduleMetadata(
color: json['metadata']['color'],
location: json['metadata']['location'],
notes: json['metadata']['notes'],
attachments:
(json['metadata']['attachments'] as List<dynamic>?)
?.map(
(a) => Attachment(
name: a['name'],
type: a['type'] == 'document'
? AttachmentType.document
: AttachmentType.reminder,
url: a['url'],
content: a['content'],
note: a['note'],
),
)
.toList() ??
[],
)
: null,
createdAt: DateTime.parse(json['created_at']),
);
}
}
class ScheduleMetadata {
final String? color;
final String? location;
final String? notes;
final List<Attachment> attachments;
ScheduleMetadata({
this.color,
this.location,
this.notes,
this.attachments = const [],
});
}
enum AttachmentType { document, reminder }
class Attachment {
final String name;
final AttachmentType type;
final String? url;
final String? content;
final String? note;
Attachment({
required this.name,
required this.type,
this.url,
this.content,
this.note,
});
}
extension ScheduleSourceTypeExtension on ScheduleSourceType {
String get displayName {
switch (this) {
case ScheduleSourceType.manual:
return '手动创建';
case ScheduleSourceType.imported:
return '导入';
case ScheduleSourceType.agentGenerated:
return 'AI生成';
}
}
IconData get icon {
switch (this) {
case ScheduleSourceType.manual:
return Icons.edit_calendar;
case ScheduleSourceType.imported:
return Icons.download;
case ScheduleSourceType.agentGenerated:
return Icons.auto_awesome;
}
}
}