neoagent 2.3.1-beta.57 → 2.3.1-beta.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1477,39 +1477,459 @@ class _Sidebar extends StatelessWidget {
1477
1477
  }
1478
1478
  }
1479
1479
 
1480
- class _AgentSwitcher extends StatelessWidget {
1480
+ class _AgentSwitcher extends StatefulWidget {
1481
1481
  const _AgentSwitcher({required this.controller, this.onChanged});
1482
1482
 
1483
1483
  final NeoAgentController controller;
1484
1484
  final VoidCallback? onChanged;
1485
1485
 
1486
+ @override
1487
+ State<_AgentSwitcher> createState() => _AgentSwitcherState();
1488
+ }
1489
+
1490
+ class _AgentSwitcherState extends State<_AgentSwitcher> {
1491
+ final MenuController _menuController = MenuController();
1492
+
1493
+ void _toggleMenu() {
1494
+ if (_menuController.isOpen) {
1495
+ _menuController.close();
1496
+ } else {
1497
+ _menuController.open();
1498
+ }
1499
+ setState(() {});
1500
+ }
1501
+
1502
+ Future<void> _selectAgent(String agentId) async {
1503
+ if (widget.controller.selectedAgentId == agentId) {
1504
+ _menuController.close();
1505
+ setState(() {});
1506
+ return;
1507
+ }
1508
+ widget.onChanged?.call();
1509
+ _menuController.close();
1510
+ setState(() {});
1511
+ await widget.controller.switchAgent(agentId);
1512
+ }
1513
+
1486
1514
  @override
1487
1515
  Widget build(BuildContext context) {
1488
- return DropdownButtonFormField<String>(
1489
- initialValue: controller.selectedAgentId,
1490
- isExpanded: true,
1491
- decoration: const InputDecoration(
1492
- labelText: 'Agent',
1493
- prefixIcon: Icon(Icons.smart_toy_outlined, size: 18),
1494
- contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 13),
1516
+ final controller = widget.controller;
1517
+ final selectedAgent =
1518
+ controller.activeAgent ?? controller.agentProfiles.first;
1519
+ final isMenuOpen = _menuController.isOpen;
1520
+
1521
+ return MenuAnchor(
1522
+ controller: _menuController,
1523
+ style: MenuStyle(
1524
+ backgroundColor: WidgetStateProperty.all(Colors.transparent),
1525
+ surfaceTintColor: WidgetStateProperty.all(Colors.transparent),
1526
+ shadowColor: WidgetStateProperty.all(Colors.transparent),
1527
+ elevation: WidgetStateProperty.all(0),
1528
+ padding: WidgetStateProperty.all(EdgeInsets.zero),
1529
+ shape: WidgetStateProperty.all(
1530
+ RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
1531
+ ),
1495
1532
  ),
1496
- items: controller.agentProfiles
1497
- .map(
1498
- (agent) => DropdownMenuItem<String>(
1499
- value: agent.id,
1500
- child: Text(agent.label, overflow: TextOverflow.ellipsis),
1533
+ crossAxisUnconstrained: false,
1534
+ onOpen: () => setState(() {}),
1535
+ onClose: () => setState(() {}),
1536
+ menuChildren: <Widget>[
1537
+ SizedBox(
1538
+ width: 320,
1539
+ child: _GlassSurface(
1540
+ borderRadius: BorderRadius.circular(24),
1541
+ blurSigma: 28,
1542
+ fillColor: _bgCard.withValues(alpha: 0.9),
1543
+ overlayGradient: LinearGradient(
1544
+ colors: <Color>[
1545
+ Colors.white.withValues(alpha: 0.1),
1546
+ _bgSecondary.withValues(alpha: 0.92),
1547
+ _bgPrimary.withValues(alpha: 0.94),
1548
+ ],
1549
+ begin: Alignment.topLeft,
1550
+ end: Alignment.bottomRight,
1501
1551
  ),
1502
- )
1503
- .toList(),
1504
- onChanged: (value) {
1505
- if (value == null) return;
1506
- onChanged?.call();
1507
- unawaited(controller.switchAgent(value));
1552
+ boxShadow: <BoxShadow>[
1553
+ ..._softPanelShadow,
1554
+ BoxShadow(
1555
+ color: Colors.black.withValues(alpha: 0.22),
1556
+ blurRadius: 28,
1557
+ offset: const Offset(0, 16),
1558
+ ),
1559
+ ],
1560
+ child: Padding(
1561
+ padding: const EdgeInsets.all(10),
1562
+ child: Column(
1563
+ mainAxisSize: MainAxisSize.min,
1564
+ children: controller.agentProfiles
1565
+ .map(
1566
+ (agent) => Padding(
1567
+ padding: const EdgeInsets.symmetric(vertical: 3),
1568
+ child: _AgentSwitcherMenuItem(
1569
+ agent: agent,
1570
+ selected: agent.id == controller.selectedAgentId,
1571
+ onTap: () => _selectAgent(agent.id),
1572
+ ),
1573
+ ),
1574
+ )
1575
+ .toList(),
1576
+ ),
1577
+ ),
1578
+ ),
1579
+ ),
1580
+ ],
1581
+ builder: (context, menuController, child) {
1582
+ return Material(
1583
+ color: Colors.transparent,
1584
+ child: InkWell(
1585
+ borderRadius: BorderRadius.circular(24),
1586
+ onTap: _toggleMenu,
1587
+ child: AnimatedContainer(
1588
+ duration: const Duration(milliseconds: 180),
1589
+ curve: Curves.easeOutCubic,
1590
+ padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
1591
+ decoration: BoxDecoration(
1592
+ borderRadius: BorderRadius.circular(24),
1593
+ gradient: LinearGradient(
1594
+ colors: <Color>[
1595
+ Colors.white.withValues(alpha: isMenuOpen ? 0.13 : 0.08),
1596
+ _accentMuted.withValues(alpha: isMenuOpen ? 0.24 : 0.14),
1597
+ _bgSecondary.withValues(alpha: isMenuOpen ? 0.92 : 0.84),
1598
+ ],
1599
+ begin: Alignment.topLeft,
1600
+ end: Alignment.bottomRight,
1601
+ ),
1602
+ border: Border.all(
1603
+ color: isMenuOpen
1604
+ ? _accent.withValues(alpha: 0.65)
1605
+ : _borderLight,
1606
+ ),
1607
+ boxShadow: isMenuOpen
1608
+ ? <BoxShadow>[
1609
+ BoxShadow(
1610
+ color: _accent.withValues(alpha: 0.16),
1611
+ blurRadius: 24,
1612
+ offset: const Offset(0, 10),
1613
+ ),
1614
+ ]
1615
+ : null,
1616
+ ),
1617
+ child: Row(
1618
+ children: <Widget>[
1619
+ _AgentGlyph(
1620
+ agent: selectedAgent,
1621
+ selected: true,
1622
+ compact: false,
1623
+ ),
1624
+ const SizedBox(width: 12),
1625
+ Expanded(
1626
+ child: Column(
1627
+ crossAxisAlignment: CrossAxisAlignment.start,
1628
+ children: <Widget>[
1629
+ Row(
1630
+ children: <Widget>[
1631
+ Flexible(
1632
+ child: Text(
1633
+ selectedAgent.displayName,
1634
+ maxLines: 1,
1635
+ overflow: TextOverflow.ellipsis,
1636
+ style: const TextStyle(
1637
+ fontSize: 14,
1638
+ fontWeight: FontWeight.w700,
1639
+ letterSpacing: -0.15,
1640
+ ),
1641
+ ),
1642
+ ),
1643
+ if (selectedAgent.isDefault) ...<Widget>[
1644
+ const SizedBox(width: 8),
1645
+ _AgentTag(
1646
+ label: 'DEFAULT',
1647
+ color: _accent,
1648
+ foreground: _accentHover,
1649
+ ),
1650
+ ],
1651
+ ],
1652
+ ),
1653
+ const SizedBox(height: 3),
1654
+ Text(
1655
+ _agentSwitcherSubtitle(selectedAgent),
1656
+ maxLines: 1,
1657
+ overflow: TextOverflow.ellipsis,
1658
+ style: TextStyle(
1659
+ color: _textSecondary,
1660
+ fontSize: 11.5,
1661
+ fontWeight: FontWeight.w500,
1662
+ height: 1.2,
1663
+ ),
1664
+ ),
1665
+ ],
1666
+ ),
1667
+ ),
1668
+ const SizedBox(width: 10),
1669
+ AnimatedRotation(
1670
+ turns: isMenuOpen ? 0.5 : 0,
1671
+ duration: const Duration(milliseconds: 180),
1672
+ curve: Curves.easeOutCubic,
1673
+ child: Icon(
1674
+ Icons.keyboard_arrow_down_rounded,
1675
+ color: isMenuOpen ? _accentHover : _textSecondary,
1676
+ ),
1677
+ ),
1678
+ ],
1679
+ ),
1680
+ ),
1681
+ ),
1682
+ );
1508
1683
  },
1509
1684
  );
1510
1685
  }
1511
1686
  }
1512
1687
 
1688
+ String _agentSwitcherSubtitle(AgentProfile agent) {
1689
+ if (agent.description.trim().isNotEmpty) {
1690
+ return agent.description.trim();
1691
+ }
1692
+ if (agent.responsibilities.trim().isNotEmpty) {
1693
+ return agent.responsibilities.trim();
1694
+ }
1695
+ return agent.canDelegate
1696
+ ? 'Can coordinate delegated work'
1697
+ : 'Focused execution profile';
1698
+ }
1699
+
1700
+ class _AgentSwitcherMenuItem extends StatelessWidget {
1701
+ const _AgentSwitcherMenuItem({
1702
+ required this.agent,
1703
+ required this.selected,
1704
+ required this.onTap,
1705
+ });
1706
+
1707
+ final AgentProfile agent;
1708
+ final bool selected;
1709
+ final VoidCallback onTap;
1710
+
1711
+ @override
1712
+ Widget build(BuildContext context) {
1713
+ return Material(
1714
+ color: Colors.transparent,
1715
+ child: InkWell(
1716
+ borderRadius: BorderRadius.circular(18),
1717
+ onTap: onTap,
1718
+ child: Ink(
1719
+ decoration: BoxDecoration(
1720
+ borderRadius: BorderRadius.circular(18),
1721
+ gradient: selected
1722
+ ? LinearGradient(
1723
+ colors: <Color>[
1724
+ _accent.withValues(alpha: 0.18),
1725
+ _accentMuted.withValues(alpha: 0.3),
1726
+ ],
1727
+ begin: Alignment.topLeft,
1728
+ end: Alignment.bottomRight,
1729
+ )
1730
+ : null,
1731
+ color: selected ? null : Colors.white.withValues(alpha: 0.025),
1732
+ border: Border.all(
1733
+ color: selected ? _accent.withValues(alpha: 0.5) : _borderLight,
1734
+ ),
1735
+ ),
1736
+ child: Padding(
1737
+ padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
1738
+ child: Row(
1739
+ crossAxisAlignment: CrossAxisAlignment.start,
1740
+ children: <Widget>[
1741
+ _AgentGlyph(agent: agent, selected: selected, compact: true),
1742
+ const SizedBox(width: 12),
1743
+ Expanded(
1744
+ child: Column(
1745
+ crossAxisAlignment: CrossAxisAlignment.start,
1746
+ children: <Widget>[
1747
+ Row(
1748
+ children: <Widget>[
1749
+ Expanded(
1750
+ child: Text(
1751
+ agent.displayName,
1752
+ maxLines: 1,
1753
+ overflow: TextOverflow.ellipsis,
1754
+ style: TextStyle(
1755
+ color: _textPrimary,
1756
+ fontSize: 13.5,
1757
+ fontWeight: selected
1758
+ ? FontWeight.w700
1759
+ : FontWeight.w600,
1760
+ letterSpacing: -0.1,
1761
+ ),
1762
+ ),
1763
+ ),
1764
+ if (agent.isDefault) ...<Widget>[
1765
+ const SizedBox(width: 8),
1766
+ _AgentTag(
1767
+ label: 'DEFAULT',
1768
+ color: _accent,
1769
+ foreground: _accentHover,
1770
+ ),
1771
+ ],
1772
+ ],
1773
+ ),
1774
+ const SizedBox(height: 4),
1775
+ Text(
1776
+ _agentSwitcherSubtitle(agent),
1777
+ maxLines: 2,
1778
+ overflow: TextOverflow.ellipsis,
1779
+ style: TextStyle(
1780
+ color: _textSecondary,
1781
+ fontSize: 11.5,
1782
+ height: 1.3,
1783
+ fontWeight: FontWeight.w500,
1784
+ ),
1785
+ ),
1786
+ ],
1787
+ ),
1788
+ ),
1789
+ const SizedBox(width: 10),
1790
+ AnimatedOpacity(
1791
+ duration: const Duration(milliseconds: 140),
1792
+ opacity: selected ? 1 : 0,
1793
+ child: Container(
1794
+ width: 24,
1795
+ height: 24,
1796
+ decoration: BoxDecoration(
1797
+ shape: BoxShape.circle,
1798
+ color: _accent.withValues(alpha: 0.2),
1799
+ border: Border.all(
1800
+ color: _accent.withValues(alpha: 0.45),
1801
+ ),
1802
+ ),
1803
+ child: Icon(
1804
+ Icons.check_rounded,
1805
+ size: 15,
1806
+ color: _accentHover,
1807
+ ),
1808
+ ),
1809
+ ),
1810
+ ],
1811
+ ),
1812
+ ),
1813
+ ),
1814
+ ),
1815
+ );
1816
+ }
1817
+ }
1818
+
1819
+ class _AgentGlyph extends StatelessWidget {
1820
+ const _AgentGlyph({
1821
+ required this.agent,
1822
+ required this.selected,
1823
+ required this.compact,
1824
+ });
1825
+
1826
+ final AgentProfile agent;
1827
+ final bool selected;
1828
+ final bool compact;
1829
+
1830
+ @override
1831
+ Widget build(BuildContext context) {
1832
+ final baseColor = agent.isDefault ? _accent : _accentAlt;
1833
+ final initials = _agentInitials(agent.displayName);
1834
+ final size = compact ? 42.0 : 44.0;
1835
+ return Container(
1836
+ width: size,
1837
+ height: size,
1838
+ decoration: BoxDecoration(
1839
+ shape: BoxShape.circle,
1840
+ gradient: LinearGradient(
1841
+ colors: <Color>[
1842
+ baseColor.withValues(alpha: selected ? 0.85 : 0.65),
1843
+ Color.lerp(
1844
+ baseColor,
1845
+ _bgSecondary,
1846
+ 0.35,
1847
+ )!.withValues(alpha: selected ? 0.9 : 0.78),
1848
+ ],
1849
+ begin: Alignment.topLeft,
1850
+ end: Alignment.bottomRight,
1851
+ ),
1852
+ border: Border.all(
1853
+ color: Colors.white.withValues(alpha: selected ? 0.34 : 0.2),
1854
+ ),
1855
+ boxShadow: <BoxShadow>[
1856
+ BoxShadow(
1857
+ color: baseColor.withValues(alpha: selected ? 0.22 : 0.12),
1858
+ blurRadius: compact ? 12 : 16,
1859
+ offset: const Offset(0, 6),
1860
+ ),
1861
+ ],
1862
+ ),
1863
+ child: Stack(
1864
+ alignment: Alignment.center,
1865
+ children: <Widget>[
1866
+ Icon(
1867
+ agent.canDelegate ? Icons.hub_rounded : Icons.smart_toy_outlined,
1868
+ size: compact ? 17 : 18,
1869
+ color: Colors.white.withValues(alpha: 0.2),
1870
+ ),
1871
+ Text(
1872
+ initials,
1873
+ style: TextStyle(
1874
+ color: Colors.white,
1875
+ fontSize: compact ? 12 : 12.5,
1876
+ fontWeight: FontWeight.w800,
1877
+ letterSpacing: 0.35,
1878
+ ),
1879
+ ),
1880
+ ],
1881
+ ),
1882
+ );
1883
+ }
1884
+ }
1885
+
1886
+ class _AgentTag extends StatelessWidget {
1887
+ const _AgentTag({
1888
+ required this.label,
1889
+ required this.color,
1890
+ required this.foreground,
1891
+ });
1892
+
1893
+ final String label;
1894
+ final Color color;
1895
+ final Color foreground;
1896
+
1897
+ @override
1898
+ Widget build(BuildContext context) {
1899
+ return Container(
1900
+ padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
1901
+ decoration: BoxDecoration(
1902
+ borderRadius: BorderRadius.circular(999),
1903
+ color: color.withValues(alpha: 0.14),
1904
+ border: Border.all(color: color.withValues(alpha: 0.3)),
1905
+ ),
1906
+ child: Text(
1907
+ label,
1908
+ style: TextStyle(
1909
+ color: foreground,
1910
+ fontSize: 9.5,
1911
+ fontWeight: FontWeight.w800,
1912
+ letterSpacing: 0.6,
1913
+ ),
1914
+ ),
1915
+ );
1916
+ }
1917
+ }
1918
+
1919
+ String _agentInitials(String label) {
1920
+ final parts = label
1921
+ .trim()
1922
+ .split(RegExp(r'\s+'))
1923
+ .where((part) => part.isNotEmpty)
1924
+ .toList(growable: false);
1925
+ if (parts.isEmpty) return 'A';
1926
+ if (parts.length == 1) {
1927
+ return parts.first.characters.take(2).toString().toUpperCase();
1928
+ }
1929
+ return (parts.first.characters.first + parts.last.characters.first)
1930
+ .toUpperCase();
1931
+ }
1932
+
1513
1933
  class _ProfileSettingsButton extends StatelessWidget {
1514
1934
  const _ProfileSettingsButton({required this.controller, required this.onTap});
1515
1935
 
@@ -16,7 +16,7 @@ class NeoAgentController extends ChangeNotifier {
16
16
  _recordingBridge.onRecordingStopped = _handleRecordingStopped;
17
17
  _recordingBridge.addListener(_handleRecordingBridgeChanged);
18
18
  _desktopCompanion.addListener(notifyListeners);
19
-
19
+
20
20
  AndroidAutoBridge.instance.onStartVoiceMode = startLiveVoiceCapture;
21
21
  AndroidAutoBridge.instance.onStopVoiceMode = interruptLiveVoiceAssistant;
22
22
 
@@ -1210,10 +1210,11 @@ class NeoAgentController extends ChangeNotifier {
1210
1210
  isAwaitingTwoFactor = false;
1211
1211
  pendingTwoFactorUsername = '';
1212
1212
  password = '';
1213
-
1214
- final bool backendCompletedOnboarding = user?['hasCompletedOnboarding'] == true;
1213
+
1214
+ final bool backendCompletedOnboarding =
1215
+ user?['hasCompletedOnboarding'] == true;
1215
1216
  showOnboarding = isRegistration || !backendCompletedOnboarding;
1216
-
1217
+
1217
1218
  _clearQrLoginChallenge();
1218
1219
  await _persistCredentials();
1219
1220
  await refresh();
@@ -1458,6 +1459,10 @@ class NeoAgentController extends ChangeNotifier {
1458
1459
  }
1459
1460
  }
1460
1461
 
1462
+ void reopenOnboarding() {
1463
+ showOnboarding = true;
1464
+ notifyListeners();
1465
+ }
1461
1466
 
1462
1467
  void _clearAuthenticatedState() {
1463
1468
  _disconnectSocket();
@@ -3923,10 +3928,11 @@ class NeoAgentController extends ChangeNotifier {
3923
3928
  if (_isStartingLiveVoice || _isStoppingLiveVoice) {
3924
3929
  return;
3925
3930
  }
3926
-
3931
+
3927
3932
  bool routingStarted = false;
3928
3933
  try {
3929
- routingStarted = await AndroidAutoBridge.instance.startTelecomCallRouting();
3934
+ routingStarted = await AndroidAutoBridge.instance
3935
+ .startTelecomCallRouting();
3930
3936
  } catch (_) {
3931
3937
  // Swallowed safely
3932
3938
  }