import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:go_router/go_router.dart'; import '../core/constants.dart'; /// Human-readable labels for each backend tool. Kept in sync with /// `_TOOL_LABELS` in `fabledassistant/services/generation_task.py` so the /// mobile chips use the same wording as the web ToolCallCard status pills. const Map _toolLabels = { 'create_note': 'Created note', 'update_note': 'Updated note', 'delete_note': 'Deleted note', 'create_task': 'Created task', 'update_task': 'Updated task', 'delete_task': 'Deleted task', 'read_note': 'Read note', 'list_notes': 'Listed notes', 'list_tasks': 'Searched tasks', 'search_notes': 'Searched notes', 'create_event': 'Created event', 'list_events': 'Searched calendar', 'search_events': 'Searched calendar', 'update_event': 'Updated event', 'delete_event': 'Removed event', 'list_calendars': 'Listed calendars', 'search_web': 'Searched the web', 'research_topic': 'Researched topic', 'search_images': 'Searched images', 'create_project': 'Created project', 'update_project': 'Updated project', 'list_projects': 'Listed projects', 'get_project': 'Read project', 'search_projects': 'Searched projects', 'create_milestone': 'Created milestone', 'update_milestone': 'Updated milestone', 'list_milestones': 'Listed milestones', 'set_rag_scope': 'Changed knowledge scope', 'calculate': 'Calculated', 'read_article': 'Read article', 'get_profile': 'Read profile', 'update_profile': 'Updated profile', 'update_person': 'Updated person', 'update_place': 'Updated place', 'add_task_log': 'Logged task progress', }; IconData _iconFor(String fn) { if (fn.contains('note')) return LucideIcons.stickyNote; if (fn.contains('task')) return LucideIcons.checkCircle2; if (fn.contains('event') || fn.contains('calendar')) { return LucideIcons.calendarCheck; } if (fn.contains('project')) return LucideIcons.folder; if (fn.contains('milestone')) return LucideIcons.flag; if (fn.contains('web') || fn.contains('research') || fn.contains('article')) { return LucideIcons.globe; } if (fn.contains('image')) return LucideIcons.image; if (fn.contains('person') || fn.contains('profile')) { return LucideIcons.user; } if (fn.contains('place')) return LucideIcons.mapPin; if (fn.contains('rag') || fn.contains('scope')) return LucideIcons.sliders; if (fn.contains('calculate')) return LucideIcons.calculator; return LucideIcons.sparkles; } /// Pull the destination route for this tool call from its `result` payload, /// if the tool produced something we can navigate to. Returns `null` for /// read-only / no-target tools so the chip stays visible but non-tappable. /// /// Backend tool results use the shape: /// `{success, type: "note"|"task"|"event"|"project"|..., data: {id, ...}}` /// which is defined alongside each tool handler (see /// `services/tools/notes.py`, `calendar.py`, etc.). String? _routeForToolCall(Map tc) { final result = tc['result']; if (result is! Map) return null; if (result['success'] != true) return null; final type = result['type'] as String?; final data = result['data']; if (type == null || data is! Map) return null; final id = data['id']; if (id is! int) return null; switch (type) { case 'note': return Routes.noteDetail.replaceFirst(':id', '$id'); case 'task': return Routes.taskEdit.replaceFirst(':id', '$id'); case 'event': case 'event_updated': // No single-event route on mobile — fall back to the calendar. return Routes.calendar; case 'project': return Routes.projectTasks.replaceFirst(':id', '$id'); default: return null; } } /// Small status pill rendered inside an assistant message bubble for each /// tool invocation. Mirrors the web app's ToolCallCard header at a glance — /// icon + label + success/error tint — and, when the tool produced a /// navigable entity (note, task, event, project), tapping the chip opens it. class ToolCallChip extends StatelessWidget { final Map toolCall; const ToolCallChip({super.key, required this.toolCall}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final function = (toolCall['function'] as String?) ?? 'tool'; final status = (toolCall['status'] as String?) ?? 'success'; final isError = status == 'error'; final label = _toolLabels[function] ?? function; final route = isError ? null : _routeForToolCall(toolCall); final bg = isError ? scheme.errorContainer.withValues(alpha: 0.55) : scheme.primary.withValues(alpha: 0.12); final fg = isError ? scheme.onErrorContainer : scheme.primary; // Pull the entity title from the result payload so the chip can show // "Created note: Grocery List" instead of a generic label. Falls back // to the generic label when the tool didn't return a titled entity. String? entityTitle; final result = toolCall['result']; if (result is Map) { final data = result['data']; if (data is Map) { final t = data['title']; if (t is String && t.isNotEmpty) entityTitle = t; } } final displayText = entityTitle != null ? '$label: $entityTitle' : label; final chip = Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(10), border: Border.all(color: fg.withValues(alpha: 0.35), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(_iconFor(function), size: 13, color: fg), const SizedBox(width: 5), Flexible( child: Text( displayText, style: TextStyle( fontSize: 11, color: fg, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), if (route != null) ...[ const SizedBox(width: 4), Icon(LucideIcons.arrowRight, size: 11, color: fg), ], ], ), ); if (route == null) return chip; return Material( color: Colors.transparent, borderRadius: BorderRadius.circular(10), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: () => context.push(route), child: chip, ), ); } }