From 12180ff65a16ae423363378b8223395f206af868 Mon Sep 17 00:00:00 2001 From: zl-q Date: Thu, 19 Mar 2026 00:51:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(apps):=20=E6=8B=86=E5=88=86=20UI=20Schema?= =?UTF-8?q?=20=E4=B8=BA=E5=A4=9A=E6=96=87=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/lib/core/schemas/ui_schema.dart | 1365 +---------------- apps/lib/core/schemas/ui_schema/actions.dart | 205 +++ apps/lib/core/schemas/ui_schema/builders.dart | 47 + .../core/schemas/ui_schema/common_types.dart | 273 ++++ apps/lib/core/schemas/ui_schema/document.dart | 128 ++ apps/lib/core/schemas/ui_schema/enums.dart | 104 ++ apps/lib/core/schemas/ui_schema/nodes.dart | 595 +++++++ 7 files changed, 1358 insertions(+), 1359 deletions(-) create mode 100644 apps/lib/core/schemas/ui_schema/actions.dart create mode 100644 apps/lib/core/schemas/ui_schema/builders.dart create mode 100644 apps/lib/core/schemas/ui_schema/common_types.dart create mode 100644 apps/lib/core/schemas/ui_schema/document.dart create mode 100644 apps/lib/core/schemas/ui_schema/enums.dart create mode 100644 apps/lib/core/schemas/ui_schema/nodes.dart diff --git a/apps/lib/core/schemas/ui_schema.dart b/apps/lib/core/schemas/ui_schema.dart index 2391674..f1401eb 100644 --- a/apps/lib/core/schemas/ui_schema.dart +++ b/apps/lib/core/schemas/ui_schema.dart @@ -6,1362 +6,9 @@ /// Version: 1.0 library; -// ========== Enums ========== - -enum SchemaType { - toolResult('tool_result'), - agentResponse('agent_response'), - notification('notification'); - - final String value; - const SchemaType(this.value); -} - -enum UiStatus { - info('info'), - success('success'), - warning('warning'), - error('error'), - pending('pending'); - - final String value; - const UiStatus(this.value); -} - -enum IconSource { - icon('icon'), - emoji('emoji'), - url('url'); - - final String value; - const IconSource(this.value); -} - -enum OperationType { - create('create'), - update('update'), - delete('delete'), - execute('execute'); - - final String value; - const OperationType(this.value); -} - -enum OperationResult { - success('success'), - failure('failure'), - partial('partial'); - - final String value; - const OperationResult(this.value); -} - -enum ContainerDirection { - vertical('vertical'), - horizontal('horizontal'); - - final String value; - const ContainerDirection(this.value); -} - -enum TextFormat { - plain('plain'), - markdown('markdown'); - - final String value; - const TextFormat(this.value); -} - -enum KvLayout { - vertical('vertical'), - horizontal('horizontal'), - grid('grid'); - - final String value; - const KvLayout(this.value); -} - -enum BadgeVariant { - def('default'), - success('success'), - warning('warning'), - error('error'), - info('info'); - - final String value; - const BadgeVariant(this.value); -} - -enum ActionStyle { - primary('primary'), - secondary('secondary'), - ghost('ghost'), - danger('danger'); - - final String value; - const ActionStyle(this.value); -} - -enum RendererTheme { - def('default'), - dark('dark'), - light('light'); - - final String value; - const RendererTheme(this.value); -} - -// ========== Common Types ========== - -class UiIcon { - final IconSource source; - final String value; - final String? color; - final int? size; - - const UiIcon({ - required this.source, - required this.value, - this.color, - this.size, - }); - - factory UiIcon.fromJson(Map json) { - return UiIcon( - source: IconSource.values.firstWhere( - (e) => e.value == json['source'], - orElse: () => IconSource.icon, - ), - value: json['value'] as String? ?? '', - color: json['color'] as String?, - size: json['size'] as int?, - ); - } - - Map toJson() { - return { - 'source': source.value, - 'value': value, - if (color != null) 'color': color, - if (size != null) 'size': size, - }; - } -} - -class UiBadge { - final String label; - final BadgeVariant variant; - - const UiBadge({required this.label, this.variant = BadgeVariant.def}); - - factory UiBadge.fromJson(Map json) { - return UiBadge( - label: json['label'] as String? ?? '', - variant: BadgeVariant.values.firstWhere( - (e) => e.value == json['variant'], - orElse: () => BadgeVariant.def, - ), - ); - } - - Map toJson() { - return {'label': label, 'variant': variant.value}; - } -} - -class Pagination { - final int page; - final int pageSize; - final int total; - final bool hasMore; - - const Pagination({ - required this.page, - required this.pageSize, - required this.total, - required this.hasMore, - }); - - factory Pagination.fromJson(Map json) { - return Pagination( - page: json['page'] as int? ?? 1, - pageSize: json['pageSize'] as int? ?? 20, - total: json['total'] as int? ?? 0, - hasMore: json['hasMore'] as bool? ?? false, - ); - } - - Map toJson() { - return { - 'page': page, - 'pageSize': pageSize, - 'total': total, - 'hasMore': hasMore, - }; - } -} - -class ActionConfirm { - final String? title; - final String? message; - final String? confirmLabel; - final String? cancelLabel; - - const ActionConfirm({ - this.title, - this.message, - this.confirmLabel, - this.cancelLabel, - }); - - factory ActionConfirm.fromJson(Map json) { - return ActionConfirm( - title: json['title'] as String?, - message: json['message'] as String?, - confirmLabel: json['confirmLabel'] as String?, - cancelLabel: json['cancelLabel'] as String?, - ); - } - - Map toJson() { - return { - if (title != null) 'title': title, - if (message != null) 'message': message, - if (confirmLabel != null) 'confirmLabel': confirmLabel, - if (cancelLabel != null) 'cancelLabel': cancelLabel, - }; - } -} - -class KeyValuePair { - final String key; - final String? label; - final dynamic value; - final bool? copyable; - - const KeyValuePair({ - required this.key, - this.label, - required this.value, - this.copyable, - }); - - factory KeyValuePair.fromJson(Map json) { - return KeyValuePair( - key: json['key'] as String? ?? '', - label: json['label'] as String?, - value: json['value'], - copyable: json['copyable'] as bool?, - ); - } - - Map toJson() { - return { - 'key': key, - if (label != null) 'label': label, - 'value': value, - if (copyable != null) 'copyable': copyable, - }; - } -} - -class TableColumn { - final String key; - final String label; - final String? width; - final String? align; - - const TableColumn({ - required this.key, - required this.label, - this.width, - this.align, - }); - - factory TableColumn.fromJson(Map json) { - return TableColumn( - key: json['key'] as String? ?? '', - label: json['label'] as String? ?? '', - width: json['width'] as String?, - align: json['align'] as String?, - ); - } - - Map toJson() { - return { - 'key': key, - 'label': label, - if (width != null) 'width': width, - if (align != null) 'align': align, - }; - } -} - -class TableRow { - final String id; - final Map cells; - final Map? metadata; - final List? actions; - - const TableRow({ - required this.id, - required this.cells, - this.metadata, - this.actions, - }); - - factory TableRow.fromJson(Map json) { - return TableRow( - id: json['id'] as String? ?? '', - cells: json['cells'] as Map? ?? {}, - metadata: json['metadata'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'cells': cells, - if (metadata != null) 'metadata': metadata, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class ListItem { - final String id; - final String title; - final String? subtitle; - final String? description; - final UiIcon? icon; - final UiBadge? badge; - final Map? metadata; - final List? actions; - - const ListItem({ - required this.id, - required this.title, - this.subtitle, - this.description, - this.icon, - this.badge, - this.metadata, - this.actions, - }); - - factory ListItem.fromJson(Map json) { - return ListItem( - id: json['id'] as String? ?? '', - title: json['title'] as String? ?? '', - subtitle: json['subtitle'] as String?, - description: json['description'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - badge: json['badge'] != null - ? UiBadge.fromJson(json['badge'] as Map) - : null, - metadata: json['metadata'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'title': title, - if (subtitle != null) 'subtitle': subtitle, - if (description != null) 'description': description, - if (icon != null) 'icon': icon!.toJson(), - if (badge != null) 'badge': badge!.toJson(), - if (metadata != null) 'metadata': metadata, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -// ========== Action Types ========== - -abstract class ActionSpec { - String get type; - Map toJson(); -} - -class NavigateAction implements ActionSpec { - final String path; - final Map? params; - - const NavigateAction({required this.path, this.params}); - - @override - String get type => 'navigation'; - - @override - Map toJson() { - return {'type': type, 'path': path, if (params != null) 'params': params}; - } -} - -class LinkAction implements ActionSpec { - final String url; - final String? target; - - const LinkAction({required this.url, this.target}); - - @override - String get type => 'url'; - - @override - Map toJson() { - return {'type': type, 'url': url, if (target != null) 'target': target}; - } -} - -class EventAction implements ActionSpec { - final String event; - final Map? payload; - - const EventAction({required this.event, this.payload}); - - @override - String get type => 'event'; - - @override - Map toJson() { - return { - 'type': type, - 'event': event, - if (payload != null) 'payload': payload, - }; - } -} - -class ToolAction implements ActionSpec { - final String toolId; - final Map? params; - - const ToolAction({required this.toolId, this.params}); - - @override - String get type => 'tool'; - - @override - Map toJson() { - return { - 'type': type, - 'toolId': toolId, - if (params != null) 'params': params, - }; - } -} - -class CopyAction implements ActionSpec { - final String content; - final String? successMessage; - - const CopyAction({required this.content, this.successMessage}); - - @override - String get type => 'copy'; - - @override - Map toJson() { - return { - 'type': type, - 'content': content, - if (successMessage != null) 'successMessage': successMessage, - }; - } -} - -class PayloadAction implements ActionSpec { - final Map payload; - final String? submitTo; - - const PayloadAction({required this.payload, this.submitTo}); - - @override - String get type => 'payload'; - - @override - Map toJson() { - return { - 'type': type, - 'payload': payload, - if (submitTo != null) 'submitTo': submitTo, - }; - } -} - -ActionSpec actionSpecFromJson(Map json) { - final type = json['type'] as String? ?? ''; - switch (type) { - case 'navigation': - return NavigateAction( - path: json['path'] as String? ?? '', - params: json['params'] as Map?, - ); - case 'url': - return LinkAction( - url: json['url'] as String? ?? '', - target: json['target'] as String?, - ); - case 'event': - return EventAction( - event: json['event'] as String? ?? '', - payload: json['payload'] as Map?, - ); - case 'tool': - return ToolAction( - toolId: json['toolId'] as String? ?? '', - params: json['params'] as Map?, - ); - case 'copy': - return CopyAction( - content: json['content'] as String? ?? '', - successMessage: json['successMessage'] as String?, - ); - case 'payload': - return PayloadAction( - payload: json['payload'] as Map? ?? {}, - submitTo: json['submitTo'] as String?, - ); - default: - return EventAction(event: 'unknown'); - } -} - -// ========== Action ========== - -class UiAction { - final String id; - final String label; - final UiIcon? icon; - final ActionStyle? style; - final bool disabled; - final ActionSpec action; - final ActionConfirm? confirm; - - const UiAction({ - required this.id, - required this.label, - this.icon, - this.style, - this.disabled = false, - required this.action, - this.confirm, - }); - - factory UiAction.fromJson(Map json) { - return UiAction( - id: json['id'] as String? ?? '', - label: json['label'] as String? ?? '', - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - style: json['style'] != null - ? ActionStyle.values.firstWhere( - (e) => e.value == json['style'], - orElse: () => ActionStyle.primary, - ) - : null, - disabled: json['disabled'] as bool? ?? false, - action: actionSpecFromJson( - json['action'] as Map? ?? {'type': 'event'}, - ), - confirm: json['confirm'] != null - ? ActionConfirm.fromJson(json['confirm'] as Map) - : null, - ); - } - - Map toJson() { - return { - 'id': id, - 'label': label, - if (icon != null) 'icon': icon!.toJson(), - if (style != null) 'style': style!.value, - 'disabled': disabled, - 'action': action.toJson(), - if (confirm != null) 'confirm': confirm!.toJson(), - }; - } -} - -// ========== Node Types ========== - -abstract class UiNode { - String get type; - String? get id; - - factory UiNode.fromJson(Map json) { - final type = json['type'] as String? ?? ''; - switch (type) { - case 'text': - return UiTextNode.fromJson(json); - case 'card': - return UiCardNode.fromJson(json); - case 'list': - return UiListNode.fromJson(json); - case 'table': - return UiTableNode.fromJson(json); - case 'kv': - return UiKvNode.fromJson(json); - case 'operation': - return UiOperationNode.fromJson(json); - case 'error': - return UiErrorNode.fromJson(json); - case 'container': - return UiContainerNode.fromJson(json); - default: - return UiTextNode(content: 'Unknown node type: $type'); - } - } -} - -class UiTextNode implements UiNode { - @override - final String? id; - @override - String get type => 'text'; - final String content; - final TextFormat format; - final UiIcon? icon; - final List? actions; - - const UiTextNode({ - this.id, - required this.content, - this.format = TextFormat.plain, - this.icon, - this.actions, - }); - - factory UiTextNode.fromJson(Map json) { - return UiTextNode( - id: json['id'] as String?, - content: json['content'] as String? ?? '', - format: TextFormat.values.firstWhere( - (e) => e.value == json['format'], - orElse: () => TextFormat.plain, - ), - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - 'content': content, - 'format': format.value, - if (icon != null) 'icon': icon!.toJson(), - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiCardNode implements UiNode { - @override - final String? id; - @override - String get type => 'card'; - final String? title; - final String? description; - final UiIcon? icon; - final UiStatus? status; - final String? timestamp; - final List? children; - final UiTextNode? footer; - final Map? extensions; - final List? actions; - - const UiCardNode({ - this.id, - this.title, - this.description, - this.icon, - this.status, - this.timestamp, - this.children, - this.footer, - this.extensions, - this.actions, - }); - - factory UiCardNode.fromJson(Map json) { - return UiCardNode( - id: json['id'] as String?, - title: json['title'] as String?, - description: json['description'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - status: json['status'] != null - ? UiStatus.values.firstWhere( - (e) => e.value == json['status'], - orElse: () => UiStatus.info, - ) - : null, - timestamp: json['timestamp'] as String?, - children: (json['children'] as List?) - ?.map((e) => UiNode.fromJson(e as Map)) - .toList(), - footer: json['footer'] != null - ? UiTextNode.fromJson(json['footer'] as Map) - : null, - extensions: json['extensions'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - if (title != null) 'title': title, - if (description != null) 'description': description, - if (icon != null) 'icon': icon!.toJson(), - if (status != null) 'status': status!.value, - if (timestamp != null) 'timestamp': timestamp, - if (children != null) - 'children': children!.map((e) => (e as dynamic).toJson()).toList(), - if (footer != null) 'footer': footer!.toJson(), - if (extensions != null) 'extensions': extensions, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiListNode implements UiNode { - @override - final String? id; - @override - String get type => 'list'; - final String? title; - final String? description; - final UiIcon? icon; - final UiStatus? status; - final List items; - final Pagination? pagination; - final String? emptyText; - final Map? extensions; - final List? actions; - - const UiListNode({ - this.id, - this.title, - this.description, - this.icon, - this.status, - required this.items, - this.pagination, - this.emptyText, - this.extensions, - this.actions, - }); - - factory UiListNode.fromJson(Map json) { - return UiListNode( - id: json['id'] as String?, - title: json['title'] as String?, - description: json['description'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - status: json['status'] != null - ? UiStatus.values.firstWhere( - (e) => e.value == json['status'], - orElse: () => UiStatus.info, - ) - : null, - items: - (json['items'] as List?) - ?.map((e) => ListItem.fromJson(e as Map)) - .toList() ?? - [], - pagination: json['pagination'] != null - ? Pagination.fromJson(json['pagination'] as Map) - : null, - emptyText: json['emptyText'] as String?, - extensions: json['extensions'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - if (title != null) 'title': title, - if (description != null) 'description': description, - if (icon != null) 'icon': icon!.toJson(), - if (status != null) 'status': status!.value, - 'items': items.map((e) => e.toJson()).toList(), - if (pagination != null) 'pagination': pagination!.toJson(), - if (emptyText != null) 'emptyText': emptyText, - if (extensions != null) 'extensions': extensions, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiTableNode implements UiNode { - @override - final String? id; - @override - String get type => 'table'; - final String? title; - final String? description; - final UiIcon? icon; - final UiStatus? status; - final List columns; - final List rows; - final Pagination? pagination; - final Map? extensions; - final List? actions; - - const UiTableNode({ - this.id, - this.title, - this.description, - this.icon, - this.status, - required this.columns, - required this.rows, - this.pagination, - this.extensions, - this.actions, - }); - - factory UiTableNode.fromJson(Map json) { - return UiTableNode( - id: json['id'] as String?, - title: json['title'] as String?, - description: json['description'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - status: json['status'] != null - ? UiStatus.values.firstWhere( - (e) => e.value == json['status'], - orElse: () => UiStatus.info, - ) - : null, - columns: - (json['columns'] as List?) - ?.map((e) => TableColumn.fromJson(e as Map)) - .toList() ?? - [], - rows: - (json['rows'] as List?) - ?.map((e) => TableRow.fromJson(e as Map)) - .toList() ?? - [], - pagination: json['pagination'] != null - ? Pagination.fromJson(json['pagination'] as Map) - : null, - extensions: json['extensions'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - if (title != null) 'title': title, - if (description != null) 'description': description, - if (icon != null) 'icon': icon!.toJson(), - if (status != null) 'status': status!.value, - 'columns': columns.map((e) => e.toJson()).toList(), - 'rows': rows.map((e) => e.toJson()).toList(), - if (pagination != null) 'pagination': pagination!.toJson(), - if (extensions != null) 'extensions': extensions, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiKvNode implements UiNode { - @override - final String? id; - @override - String get type => 'kv'; - final String? title; - final String? description; - final UiIcon? icon; - final UiStatus? status; - final List pairs; - final KvLayout layout; - final Map? extensions; - final List? actions; - - const UiKvNode({ - this.id, - this.title, - this.description, - this.icon, - this.status, - required this.pairs, - this.layout = KvLayout.vertical, - this.extensions, - this.actions, - }); - - factory UiKvNode.fromJson(Map json) { - return UiKvNode( - id: json['id'] as String?, - title: json['title'] as String?, - description: json['description'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - status: json['status'] != null - ? UiStatus.values.firstWhere( - (e) => e.value == json['status'], - orElse: () => UiStatus.info, - ) - : null, - pairs: - (json['pairs'] as List?) - ?.map((e) => KeyValuePair.fromJson(e as Map)) - .toList() ?? - [], - layout: KvLayout.values.firstWhere( - (e) => e.value == json['layout'], - orElse: () => KvLayout.vertical, - ), - extensions: json['extensions'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - if (title != null) 'title': title, - if (description != null) 'description': description, - if (icon != null) 'icon': icon!.toJson(), - if (status != null) 'status': status!.value, - 'pairs': pairs.map((e) => e.toJson()).toList(), - 'layout': layout.value, - if (extensions != null) 'extensions': extensions, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiOperationNode implements UiNode { - @override - final String? id; - @override - String get type => 'operation'; - final String? title; - final String? description; - final UiIcon? icon; - final UiStatus? status; - final OperationType operation; - final OperationResult result; - final String? message; - final int? affectedCount; - final UiNode? details; - final UiAction? rollback; - final Map? extensions; - final List? actions; - - const UiOperationNode({ - this.id, - this.title, - this.description, - this.icon, - this.status, - required this.operation, - required this.result, - this.message, - this.affectedCount, - this.details, - this.rollback, - this.extensions, - this.actions, - }); - - factory UiOperationNode.fromJson(Map json) { - return UiOperationNode( - id: json['id'] as String?, - title: json['title'] as String?, - description: json['description'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - status: json['status'] != null - ? UiStatus.values.firstWhere( - (e) => e.value == json['status'], - orElse: () => UiStatus.info, - ) - : null, - operation: OperationType.values.firstWhere( - (e) => e.value == json['operation'], - orElse: () => OperationType.execute, - ), - result: OperationResult.values.firstWhere( - (e) => e.value == json['result'], - orElse: () => OperationResult.failure, - ), - message: json['message'] as String?, - affectedCount: json['affectedCount'] as int?, - details: json['details'] != null - ? UiNode.fromJson(json['details'] as Map) - : null, - rollback: json['rollback'] != null - ? UiAction.fromJson(json['rollback'] as Map) - : null, - extensions: json['extensions'] as Map?, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - if (title != null) 'title': title, - if (description != null) 'description': description, - if (icon != null) 'icon': icon!.toJson(), - if (status != null) 'status': status!.value, - 'operation': operation.value, - 'result': result.value, - if (message != null) 'message': message, - if (affectedCount != null) 'affectedCount': affectedCount, - if (details != null) 'details': (details as dynamic).toJson(), - if (rollback != null) 'rollback': rollback!.toJson(), - if (extensions != null) 'extensions': extensions, - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiErrorNode implements UiNode { - @override - final String? id; - @override - String get type => 'error'; - final String? title; - final UiIcon? icon; - final String errorCode; - final String message; - final String? details; - final String? stack; - final bool retryable; - final List? suggestions; - final UiAction? retry; - final UiAction? support; - final List? actions; - - const UiErrorNode({ - this.id, - this.title, - this.icon, - required this.errorCode, - required this.message, - this.details, - this.stack, - this.retryable = false, - this.suggestions, - this.retry, - this.support, - this.actions, - }); - - factory UiErrorNode.fromJson(Map json) { - return UiErrorNode( - id: json['id'] as String?, - title: json['title'] as String?, - icon: json['icon'] != null - ? UiIcon.fromJson(json['icon'] as Map) - : null, - errorCode: json['errorCode'] as String? ?? 'UNKNOWN', - message: json['message'] as String? ?? 'An error occurred', - details: json['details'] as String?, - stack: json['stack'] as String?, - retryable: json['retryable'] as bool? ?? false, - suggestions: (json['suggestions'] as List?) - ?.map((e) => e as String) - .toList(), - retry: json['retry'] != null - ? UiAction.fromJson(json['retry'] as Map) - : null, - support: json['support'] != null - ? UiAction.fromJson(json['support'] as Map) - : null, - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - if (title != null) 'title': title, - if (icon != null) 'icon': icon!.toJson(), - 'errorCode': errorCode, - 'message': message, - if (details != null) 'details': details, - if (stack != null) 'stack': stack, - 'retryable': retryable, - if (suggestions != null) 'suggestions': suggestions, - if (retry != null) 'retry': retry!.toJson(), - if (support != null) 'support': support!.toJson(), - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -class UiContainerNode implements UiNode { - @override - final String? id; - @override - String get type => 'container'; - final ContainerDirection direction; - final int? gap; - final List children; - final List? actions; - - const UiContainerNode({ - this.id, - this.direction = ContainerDirection.vertical, - this.gap, - required this.children, - this.actions, - }); - - factory UiContainerNode.fromJson(Map json) { - return UiContainerNode( - id: json['id'] as String?, - direction: ContainerDirection.values.firstWhere( - (e) => e.value == json['direction'], - orElse: () => ContainerDirection.vertical, - ), - gap: json['gap'] as int?, - children: - (json['children'] as List?) - ?.map((e) => UiNode.fromJson(e as Map)) - .toList() ?? - [], - actions: (json['actions'] as List?) - ?.map((e) => UiAction.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'id': id, - 'type': type, - 'direction': direction.value, - if (gap != null) 'gap': gap, - 'children': children.map((e) => (e as dynamic).toJson()).toList(), - if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), - }; - } -} - -// ========== Document Types ========== - -class RendererConfig { - final String? renderer; - final RendererTheme? theme; - - const RendererConfig({this.renderer, this.theme}); - - factory RendererConfig.fromJson(Map json) { - return RendererConfig( - renderer: json['renderer'] as String?, - theme: json['theme'] != null - ? RendererTheme.values.firstWhere( - (e) => e.value == json['theme'], - orElse: () => RendererTheme.def, - ) - : null, - ); - } - - Map toJson() { - return { - if (renderer != null) 'renderer': renderer, - if (theme != null) 'theme': theme!.value, - }; - } -} - -class DocumentMeta { - final String? requestId; - final String? toolId; - final String? traceId; - final String? userId; - final Map? extra; - - const DocumentMeta({ - this.requestId, - this.toolId, - this.traceId, - this.userId, - this.extra, - }); - - factory DocumentMeta.fromJson(Map json) { - return DocumentMeta( - requestId: json['requestId'] as String?, - toolId: json['toolId'] as String?, - traceId: json['traceId'] as String?, - userId: json['userId'] as String?, - extra: json, - ); - } - - Map toJson() { - return { - if (requestId != null) 'requestId': requestId, - if (toolId != null) 'toolId': toolId, - if (traceId != null) 'traceId': traceId, - if (userId != null) 'userId': userId, - }; - } -} - -class UiSchemaDocument { - final String version; - final SchemaType schemaType; - final String? docId; - final String? timestamp; - final String? locale; - final UiStatus status; - final RendererConfig? renderer; - final DocumentMeta? meta; - final List nodes; - - const UiSchemaDocument({ - required this.version, - required this.schemaType, - this.docId, - this.timestamp, - this.locale, - required this.status, - this.renderer, - this.meta, - required this.nodes, - }); - - factory UiSchemaDocument.fromJson(Map json) { - return UiSchemaDocument( - version: json['version'] as String? ?? '1.0', - schemaType: SchemaType.values.firstWhere( - (e) => e.value == json['schemaType'], - orElse: () => SchemaType.toolResult, - ), - docId: json['docId'] as String?, - timestamp: json['timestamp'] as String?, - locale: json['locale'] as String?, - status: UiStatus.values.firstWhere( - (e) => e.value == json['status'], - orElse: () => UiStatus.info, - ), - renderer: json['renderer'] != null - ? RendererConfig.fromJson(json['renderer'] as Map) - : null, - meta: json['meta'] != null - ? DocumentMeta.fromJson(json['meta'] as Map) - : null, - nodes: - (json['nodes'] as List?) - ?.map((e) => UiNode.fromJson(e as Map)) - .toList() ?? - [], - ); - } - - Map toJson() { - return { - 'version': version, - 'schemaType': schemaType.value, - if (docId != null) 'docId': docId, - if (timestamp != null) 'timestamp': timestamp, - if (locale != null) 'locale': locale, - 'status': status.value, - if (renderer != null) 'renderer': renderer!.toJson(), - if (meta != null) 'meta': meta!.toJson(), - 'nodes': nodes.map((e) => (e as dynamic).toJson()).toList(), - }; - } -} - -// ========== Builder Functions ========== - -UiSchemaDocument buildSuccessDocument( - List nodes, { - String version = '1.0', - SchemaType schemaType = SchemaType.toolResult, - String? docId, - String? timestamp, - String locale = 'zh-CN', - RendererConfig? renderer, - DocumentMeta? meta, -}) { - return UiSchemaDocument( - version: version, - schemaType: schemaType, - docId: docId, - timestamp: timestamp, - locale: locale, - status: UiStatus.success, - renderer: renderer, - meta: meta, - nodes: nodes, - ); -} - -UiSchemaDocument buildErrorDocument( - List nodes, { - String version = '1.0', - SchemaType schemaType = SchemaType.toolResult, - String? docId, - String? timestamp, - String locale = 'zh-CN', - RendererConfig? renderer, - DocumentMeta? meta, -}) { - return UiSchemaDocument( - version: version, - schemaType: schemaType, - docId: docId, - timestamp: timestamp, - locale: locale, - status: UiStatus.error, - renderer: renderer, - meta: meta, - nodes: nodes, - ); -} +part 'ui_schema/enums.dart'; +part 'ui_schema/common_types.dart'; +part 'ui_schema/actions.dart'; +part 'ui_schema/nodes.dart'; +part 'ui_schema/document.dart'; +part 'ui_schema/builders.dart'; diff --git a/apps/lib/core/schemas/ui_schema/actions.dart b/apps/lib/core/schemas/ui_schema/actions.dart new file mode 100644 index 0000000..f227dda --- /dev/null +++ b/apps/lib/core/schemas/ui_schema/actions.dart @@ -0,0 +1,205 @@ +part of '../ui_schema.dart'; + +abstract class ActionSpec { + String get type; + Map toJson(); +} + +class NavigateAction implements ActionSpec { + final String path; + final Map? params; + + const NavigateAction({required this.path, this.params}); + + @override + String get type => 'navigation'; + + @override + Map toJson() { + return {'type': type, 'path': path, if (params != null) 'params': params}; + } +} + +class LinkAction implements ActionSpec { + final String url; + final String? target; + + const LinkAction({required this.url, this.target}); + + @override + String get type => 'url'; + + @override + Map toJson() { + return {'type': type, 'url': url, if (target != null) 'target': target}; + } +} + +class EventAction implements ActionSpec { + final String event; + final Map? payload; + + const EventAction({required this.event, this.payload}); + + @override + String get type => 'event'; + + @override + Map toJson() { + return { + 'type': type, + 'event': event, + if (payload != null) 'payload': payload, + }; + } +} + +class ToolAction implements ActionSpec { + final String toolId; + final Map? params; + + const ToolAction({required this.toolId, this.params}); + + @override + String get type => 'tool'; + + @override + Map toJson() { + return { + 'type': type, + 'toolId': toolId, + if (params != null) 'params': params, + }; + } +} + +class CopyAction implements ActionSpec { + final String content; + final String? successMessage; + + const CopyAction({required this.content, this.successMessage}); + + @override + String get type => 'copy'; + + @override + Map toJson() { + return { + 'type': type, + 'content': content, + if (successMessage != null) 'successMessage': successMessage, + }; + } +} + +class PayloadAction implements ActionSpec { + final Map payload; + final String? submitTo; + + const PayloadAction({required this.payload, this.submitTo}); + + @override + String get type => 'payload'; + + @override + Map toJson() { + return { + 'type': type, + 'payload': payload, + if (submitTo != null) 'submitTo': submitTo, + }; + } +} + +ActionSpec actionSpecFromJson(Map json) { + final type = json['type'] as String? ?? ''; + switch (type) { + case 'navigation': + return NavigateAction( + path: json['path'] as String? ?? '', + params: json['params'] as Map?, + ); + case 'url': + return LinkAction( + url: json['url'] as String? ?? '', + target: json['target'] as String?, + ); + case 'event': + return EventAction( + event: json['event'] as String? ?? '', + payload: json['payload'] as Map?, + ); + case 'tool': + return ToolAction( + toolId: json['toolId'] as String? ?? '', + params: json['params'] as Map?, + ); + case 'copy': + return CopyAction( + content: json['content'] as String? ?? '', + successMessage: json['successMessage'] as String?, + ); + case 'payload': + return PayloadAction( + payload: json['payload'] as Map? ?? {}, + submitTo: json['submitTo'] as String?, + ); + default: + return EventAction(event: 'unknown'); + } +} + +class UiAction { + final String id; + final String label; + final UiIcon? icon; + final ActionStyle? style; + final bool disabled; + final ActionSpec action; + final ActionConfirm? confirm; + + const UiAction({ + required this.id, + required this.label, + this.icon, + this.style, + this.disabled = false, + required this.action, + this.confirm, + }); + + factory UiAction.fromJson(Map json) { + return UiAction( + id: json['id'] as String? ?? '', + label: json['label'] as String? ?? '', + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + style: json['style'] != null + ? ActionStyle.values.firstWhere( + (e) => e.value == json['style'], + orElse: () => ActionStyle.primary, + ) + : null, + disabled: json['disabled'] as bool? ?? false, + action: actionSpecFromJson( + json['action'] as Map? ?? {'type': 'event'}, + ), + confirm: json['confirm'] != null + ? ActionConfirm.fromJson(json['confirm'] as Map) + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'label': label, + if (icon != null) 'icon': icon!.toJson(), + if (style != null) 'style': style!.value, + 'disabled': disabled, + 'action': action.toJson(), + if (confirm != null) 'confirm': confirm!.toJson(), + }; + } +} diff --git a/apps/lib/core/schemas/ui_schema/builders.dart b/apps/lib/core/schemas/ui_schema/builders.dart new file mode 100644 index 0000000..237be92 --- /dev/null +++ b/apps/lib/core/schemas/ui_schema/builders.dart @@ -0,0 +1,47 @@ +part of '../ui_schema.dart'; + +UiSchemaDocument buildSuccessDocument( + List nodes, { + String version = '1.0', + SchemaType schemaType = SchemaType.toolResult, + String? docId, + String? timestamp, + String locale = 'zh-CN', + RendererConfig? renderer, + DocumentMeta? meta, +}) { + return UiSchemaDocument( + version: version, + schemaType: schemaType, + docId: docId, + timestamp: timestamp, + locale: locale, + status: UiStatus.success, + renderer: renderer, + meta: meta, + nodes: nodes, + ); +} + +UiSchemaDocument buildErrorDocument( + List nodes, { + String version = '1.0', + SchemaType schemaType = SchemaType.toolResult, + String? docId, + String? timestamp, + String locale = 'zh-CN', + RendererConfig? renderer, + DocumentMeta? meta, +}) { + return UiSchemaDocument( + version: version, + schemaType: schemaType, + docId: docId, + timestamp: timestamp, + locale: locale, + status: UiStatus.error, + renderer: renderer, + meta: meta, + nodes: nodes, + ); +} diff --git a/apps/lib/core/schemas/ui_schema/common_types.dart b/apps/lib/core/schemas/ui_schema/common_types.dart new file mode 100644 index 0000000..9da555c --- /dev/null +++ b/apps/lib/core/schemas/ui_schema/common_types.dart @@ -0,0 +1,273 @@ +part of '../ui_schema.dart'; + +class UiIcon { + final IconSource source; + final String value; + final String? color; + final int? size; + + const UiIcon({ + required this.source, + required this.value, + this.color, + this.size, + }); + + factory UiIcon.fromJson(Map json) { + return UiIcon( + source: IconSource.values.firstWhere( + (e) => e.value == json['source'], + orElse: () => IconSource.icon, + ), + value: json['value'] as String? ?? '', + color: json['color'] as String?, + size: json['size'] as int?, + ); + } + + Map toJson() { + return { + 'source': source.value, + 'value': value, + if (color != null) 'color': color, + if (size != null) 'size': size, + }; + } +} + +class UiBadge { + final String label; + final BadgeVariant variant; + + const UiBadge({required this.label, this.variant = BadgeVariant.def}); + + factory UiBadge.fromJson(Map json) { + return UiBadge( + label: json['label'] as String? ?? '', + variant: BadgeVariant.values.firstWhere( + (e) => e.value == json['variant'], + orElse: () => BadgeVariant.def, + ), + ); + } + + Map toJson() { + return {'label': label, 'variant': variant.value}; + } +} + +class Pagination { + final int page; + final int pageSize; + final int total; + final bool hasMore; + + const Pagination({ + required this.page, + required this.pageSize, + required this.total, + required this.hasMore, + }); + + factory Pagination.fromJson(Map json) { + return Pagination( + page: json['page'] as int? ?? 1, + pageSize: json['pageSize'] as int? ?? 20, + total: json['total'] as int? ?? 0, + hasMore: json['hasMore'] as bool? ?? false, + ); + } + + Map toJson() { + return { + 'page': page, + 'pageSize': pageSize, + 'total': total, + 'hasMore': hasMore, + }; + } +} + +class ActionConfirm { + final String? title; + final String? message; + final String? confirmLabel; + final String? cancelLabel; + + const ActionConfirm({ + this.title, + this.message, + this.confirmLabel, + this.cancelLabel, + }); + + factory ActionConfirm.fromJson(Map json) { + return ActionConfirm( + title: json['title'] as String?, + message: json['message'] as String?, + confirmLabel: json['confirmLabel'] as String?, + cancelLabel: json['cancelLabel'] as String?, + ); + } + + Map toJson() { + return { + if (title != null) 'title': title, + if (message != null) 'message': message, + if (confirmLabel != null) 'confirmLabel': confirmLabel, + if (cancelLabel != null) 'cancelLabel': cancelLabel, + }; + } +} + +class KeyValuePair { + final String key; + final String? label; + final dynamic value; + final bool? copyable; + + const KeyValuePair({ + required this.key, + this.label, + required this.value, + this.copyable, + }); + + factory KeyValuePair.fromJson(Map json) { + return KeyValuePair( + key: json['key'] as String? ?? '', + label: json['label'] as String?, + value: json['value'], + copyable: json['copyable'] as bool?, + ); + } + + Map toJson() { + return { + 'key': key, + if (label != null) 'label': label, + 'value': value, + if (copyable != null) 'copyable': copyable, + }; + } +} + +class TableColumn { + final String key; + final String label; + final String? width; + final String? align; + + const TableColumn({ + required this.key, + required this.label, + this.width, + this.align, + }); + + factory TableColumn.fromJson(Map json) { + return TableColumn( + key: json['key'] as String? ?? '', + label: json['label'] as String? ?? '', + width: json['width'] as String?, + align: json['align'] as String?, + ); + } + + Map toJson() { + return { + 'key': key, + 'label': label, + if (width != null) 'width': width, + if (align != null) 'align': align, + }; + } +} + +class TableRow { + final String id; + final Map cells; + final Map? metadata; + final List? actions; + + const TableRow({ + required this.id, + required this.cells, + this.metadata, + this.actions, + }); + + factory TableRow.fromJson(Map json) { + return TableRow( + id: json['id'] as String? ?? '', + cells: json['cells'] as Map? ?? {}, + metadata: json['metadata'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'cells': cells, + if (metadata != null) 'metadata': metadata, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class ListItem { + final String id; + final String title; + final String? subtitle; + final String? description; + final UiIcon? icon; + final UiBadge? badge; + final Map? metadata; + final List? actions; + + const ListItem({ + required this.id, + required this.title, + this.subtitle, + this.description, + this.icon, + this.badge, + this.metadata, + this.actions, + }); + + factory ListItem.fromJson(Map json) { + return ListItem( + id: json['id'] as String? ?? '', + title: json['title'] as String? ?? '', + subtitle: json['subtitle'] as String?, + description: json['description'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + badge: json['badge'] != null + ? UiBadge.fromJson(json['badge'] as Map) + : null, + metadata: json['metadata'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + if (subtitle != null) 'subtitle': subtitle, + if (description != null) 'description': description, + if (icon != null) 'icon': icon!.toJson(), + if (badge != null) 'badge': badge!.toJson(), + if (metadata != null) 'metadata': metadata, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} diff --git a/apps/lib/core/schemas/ui_schema/document.dart b/apps/lib/core/schemas/ui_schema/document.dart new file mode 100644 index 0000000..9f2a75d --- /dev/null +++ b/apps/lib/core/schemas/ui_schema/document.dart @@ -0,0 +1,128 @@ +part of '../ui_schema.dart'; + +class RendererConfig { + final String? renderer; + final RendererTheme? theme; + + const RendererConfig({this.renderer, this.theme}); + + factory RendererConfig.fromJson(Map json) { + return RendererConfig( + renderer: json['renderer'] as String?, + theme: json['theme'] != null + ? RendererTheme.values.firstWhere( + (e) => e.value == json['theme'], + orElse: () => RendererTheme.def, + ) + : null, + ); + } + + Map toJson() { + return { + if (renderer != null) 'renderer': renderer, + if (theme != null) 'theme': theme!.value, + }; + } +} + +class DocumentMeta { + final String? requestId; + final String? toolId; + final String? traceId; + final String? userId; + final Map? extra; + + const DocumentMeta({ + this.requestId, + this.toolId, + this.traceId, + this.userId, + this.extra, + }); + + factory DocumentMeta.fromJson(Map json) { + return DocumentMeta( + requestId: json['requestId'] as String?, + toolId: json['toolId'] as String?, + traceId: json['traceId'] as String?, + userId: json['userId'] as String?, + extra: json, + ); + } + + Map toJson() { + return { + if (requestId != null) 'requestId': requestId, + if (toolId != null) 'toolId': toolId, + if (traceId != null) 'traceId': traceId, + if (userId != null) 'userId': userId, + }; + } +} + +class UiSchemaDocument { + final String version; + final SchemaType schemaType; + final String? docId; + final String? timestamp; + final String? locale; + final UiStatus status; + final RendererConfig? renderer; + final DocumentMeta? meta; + final List nodes; + + const UiSchemaDocument({ + required this.version, + required this.schemaType, + this.docId, + this.timestamp, + this.locale, + required this.status, + this.renderer, + this.meta, + required this.nodes, + }); + + factory UiSchemaDocument.fromJson(Map json) { + return UiSchemaDocument( + version: json['version'] as String? ?? '1.0', + schemaType: SchemaType.values.firstWhere( + (e) => e.value == json['schemaType'], + orElse: () => SchemaType.toolResult, + ), + docId: json['docId'] as String?, + timestamp: json['timestamp'] as String?, + locale: json['locale'] as String?, + status: UiStatus.values.firstWhere( + (e) => e.value == json['status'], + orElse: () => UiStatus.info, + ), + renderer: json['renderer'] != null + ? RendererConfig.fromJson(json['renderer'] as Map) + : null, + meta: json['meta'] != null + ? DocumentMeta.fromJson(json['meta'] as Map) + : null, + nodes: + (json['nodes'] as List?) + ?.map((e) => UiNode.fromJson(e as Map)) + .toList() ?? + [], + ); + } + + Map toJson() { + return { + 'version': version, + 'schemaType': schemaType.value, + if (docId != null) 'docId': docId, + if (timestamp != null) 'timestamp': timestamp, + if (locale != null) 'locale': locale, + 'status': status.value, + if (renderer != null) 'renderer': renderer!.toJson(), + if (meta != null) 'meta': meta!.toJson(), + 'nodes': nodes.map((e) => (e as dynamic).toJson()).toList(), + }; + } +} diff --git a/apps/lib/core/schemas/ui_schema/enums.dart b/apps/lib/core/schemas/ui_schema/enums.dart new file mode 100644 index 0000000..a8d91d4 --- /dev/null +++ b/apps/lib/core/schemas/ui_schema/enums.dart @@ -0,0 +1,104 @@ +part of '../ui_schema.dart'; + +enum SchemaType { + toolResult('tool_result'), + agentResponse('agent_response'), + notification('notification'); + + final String value; + const SchemaType(this.value); +} + +enum UiStatus { + info('info'), + success('success'), + warning('warning'), + error('error'), + pending('pending'); + + final String value; + const UiStatus(this.value); +} + +enum IconSource { + icon('icon'), + emoji('emoji'), + url('url'); + + final String value; + const IconSource(this.value); +} + +enum OperationType { + create('create'), + update('update'), + delete('delete'), + execute('execute'); + + final String value; + const OperationType(this.value); +} + +enum OperationResult { + success('success'), + failure('failure'), + partial('partial'); + + final String value; + const OperationResult(this.value); +} + +enum ContainerDirection { + vertical('vertical'), + horizontal('horizontal'); + + final String value; + const ContainerDirection(this.value); +} + +enum TextFormat { + plain('plain'), + markdown('markdown'); + + final String value; + const TextFormat(this.value); +} + +enum KvLayout { + vertical('vertical'), + horizontal('horizontal'), + grid('grid'); + + final String value; + const KvLayout(this.value); +} + +enum BadgeVariant { + def('default'), + success('success'), + warning('warning'), + error('error'), + info('info'); + + final String value; + const BadgeVariant(this.value); +} + +enum ActionStyle { + primary('primary'), + secondary('secondary'), + ghost('ghost'), + danger('danger'); + + final String value; + const ActionStyle(this.value); +} + +enum RendererTheme { + def('default'), + dark('dark'), + light('light'); + + final String value; + const RendererTheme(this.value); +} diff --git a/apps/lib/core/schemas/ui_schema/nodes.dart b/apps/lib/core/schemas/ui_schema/nodes.dart new file mode 100644 index 0000000..e1e11c1 --- /dev/null +++ b/apps/lib/core/schemas/ui_schema/nodes.dart @@ -0,0 +1,595 @@ +part of '../ui_schema.dart'; + +abstract class UiNode { + String get type; + String? get id; + + factory UiNode.fromJson(Map json) { + final type = json['type'] as String? ?? ''; + switch (type) { + case 'text': + return UiTextNode.fromJson(json); + case 'card': + return UiCardNode.fromJson(json); + case 'list': + return UiListNode.fromJson(json); + case 'table': + return UiTableNode.fromJson(json); + case 'kv': + return UiKvNode.fromJson(json); + case 'operation': + return UiOperationNode.fromJson(json); + case 'error': + return UiErrorNode.fromJson(json); + case 'container': + return UiContainerNode.fromJson(json); + default: + return UiTextNode(content: 'Unknown node type: $type'); + } + } +} + +class UiTextNode implements UiNode { + @override + final String? id; + @override + String get type => 'text'; + final String content; + final TextFormat format; + final UiIcon? icon; + final List? actions; + + const UiTextNode({ + this.id, + required this.content, + this.format = TextFormat.plain, + this.icon, + this.actions, + }); + + factory UiTextNode.fromJson(Map json) { + return UiTextNode( + id: json['id'] as String?, + content: json['content'] as String? ?? '', + format: TextFormat.values.firstWhere( + (e) => e.value == json['format'], + orElse: () => TextFormat.plain, + ), + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + 'content': content, + 'format': format.value, + if (icon != null) 'icon': icon!.toJson(), + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiCardNode implements UiNode { + @override + final String? id; + @override + String get type => 'card'; + final String? title; + final String? description; + final UiIcon? icon; + final UiStatus? status; + final String? timestamp; + final List? children; + final UiTextNode? footer; + final Map? extensions; + final List? actions; + + const UiCardNode({ + this.id, + this.title, + this.description, + this.icon, + this.status, + this.timestamp, + this.children, + this.footer, + this.extensions, + this.actions, + }); + + factory UiCardNode.fromJson(Map json) { + return UiCardNode( + id: json['id'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + status: json['status'] != null + ? UiStatus.values.firstWhere( + (e) => e.value == json['status'], + orElse: () => UiStatus.info, + ) + : null, + timestamp: json['timestamp'] as String?, + children: (json['children'] as List?) + ?.map((e) => UiNode.fromJson(e as Map)) + .toList(), + footer: json['footer'] != null + ? UiTextNode.fromJson(json['footer'] as Map) + : null, + extensions: json['extensions'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (icon != null) 'icon': icon!.toJson(), + if (status != null) 'status': status!.value, + if (timestamp != null) 'timestamp': timestamp, + if (children != null) + 'children': children!.map((e) => (e as dynamic).toJson()).toList(), + if (footer != null) 'footer': footer!.toJson(), + if (extensions != null) 'extensions': extensions, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiListNode implements UiNode { + @override + final String? id; + @override + String get type => 'list'; + final String? title; + final String? description; + final UiIcon? icon; + final UiStatus? status; + final List items; + final Pagination? pagination; + final String? emptyText; + final Map? extensions; + final List? actions; + + const UiListNode({ + this.id, + this.title, + this.description, + this.icon, + this.status, + required this.items, + this.pagination, + this.emptyText, + this.extensions, + this.actions, + }); + + factory UiListNode.fromJson(Map json) { + return UiListNode( + id: json['id'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + status: json['status'] != null + ? UiStatus.values.firstWhere( + (e) => e.value == json['status'], + orElse: () => UiStatus.info, + ) + : null, + items: + (json['items'] as List?) + ?.map((e) => ListItem.fromJson(e as Map)) + .toList() ?? + [], + pagination: json['pagination'] != null + ? Pagination.fromJson(json['pagination'] as Map) + : null, + emptyText: json['emptyText'] as String?, + extensions: json['extensions'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (icon != null) 'icon': icon!.toJson(), + if (status != null) 'status': status!.value, + 'items': items.map((e) => e.toJson()).toList(), + if (pagination != null) 'pagination': pagination!.toJson(), + if (emptyText != null) 'emptyText': emptyText, + if (extensions != null) 'extensions': extensions, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiTableNode implements UiNode { + @override + final String? id; + @override + String get type => 'table'; + final String? title; + final String? description; + final UiIcon? icon; + final UiStatus? status; + final List columns; + final List rows; + final Pagination? pagination; + final Map? extensions; + final List? actions; + + const UiTableNode({ + this.id, + this.title, + this.description, + this.icon, + this.status, + required this.columns, + required this.rows, + this.pagination, + this.extensions, + this.actions, + }); + + factory UiTableNode.fromJson(Map json) { + return UiTableNode( + id: json['id'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + status: json['status'] != null + ? UiStatus.values.firstWhere( + (e) => e.value == json['status'], + orElse: () => UiStatus.info, + ) + : null, + columns: + (json['columns'] as List?) + ?.map((e) => TableColumn.fromJson(e as Map)) + .toList() ?? + [], + rows: + (json['rows'] as List?) + ?.map((e) => TableRow.fromJson(e as Map)) + .toList() ?? + [], + pagination: json['pagination'] != null + ? Pagination.fromJson(json['pagination'] as Map) + : null, + extensions: json['extensions'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (icon != null) 'icon': icon!.toJson(), + if (status != null) 'status': status!.value, + 'columns': columns.map((e) => e.toJson()).toList(), + 'rows': rows.map((e) => e.toJson()).toList(), + if (pagination != null) 'pagination': pagination!.toJson(), + if (extensions != null) 'extensions': extensions, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiKvNode implements UiNode { + @override + final String? id; + @override + String get type => 'kv'; + final String? title; + final String? description; + final UiIcon? icon; + final UiStatus? status; + final List pairs; + final KvLayout layout; + final Map? extensions; + final List? actions; + + const UiKvNode({ + this.id, + this.title, + this.description, + this.icon, + this.status, + required this.pairs, + this.layout = KvLayout.vertical, + this.extensions, + this.actions, + }); + + factory UiKvNode.fromJson(Map json) { + return UiKvNode( + id: json['id'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + status: json['status'] != null + ? UiStatus.values.firstWhere( + (e) => e.value == json['status'], + orElse: () => UiStatus.info, + ) + : null, + pairs: + (json['pairs'] as List?) + ?.map((e) => KeyValuePair.fromJson(e as Map)) + .toList() ?? + [], + layout: KvLayout.values.firstWhere( + (e) => e.value == json['layout'], + orElse: () => KvLayout.vertical, + ), + extensions: json['extensions'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (icon != null) 'icon': icon!.toJson(), + if (status != null) 'status': status!.value, + 'pairs': pairs.map((e) => e.toJson()).toList(), + 'layout': layout.value, + if (extensions != null) 'extensions': extensions, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiOperationNode implements UiNode { + @override + final String? id; + @override + String get type => 'operation'; + final String? title; + final String? description; + final UiIcon? icon; + final UiStatus? status; + final OperationType operation; + final OperationResult result; + final String? message; + final int? affectedCount; + final UiNode? details; + final UiAction? rollback; + final Map? extensions; + final List? actions; + + const UiOperationNode({ + this.id, + this.title, + this.description, + this.icon, + this.status, + required this.operation, + required this.result, + this.message, + this.affectedCount, + this.details, + this.rollback, + this.extensions, + this.actions, + }); + + factory UiOperationNode.fromJson(Map json) { + return UiOperationNode( + id: json['id'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + status: json['status'] != null + ? UiStatus.values.firstWhere( + (e) => e.value == json['status'], + orElse: () => UiStatus.info, + ) + : null, + operation: OperationType.values.firstWhere( + (e) => e.value == json['operation'], + orElse: () => OperationType.execute, + ), + result: OperationResult.values.firstWhere( + (e) => e.value == json['result'], + orElse: () => OperationResult.failure, + ), + message: json['message'] as String?, + affectedCount: json['affectedCount'] as int?, + details: json['details'] != null + ? UiNode.fromJson(json['details'] as Map) + : null, + rollback: json['rollback'] != null + ? UiAction.fromJson(json['rollback'] as Map) + : null, + extensions: json['extensions'] as Map?, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (icon != null) 'icon': icon!.toJson(), + if (status != null) 'status': status!.value, + 'operation': operation.value, + 'result': result.value, + if (message != null) 'message': message, + if (affectedCount != null) 'affectedCount': affectedCount, + if (details != null) 'details': (details as dynamic).toJson(), + if (rollback != null) 'rollback': rollback!.toJson(), + if (extensions != null) 'extensions': extensions, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiErrorNode implements UiNode { + @override + final String? id; + @override + String get type => 'error'; + final String? title; + final UiIcon? icon; + final String errorCode; + final String message; + final String? details; + final String? stack; + final bool retryable; + final List? suggestions; + final UiAction? retry; + final UiAction? support; + final List? actions; + + const UiErrorNode({ + this.id, + this.title, + this.icon, + required this.errorCode, + required this.message, + this.details, + this.stack, + this.retryable = false, + this.suggestions, + this.retry, + this.support, + this.actions, + }); + + factory UiErrorNode.fromJson(Map json) { + return UiErrorNode( + id: json['id'] as String?, + title: json['title'] as String?, + icon: json['icon'] != null + ? UiIcon.fromJson(json['icon'] as Map) + : null, + errorCode: json['errorCode'] as String? ?? 'UNKNOWN', + message: json['message'] as String? ?? 'An error occurred', + details: json['details'] as String?, + stack: json['stack'] as String?, + retryable: json['retryable'] as bool? ?? false, + suggestions: (json['suggestions'] as List?) + ?.map((e) => e as String) + .toList(), + retry: json['retry'] != null + ? UiAction.fromJson(json['retry'] as Map) + : null, + support: json['support'] != null + ? UiAction.fromJson(json['support'] as Map) + : null, + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + if (title != null) 'title': title, + if (icon != null) 'icon': icon!.toJson(), + 'errorCode': errorCode, + 'message': message, + if (details != null) 'details': details, + if (stack != null) 'stack': stack, + 'retryable': retryable, + if (suggestions != null) 'suggestions': suggestions, + if (retry != null) 'retry': retry!.toJson(), + if (support != null) 'support': support!.toJson(), + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +} + +class UiContainerNode implements UiNode { + @override + final String? id; + @override + String get type => 'container'; + final ContainerDirection direction; + final int? gap; + final List children; + final List? actions; + + const UiContainerNode({ + this.id, + this.direction = ContainerDirection.vertical, + this.gap, + required this.children, + this.actions, + }); + + factory UiContainerNode.fromJson(Map json) { + return UiContainerNode( + id: json['id'] as String?, + direction: ContainerDirection.values.firstWhere( + (e) => e.value == json['direction'], + orElse: () => ContainerDirection.vertical, + ), + gap: json['gap'] as int?, + children: + (json['children'] as List?) + ?.map((e) => UiNode.fromJson(e as Map)) + .toList() ?? + [], + actions: (json['actions'] as List?) + ?.map((e) => UiAction.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'type': type, + 'direction': direction.value, + if (gap != null) 'gap': gap, + 'children': children.map((e) => (e as dynamic).toJson()).toList(), + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + }; + } +}