neoagent 2.4.1-beta.8 → 2.4.1
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.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -9,23 +9,35 @@ class ChatPanel extends StatefulWidget {
|
|
|
9
9
|
State<ChatPanel> createState() => _ChatPanelState();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
class _ChatPanelState extends State<ChatPanel> {
|
|
12
|
+
class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
13
|
+
static const double _autoScrollBottomThreshold = 120;
|
|
14
|
+
static const int _autoScrollSettlePasses = 4;
|
|
15
|
+
|
|
13
16
|
late final TextEditingController _composerController;
|
|
14
17
|
final ScrollController _scrollController = ScrollController();
|
|
15
18
|
List<SharedChatAttachment> _pendingSharedAttachments =
|
|
16
19
|
const <SharedChatAttachment>[];
|
|
17
20
|
String? _appliedSharedPayloadSignature;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
String _lastScrollContentSignature = '';
|
|
22
|
+
bool _stickToBottom = true;
|
|
23
|
+
bool _ignoreScrollUpdates = false;
|
|
24
|
+
int _scrollGeneration = 0;
|
|
21
25
|
bool _isSendingChatMessage = false;
|
|
26
|
+
bool _isDictating = false;
|
|
27
|
+
bool _isTranscribing = false;
|
|
28
|
+
LiveVoiceCapture? _dictationCapture;
|
|
29
|
+
final List<Uint8List> _dictationChunks = [];
|
|
22
30
|
|
|
23
31
|
@override
|
|
24
32
|
void initState() {
|
|
25
33
|
super.initState();
|
|
34
|
+
WidgetsBinding.instance.addObserver(this);
|
|
26
35
|
_composerController = TextEditingController();
|
|
36
|
+
_composerController.addListener(_handleComposerLayoutChanged);
|
|
37
|
+
_scrollController.addListener(_handleScrollPositionChanged);
|
|
27
38
|
widget.controller.addListener(_consumeQueuedDraft);
|
|
28
39
|
_consumeQueuedDraft();
|
|
40
|
+
_scheduleScrollToBottom(force: true);
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
@override
|
|
@@ -35,18 +47,98 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
35
47
|
oldWidget.controller.removeListener(_consumeQueuedDraft);
|
|
36
48
|
widget.controller.addListener(_consumeQueuedDraft);
|
|
37
49
|
_appliedSharedPayloadSignature = null;
|
|
50
|
+
_lastScrollContentSignature = '';
|
|
51
|
+
_stickToBottom = true;
|
|
38
52
|
_consumeQueuedDraft();
|
|
53
|
+
_scheduleScrollToBottom(force: true);
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
@override
|
|
43
58
|
void dispose() {
|
|
59
|
+
WidgetsBinding.instance.removeObserver(this);
|
|
44
60
|
widget.controller.removeListener(_consumeQueuedDraft);
|
|
61
|
+
_composerController.removeListener(_handleComposerLayoutChanged);
|
|
62
|
+
_scrollController.removeListener(_handleScrollPositionChanged);
|
|
45
63
|
_composerController.dispose();
|
|
46
64
|
_scrollController.dispose();
|
|
65
|
+
_dictationCapture?.dispose();
|
|
47
66
|
super.dispose();
|
|
48
67
|
}
|
|
49
68
|
|
|
69
|
+
@override
|
|
70
|
+
void didChangeMetrics() {
|
|
71
|
+
super.didChangeMetrics();
|
|
72
|
+
if (_stickToBottom) {
|
|
73
|
+
_scheduleScrollToBottom();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Future<void> _startDictation() async {
|
|
78
|
+
if (_isDictating || _isTranscribing) return;
|
|
79
|
+
final capture = LiveVoiceCapture();
|
|
80
|
+
_dictationCapture = capture;
|
|
81
|
+
_dictationChunks.clear();
|
|
82
|
+
try {
|
|
83
|
+
await capture.start(
|
|
84
|
+
onChunk: (chunk) => _dictationChunks.add(chunk),
|
|
85
|
+
sampleRate: 16000,
|
|
86
|
+
channels: 1,
|
|
87
|
+
);
|
|
88
|
+
if (mounted) setState(() => _isDictating = true);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
await capture.dispose();
|
|
91
|
+
_dictationCapture = null;
|
|
92
|
+
if (mounted) {
|
|
93
|
+
ScaffoldMessenger.of(
|
|
94
|
+
context,
|
|
95
|
+
).showSnackBar(SnackBar(content: Text('Microphone error: $e')));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Future<void> _stopAndTranscribe() async {
|
|
101
|
+
if (!_isDictating) return;
|
|
102
|
+
final capture = _dictationCapture;
|
|
103
|
+
_dictationCapture = null;
|
|
104
|
+
setState(() {
|
|
105
|
+
_isDictating = false;
|
|
106
|
+
_isTranscribing = true;
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
await capture?.stop();
|
|
110
|
+
if (_dictationChunks.isEmpty) return;
|
|
111
|
+
final allBytes = _dictationChunks.fold<List<int>>(
|
|
112
|
+
<int>[],
|
|
113
|
+
(acc, chunk) => acc..addAll(chunk),
|
|
114
|
+
);
|
|
115
|
+
final audioBase64 = base64Encode(Uint8List.fromList(allBytes));
|
|
116
|
+
final transcript = await widget.controller.transcribeDictationAudio(
|
|
117
|
+
audioBase64: audioBase64,
|
|
118
|
+
);
|
|
119
|
+
if (mounted && transcript.isNotEmpty) {
|
|
120
|
+
final current = _composerController.text;
|
|
121
|
+
final separator = current.isNotEmpty && !current.endsWith(' ')
|
|
122
|
+
? ' '
|
|
123
|
+
: '';
|
|
124
|
+
_composerController.text = '$current$separator$transcript';
|
|
125
|
+
_composerController.selection = TextSelection.collapsed(
|
|
126
|
+
offset: _composerController.text.length,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (mounted) {
|
|
131
|
+
ScaffoldMessenger.of(
|
|
132
|
+
context,
|
|
133
|
+
).showSnackBar(SnackBar(content: Text('Transcription failed: $e')));
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
await capture?.dispose();
|
|
137
|
+
_dictationChunks.clear();
|
|
138
|
+
if (mounted) setState(() => _isTranscribing = false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
50
142
|
void _consumeQueuedDraft() {
|
|
51
143
|
final draft = widget.controller.peekPendingChatDraft();
|
|
52
144
|
final attachments = widget.controller.peekPendingSharedChatAttachments();
|
|
@@ -90,6 +182,9 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
90
182
|
setState(() {
|
|
91
183
|
_pendingSharedAttachments = const <SharedChatAttachment>[];
|
|
92
184
|
});
|
|
185
|
+
if (_stickToBottom) {
|
|
186
|
+
_scheduleScrollToBottom();
|
|
187
|
+
}
|
|
93
188
|
}
|
|
94
189
|
|
|
95
190
|
String _mimeTypeForFileName(String fileName) {
|
|
@@ -136,10 +231,9 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
136
231
|
}
|
|
137
232
|
final attachments = result.files
|
|
138
233
|
.where(
|
|
139
|
-
(file) =>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
: file.path?.trim().isNotEmpty == true,
|
|
234
|
+
(file) => kIsWeb
|
|
235
|
+
? file.bytes != null
|
|
236
|
+
: file.path?.trim().isNotEmpty == true,
|
|
143
237
|
)
|
|
144
238
|
.map(
|
|
145
239
|
(file) => SharedChatAttachment(
|
|
@@ -161,271 +255,621 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
161
255
|
...attachments,
|
|
162
256
|
];
|
|
163
257
|
});
|
|
258
|
+
if (_stickToBottom) {
|
|
259
|
+
_scheduleScrollToBottom();
|
|
260
|
+
}
|
|
164
261
|
}
|
|
165
262
|
|
|
166
263
|
bool get _isNearBottom {
|
|
167
264
|
if (!_scrollController.hasClients) return true;
|
|
168
265
|
final pos = _scrollController.position;
|
|
169
266
|
if (!pos.hasContentDimensions) return true;
|
|
170
|
-
return pos.pixels >= pos.maxScrollExtent -
|
|
267
|
+
return pos.pixels >= pos.maxScrollExtent - _autoScrollBottomThreshold;
|
|
171
268
|
}
|
|
172
269
|
|
|
173
|
-
void
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
270
|
+
void _handleScrollPositionChanged() {
|
|
271
|
+
if (_ignoreScrollUpdates || !_scrollController.hasClients) return;
|
|
272
|
+
final nearBottom = _isNearBottom;
|
|
273
|
+
if (_stickToBottom && !nearBottom) {
|
|
274
|
+
_scrollGeneration++;
|
|
275
|
+
}
|
|
276
|
+
if (_stickToBottom != nearBottom) {
|
|
277
|
+
setState(() => _stickToBottom = nearBottom);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
void _openModelPicker() {
|
|
282
|
+
final controller = widget.controller;
|
|
283
|
+
if (controller.hasLiveRun) return;
|
|
284
|
+
final enabled = controller.enabledModelIds;
|
|
285
|
+
final models = controller.supportedModels
|
|
286
|
+
.where((m) => enabled.contains(m.id))
|
|
287
|
+
.toList();
|
|
288
|
+
final options = _modelPickerOptions(models, allowAuto: true);
|
|
289
|
+
showGeneralDialog<void>(
|
|
290
|
+
context: context,
|
|
291
|
+
barrierDismissible: true,
|
|
292
|
+
barrierLabel: 'Dismiss',
|
|
293
|
+
barrierColor: Colors.black.withValues(alpha: 0.55),
|
|
294
|
+
transitionDuration: const Duration(milliseconds: 230),
|
|
295
|
+
transitionBuilder: (ctx, animation, secondary, child) => FadeTransition(
|
|
296
|
+
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
|
297
|
+
child: SlideTransition(
|
|
298
|
+
position: Tween<Offset>(
|
|
299
|
+
begin: const Offset(0, 0.04),
|
|
300
|
+
end: Offset.zero,
|
|
301
|
+
).animate(
|
|
302
|
+
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
|
|
303
|
+
),
|
|
304
|
+
child: child,
|
|
305
|
+
),
|
|
306
|
+
),
|
|
307
|
+
pageBuilder: (dialogContext, _, __) => _ModelPickerDialog(
|
|
308
|
+
title: 'Chat Model',
|
|
309
|
+
options: options,
|
|
310
|
+
currentValue: controller.defaultChatModel,
|
|
311
|
+
onChanged: (v) {
|
|
312
|
+
Navigator.of(dialogContext).pop();
|
|
313
|
+
controller.saveSettingsPayload({'default_chat_model': v});
|
|
314
|
+
},
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
void _handleComposerLayoutChanged() {
|
|
320
|
+
if (_stickToBottom) {
|
|
321
|
+
_scheduleScrollToBottom();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
String _scrollContentSignature(
|
|
326
|
+
List<ChatEntry> messages,
|
|
327
|
+
NeoAgentController controller,
|
|
328
|
+
) {
|
|
329
|
+
final last = messages.isEmpty ? null : messages.last;
|
|
330
|
+
final activeRun = controller.activeRun;
|
|
331
|
+
return <Object?>[
|
|
332
|
+
messages.length,
|
|
333
|
+
last?.id,
|
|
334
|
+
last?.role,
|
|
335
|
+
last?.content.length,
|
|
336
|
+
last?.typing,
|
|
337
|
+
controller.toolEvents.length,
|
|
338
|
+
activeRun?.runId,
|
|
339
|
+
activeRun?.phase,
|
|
340
|
+
activeRun?.iteration,
|
|
341
|
+
controller.streamingAssistant.length,
|
|
342
|
+
controller.isSendingMessage,
|
|
343
|
+
].join('|');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
void _scheduleScrollToBottom({bool force = false}) {
|
|
347
|
+
if (!force && !_stickToBottom) return;
|
|
348
|
+
final generation = ++_scrollGeneration;
|
|
349
|
+
|
|
350
|
+
void settle(int remainingPasses) {
|
|
351
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
352
|
+
if (!mounted ||
|
|
353
|
+
generation != _scrollGeneration ||
|
|
354
|
+
!_scrollController.hasClients) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
final pos = _scrollController.position;
|
|
358
|
+
if (pos.hasContentDimensions) {
|
|
359
|
+
final target = pos.maxScrollExtent;
|
|
360
|
+
if ((pos.pixels - target).abs() > 0.5) {
|
|
361
|
+
_ignoreScrollUpdates = true;
|
|
362
|
+
_scrollController.jumpTo(target);
|
|
363
|
+
_ignoreScrollUpdates = false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
_stickToBottom = true;
|
|
367
|
+
if (remainingPasses > 0) {
|
|
368
|
+
Timer(const Duration(milliseconds: 24), () {
|
|
369
|
+
if (mounted && generation == _scrollGeneration) {
|
|
370
|
+
settle(remainingPasses - 1);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
settle(_autoScrollSettlePasses);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
void _maybeFollowChatContent(
|
|
381
|
+
List<ChatEntry> messages,
|
|
382
|
+
NeoAgentController controller,
|
|
383
|
+
) {
|
|
384
|
+
final signature = _scrollContentSignature(messages, controller);
|
|
385
|
+
if (_lastScrollContentSignature == signature) return;
|
|
386
|
+
final isInitialContent = _lastScrollContentSignature.isEmpty;
|
|
387
|
+
final shouldFollow = _stickToBottom || isInitialContent;
|
|
388
|
+
_lastScrollContentSignature = signature;
|
|
389
|
+
if (shouldFollow) {
|
|
390
|
+
_scheduleScrollToBottom(force: isInitialContent);
|
|
391
|
+
}
|
|
181
392
|
}
|
|
182
393
|
|
|
183
394
|
@override
|
|
184
395
|
Widget build(BuildContext context) {
|
|
185
396
|
final controller = widget.controller;
|
|
186
397
|
final messages = controller.visibleChatMessages;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
398
|
+
_maybeFollowChatContent(messages, controller);
|
|
399
|
+
|
|
400
|
+
final threadChildren = <Widget>[
|
|
401
|
+
if (controller.errorMessage != null) ...<Widget>[
|
|
402
|
+
_InlineError(message: controller.errorMessage!),
|
|
403
|
+
const SizedBox(height: 16),
|
|
404
|
+
],
|
|
405
|
+
if (controller.activeRun != null || controller.toolEvents.isNotEmpty)
|
|
406
|
+
Padding(
|
|
407
|
+
padding: const EdgeInsets.only(bottom: 16),
|
|
408
|
+
child: _RunStatusPanel(
|
|
409
|
+
run: controller.activeRun,
|
|
410
|
+
tools: controller.toolEvents,
|
|
411
|
+
),
|
|
412
|
+
),
|
|
413
|
+
if (messages.isEmpty)
|
|
414
|
+
Padding(
|
|
415
|
+
padding: const EdgeInsets.only(top: 64),
|
|
416
|
+
child: Center(
|
|
417
|
+
child: _EmptyState(
|
|
418
|
+
title: 'How can I help?',
|
|
419
|
+
subtitle:
|
|
420
|
+
'Runs, tools, memory, scheduling, skills, and MCP are all available here.',
|
|
421
|
+
),
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
else
|
|
425
|
+
...messages.map(
|
|
426
|
+
(entry) => Padding(
|
|
427
|
+
padding: const EdgeInsets.only(bottom: 18),
|
|
428
|
+
child: _ChatBubble(
|
|
429
|
+
entry: entry,
|
|
430
|
+
onLoadRunDetail: controller.fetchRunDetail,
|
|
431
|
+
onSendMessage: controller.sendMessage,
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
),
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
Future<void> sendComposerMessage() async {
|
|
438
|
+
final task = _composerController.text;
|
|
439
|
+
if ((task.trim().isEmpty && _pendingSharedAttachments.isEmpty) ||
|
|
440
|
+
_isSendingChatMessage) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
setState(() {
|
|
444
|
+
_isSendingChatMessage = true;
|
|
445
|
+
});
|
|
446
|
+
_composerController.clear();
|
|
447
|
+
final outgoingAttachments = _pendingSharedAttachments;
|
|
448
|
+
_clearSharedPayload();
|
|
449
|
+
try {
|
|
450
|
+
await controller.sendMessage(
|
|
451
|
+
task,
|
|
452
|
+
sharedAttachments: outgoingAttachments,
|
|
453
|
+
);
|
|
454
|
+
} finally {
|
|
455
|
+
if (mounted) {
|
|
456
|
+
setState(() {
|
|
457
|
+
_isSendingChatMessage = false;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
194
461
|
}
|
|
195
462
|
|
|
463
|
+
final sidePadding = _chatSidePadding(context);
|
|
464
|
+
|
|
196
465
|
return Column(
|
|
197
466
|
children: <Widget>[
|
|
467
|
+
_ChatTopBar(controller: controller),
|
|
198
468
|
Expanded(
|
|
199
|
-
child:
|
|
200
|
-
child: ListView(
|
|
201
|
-
controller: _scrollController,
|
|
202
|
-
padding: _pagePadding(context),
|
|
469
|
+
child: Stack(
|
|
203
470
|
children: <Widget>[
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
runSpacing: 10,
|
|
210
|
-
crossAxisAlignment: WrapCrossAlignment.center,
|
|
471
|
+
SelectionArea(
|
|
472
|
+
child: ListView(
|
|
473
|
+
controller: _scrollController,
|
|
474
|
+
padding:
|
|
475
|
+
EdgeInsets.fromLTRB(sidePadding, 30, sidePadding, 18),
|
|
211
476
|
children: <Widget>[
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
477
|
+
Center(
|
|
478
|
+
child: ConstrainedBox(
|
|
479
|
+
constraints: const BoxConstraints(maxWidth: 860),
|
|
480
|
+
child: Column(
|
|
481
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
482
|
+
children: threadChildren,
|
|
483
|
+
),
|
|
215
484
|
),
|
|
216
|
-
icon: Icon(Icons.call),
|
|
217
|
-
label: Text('Call'),
|
|
218
|
-
),
|
|
219
|
-
_MetaPill(
|
|
220
|
-
label: controller.modelIndicator,
|
|
221
|
-
icon: Icons.memory_outlined,
|
|
222
|
-
),
|
|
223
|
-
_MetaPill(
|
|
224
|
-
label: 'Agent: ${controller.activeAgentLabel}',
|
|
225
|
-
icon: Icons.smart_toy_outlined,
|
|
226
485
|
),
|
|
227
486
|
],
|
|
228
487
|
),
|
|
229
488
|
),
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
run: controller.activeRun,
|
|
240
|
-
tools: controller.toolEvents,
|
|
241
|
-
),
|
|
242
|
-
),
|
|
243
|
-
if (messages.isEmpty)
|
|
244
|
-
Padding(
|
|
245
|
-
padding: EdgeInsets.only(top: 64),
|
|
246
|
-
child: Center(
|
|
247
|
-
child: _EmptyState(
|
|
248
|
-
title: 'How can I help?',
|
|
249
|
-
subtitle:
|
|
250
|
-
'Runs, tools, memory, scheduling, skills, and MCP are all available here.',
|
|
251
|
-
),
|
|
252
|
-
),
|
|
253
|
-
)
|
|
254
|
-
else
|
|
255
|
-
...messages.map(
|
|
256
|
-
(entry) => Padding(
|
|
257
|
-
padding: const EdgeInsets.only(bottom: 18),
|
|
258
|
-
child: _ChatBubble(
|
|
259
|
-
entry: entry,
|
|
260
|
-
onLoadRunDetail: controller.fetchRunDetail,
|
|
261
|
-
),
|
|
489
|
+
if (!_stickToBottom)
|
|
490
|
+
Positioned(
|
|
491
|
+
bottom: 14,
|
|
492
|
+
right: 16,
|
|
493
|
+
child: _ScrollToBottomButton(
|
|
494
|
+
onTap: () {
|
|
495
|
+
setState(() => _stickToBottom = true);
|
|
496
|
+
_scheduleScrollToBottom(force: true);
|
|
497
|
+
},
|
|
262
498
|
),
|
|
263
499
|
),
|
|
264
500
|
],
|
|
265
501
|
),
|
|
266
|
-
),
|
|
267
502
|
),
|
|
268
503
|
Container(
|
|
269
|
-
padding:
|
|
504
|
+
padding: EdgeInsets.fromLTRB(sidePadding, 12, sidePadding, 16),
|
|
270
505
|
decoration: BoxDecoration(
|
|
271
|
-
color: _bgPrimary,
|
|
506
|
+
color: _bgPrimary.withValues(alpha: 0.88),
|
|
272
507
|
border: Border(top: BorderSide(color: _border)),
|
|
273
508
|
),
|
|
274
|
-
child:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
color: _bgTertiary,
|
|
303
|
-
borderRadius: BorderRadius.circular(14),
|
|
304
|
-
border: Border.all(color: _border),
|
|
305
|
-
),
|
|
306
|
-
child: Row(
|
|
307
|
-
crossAxisAlignment: CrossAxisAlignment.end,
|
|
308
|
-
children: <Widget>[
|
|
309
|
-
Expanded(
|
|
310
|
-
child: TextField(
|
|
311
|
-
controller: _composerController,
|
|
312
|
-
minLines: 1,
|
|
313
|
-
maxLines: 6,
|
|
314
|
-
keyboardType: TextInputType.multiline,
|
|
315
|
-
textInputAction: TextInputAction.newline,
|
|
316
|
-
decoration: InputDecoration(
|
|
317
|
-
hintText: controller.chatComposerHint,
|
|
318
|
-
isDense: true,
|
|
319
|
-
filled: false,
|
|
320
|
-
border: InputBorder.none,
|
|
321
|
-
enabledBorder: InputBorder.none,
|
|
322
|
-
focusedBorder: InputBorder.none,
|
|
323
|
-
),
|
|
509
|
+
child: Center(
|
|
510
|
+
child: ConstrainedBox(
|
|
511
|
+
constraints: const BoxConstraints(maxWidth: 860),
|
|
512
|
+
child: Column(
|
|
513
|
+
children: <Widget>[
|
|
514
|
+
if (_pendingSharedAttachments.isNotEmpty)
|
|
515
|
+
Padding(
|
|
516
|
+
padding: const EdgeInsets.only(bottom: 10),
|
|
517
|
+
child: _SharedAttachmentTray(
|
|
518
|
+
attachments: _pendingSharedAttachments,
|
|
519
|
+
onRemoveAt: (index) {
|
|
520
|
+
setState(() {
|
|
521
|
+
_pendingSharedAttachments =
|
|
522
|
+
_pendingSharedAttachments
|
|
523
|
+
.asMap()
|
|
524
|
+
.entries
|
|
525
|
+
.where((entry) => entry.key != index)
|
|
526
|
+
.map((entry) => entry.value)
|
|
527
|
+
.toList(growable: false);
|
|
528
|
+
});
|
|
529
|
+
if (_stickToBottom) {
|
|
530
|
+
_scheduleScrollToBottom();
|
|
531
|
+
}
|
|
532
|
+
if (_pendingSharedAttachments.isEmpty) {
|
|
533
|
+
_clearSharedPayload();
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
onClear: _clearSharedPayload,
|
|
324
537
|
),
|
|
325
538
|
),
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
color:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
),
|
|
338
|
-
style: FilledButton.styleFrom(
|
|
339
|
-
minimumSize: const Size(46, 42),
|
|
340
|
-
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
341
|
-
backgroundColor: _success,
|
|
342
|
-
shape: RoundedRectangleBorder(
|
|
343
|
-
borderRadius: BorderRadius.circular(10),
|
|
539
|
+
Container(
|
|
540
|
+
padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
|
|
541
|
+
decoration: BoxDecoration(
|
|
542
|
+
color: _bgCard,
|
|
543
|
+
borderRadius: BorderRadius.circular(21),
|
|
544
|
+
border: Border.all(color: _borderLight),
|
|
545
|
+
boxShadow: <BoxShadow>[
|
|
546
|
+
BoxShadow(
|
|
547
|
+
color: Colors.black.withValues(alpha: 0.08),
|
|
548
|
+
blurRadius: 18,
|
|
549
|
+
offset: const Offset(0, 6),
|
|
344
550
|
),
|
|
345
|
-
|
|
346
|
-
child: Icon(Icons.call_rounded, color: Colors.white),
|
|
551
|
+
],
|
|
347
552
|
),
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
setState(() {
|
|
376
|
-
_isSendingChatMessage = false;
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
style: FilledButton.styleFrom(
|
|
382
|
-
minimumSize: const Size(46, 42),
|
|
383
|
-
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
384
|
-
backgroundColor: _accent,
|
|
385
|
-
shape: RoundedRectangleBorder(
|
|
386
|
-
borderRadius: BorderRadius.circular(10),
|
|
553
|
+
child: Row(
|
|
554
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
555
|
+
children: <Widget>[
|
|
556
|
+
_ChatComposerIconButton(
|
|
557
|
+
tooltip: 'Attach files',
|
|
558
|
+
icon: Icons.attach_file_rounded,
|
|
559
|
+
onPressed: _attachFiles,
|
|
560
|
+
),
|
|
561
|
+
const SizedBox(width: 8),
|
|
562
|
+
Expanded(
|
|
563
|
+
child: TextField(
|
|
564
|
+
controller: _composerController,
|
|
565
|
+
minLines: 1,
|
|
566
|
+
maxLines: 6,
|
|
567
|
+
keyboardType: TextInputType.multiline,
|
|
568
|
+
textInputAction: TextInputAction.newline,
|
|
569
|
+
decoration: InputDecoration(
|
|
570
|
+
hintText: controller.chatComposerHint,
|
|
571
|
+
isDense: true,
|
|
572
|
+
filled: false,
|
|
573
|
+
border: InputBorder.none,
|
|
574
|
+
enabledBorder: InputBorder.none,
|
|
575
|
+
focusedBorder: InputBorder.none,
|
|
576
|
+
contentPadding: const EdgeInsets.symmetric(
|
|
577
|
+
vertical: 10,
|
|
578
|
+
),
|
|
579
|
+
),
|
|
387
580
|
),
|
|
388
581
|
),
|
|
389
|
-
|
|
390
|
-
|
|
582
|
+
const SizedBox(width: 8),
|
|
583
|
+
_isTranscribing
|
|
584
|
+
? const SizedBox(
|
|
585
|
+
width: 40,
|
|
586
|
+
height: 40,
|
|
587
|
+
child: Padding(
|
|
588
|
+
padding: EdgeInsets.all(10),
|
|
589
|
+
child: CircularProgressIndicator(
|
|
590
|
+
strokeWidth: 2,
|
|
591
|
+
),
|
|
592
|
+
),
|
|
593
|
+
)
|
|
594
|
+
: _ChatComposerIconButton(
|
|
595
|
+
tooltip: _isDictating
|
|
596
|
+
? 'Stop & transcribe'
|
|
597
|
+
: 'Dictate',
|
|
598
|
+
icon: _isDictating
|
|
599
|
+
? Icons.stop_circle_outlined
|
|
600
|
+
: Icons.mic_none_rounded,
|
|
601
|
+
color: _isDictating
|
|
602
|
+
? Theme.of(context).colorScheme.error
|
|
603
|
+
: null,
|
|
604
|
+
onPressed: _isDictating
|
|
605
|
+
? _stopAndTranscribe
|
|
606
|
+
: _startDictation,
|
|
607
|
+
),
|
|
608
|
+
const SizedBox(width: 6),
|
|
609
|
+
_ChatComposerIconButton(
|
|
610
|
+
tooltip: 'Call agent',
|
|
611
|
+
icon: Icons.call_rounded,
|
|
612
|
+
color: Colors.white,
|
|
613
|
+
backgroundColor: _success,
|
|
614
|
+
onPressed: () => controller.setSelectedSection(
|
|
615
|
+
AppSection.voiceAssistant,
|
|
616
|
+
),
|
|
617
|
+
),
|
|
618
|
+
const SizedBox(width: 6),
|
|
619
|
+
_ChatComposerIconButton(
|
|
620
|
+
tooltip: 'Send',
|
|
621
|
+
icon: controller.hasLiveRun
|
|
391
622
|
? Icons.alt_route_rounded
|
|
392
623
|
: Icons.north_east_rounded,
|
|
393
624
|
color: Colors.white,
|
|
625
|
+
backgroundColor: _accent,
|
|
626
|
+
onPressed: _isSendingChatMessage
|
|
627
|
+
? null
|
|
628
|
+
: sendComposerMessage,
|
|
629
|
+
),
|
|
630
|
+
],
|
|
631
|
+
),
|
|
632
|
+
),
|
|
633
|
+
const SizedBox(height: 10),
|
|
634
|
+
Row(
|
|
635
|
+
children: <Widget>[
|
|
636
|
+
Expanded(
|
|
637
|
+
child: Row(
|
|
638
|
+
children: <Widget>[
|
|
639
|
+
Container(
|
|
640
|
+
width: 6,
|
|
641
|
+
height: 6,
|
|
642
|
+
decoration: BoxDecoration(
|
|
643
|
+
shape: BoxShape.circle,
|
|
644
|
+
color: controller.hasLiveRun
|
|
645
|
+
? _success
|
|
646
|
+
: _textMuted,
|
|
647
|
+
),
|
|
648
|
+
),
|
|
649
|
+
const SizedBox(width: 7),
|
|
650
|
+
Expanded(
|
|
651
|
+
child: Text(
|
|
652
|
+
controller.chatStatusLabel,
|
|
653
|
+
maxLines: 1,
|
|
654
|
+
overflow: TextOverflow.ellipsis,
|
|
655
|
+
style: GoogleFonts.geistMono(
|
|
656
|
+
fontSize: 11.5,
|
|
657
|
+
color: _textMuted,
|
|
658
|
+
),
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
],
|
|
394
662
|
),
|
|
395
663
|
),
|
|
664
|
+
const SizedBox(width: 12),
|
|
665
|
+
GestureDetector(
|
|
666
|
+
onTap: controller.hasLiveRun ? null : _openModelPicker,
|
|
667
|
+
child: Container(
|
|
668
|
+
padding: const EdgeInsets.symmetric(
|
|
669
|
+
horizontal: 9,
|
|
670
|
+
vertical: 4,
|
|
671
|
+
),
|
|
672
|
+
constraints: const BoxConstraints(maxWidth: 200),
|
|
673
|
+
decoration: BoxDecoration(
|
|
674
|
+
color: _bgCard,
|
|
675
|
+
borderRadius: BorderRadius.circular(8),
|
|
676
|
+
border: Border.all(color: _border),
|
|
677
|
+
),
|
|
678
|
+
child: Row(
|
|
679
|
+
mainAxisSize: MainAxisSize.min,
|
|
680
|
+
children: <Widget>[
|
|
681
|
+
Flexible(
|
|
682
|
+
child: Text(
|
|
683
|
+
controller.hasLiveRun
|
|
684
|
+
? 'Steering mode'
|
|
685
|
+
: controller.modelIndicator,
|
|
686
|
+
maxLines: 1,
|
|
687
|
+
overflow: TextOverflow.ellipsis,
|
|
688
|
+
style: GoogleFonts.geistMono(
|
|
689
|
+
fontSize: 11.5,
|
|
690
|
+
color: _textSecondary,
|
|
691
|
+
),
|
|
692
|
+
),
|
|
693
|
+
),
|
|
694
|
+
const SizedBox(width: 5),
|
|
695
|
+
Icon(
|
|
696
|
+
Icons.keyboard_arrow_down_rounded,
|
|
697
|
+
size: 13,
|
|
698
|
+
color: controller.hasLiveRun
|
|
699
|
+
? _textMuted
|
|
700
|
+
: _textSecondary,
|
|
701
|
+
),
|
|
702
|
+
],
|
|
703
|
+
),
|
|
704
|
+
),
|
|
705
|
+
),
|
|
706
|
+
],
|
|
707
|
+
),
|
|
708
|
+
],
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
),
|
|
712
|
+
),
|
|
713
|
+
],
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
double _chatSidePadding(BuildContext context) {
|
|
719
|
+
final width = MediaQuery.sizeOf(context).width;
|
|
720
|
+
if (width >= 1280) return 40;
|
|
721
|
+
if (width >= 900) return 30;
|
|
722
|
+
return 20;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
class _ChatTopBar extends StatelessWidget {
|
|
726
|
+
const _ChatTopBar({required this.controller});
|
|
727
|
+
|
|
728
|
+
final NeoAgentController controller;
|
|
729
|
+
|
|
730
|
+
@override
|
|
731
|
+
Widget build(BuildContext context) {
|
|
732
|
+
return Container(
|
|
733
|
+
height: 58,
|
|
734
|
+
padding: EdgeInsets.symmetric(horizontal: _chatSidePadding(context)),
|
|
735
|
+
decoration: BoxDecoration(
|
|
736
|
+
color: _bgPrimary.withValues(alpha: 0.72),
|
|
737
|
+
border: Border(bottom: BorderSide(color: _border)),
|
|
738
|
+
),
|
|
739
|
+
child: Row(
|
|
740
|
+
children: <Widget>[
|
|
741
|
+
Text(
|
|
742
|
+
'Chat',
|
|
743
|
+
style: GoogleFonts.geist(
|
|
744
|
+
fontSize: 15,
|
|
745
|
+
fontWeight: FontWeight.w600,
|
|
746
|
+
color: _textPrimary,
|
|
747
|
+
),
|
|
748
|
+
),
|
|
749
|
+
const SizedBox(width: 12),
|
|
750
|
+
Expanded(
|
|
751
|
+
child: Text(
|
|
752
|
+
'/ ${controller.activeAgentLabel} / ${controller.modelIndicator}',
|
|
753
|
+
maxLines: 1,
|
|
754
|
+
overflow: TextOverflow.ellipsis,
|
|
755
|
+
style: GoogleFonts.geistMono(fontSize: 11, color: _textMuted),
|
|
756
|
+
),
|
|
757
|
+
),
|
|
758
|
+
const SizedBox(width: 12),
|
|
759
|
+
Row(
|
|
760
|
+
mainAxisSize: MainAxisSize.min,
|
|
761
|
+
children: <Widget>[
|
|
762
|
+
Container(
|
|
763
|
+
width: 7,
|
|
764
|
+
height: 7,
|
|
765
|
+
decoration: BoxDecoration(
|
|
766
|
+
shape: BoxShape.circle,
|
|
767
|
+
color: controller.hasLiveRun ? _success : _accentAlt,
|
|
768
|
+
boxShadow: <BoxShadow>[
|
|
769
|
+
BoxShadow(
|
|
770
|
+
color: (controller.hasLiveRun ? _success : _accentAlt)
|
|
771
|
+
.withValues(alpha: 0.18),
|
|
772
|
+
blurRadius: 0,
|
|
773
|
+
spreadRadius: 3,
|
|
396
774
|
),
|
|
397
775
|
],
|
|
398
776
|
),
|
|
399
777
|
),
|
|
400
|
-
const SizedBox(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
overflow: TextOverflow.ellipsis,
|
|
408
|
-
style: TextStyle(fontSize: 12, color: _textSecondary),
|
|
409
|
-
),
|
|
410
|
-
),
|
|
411
|
-
const SizedBox(width: 12),
|
|
412
|
-
Flexible(
|
|
413
|
-
child: Text(
|
|
414
|
-
controller.hasLiveRun
|
|
415
|
-
? 'Steering mode'
|
|
416
|
-
: controller.modelIndicator,
|
|
417
|
-
maxLines: 1,
|
|
418
|
-
overflow: TextOverflow.ellipsis,
|
|
419
|
-
textAlign: TextAlign.right,
|
|
420
|
-
style: TextStyle(fontSize: 12, color: _textSecondary),
|
|
421
|
-
),
|
|
422
|
-
),
|
|
423
|
-
],
|
|
778
|
+
const SizedBox(width: 7),
|
|
779
|
+
Text(
|
|
780
|
+
controller.hasLiveRun ? 'live' : 'idle',
|
|
781
|
+
style: GoogleFonts.geistMono(
|
|
782
|
+
fontSize: 12,
|
|
783
|
+
color: _textSecondary,
|
|
784
|
+
),
|
|
424
785
|
),
|
|
425
786
|
],
|
|
426
787
|
),
|
|
788
|
+
],
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
class _ChatComposerIconButton extends StatelessWidget {
|
|
795
|
+
const _ChatComposerIconButton({
|
|
796
|
+
required this.tooltip,
|
|
797
|
+
required this.icon,
|
|
798
|
+
required this.onPressed,
|
|
799
|
+
this.color,
|
|
800
|
+
this.backgroundColor,
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
final String tooltip;
|
|
804
|
+
final IconData icon;
|
|
805
|
+
final VoidCallback? onPressed;
|
|
806
|
+
final Color? color;
|
|
807
|
+
final Color? backgroundColor;
|
|
808
|
+
|
|
809
|
+
@override
|
|
810
|
+
Widget build(BuildContext context) {
|
|
811
|
+
final foreground = color ?? _textSecondary;
|
|
812
|
+
return Tooltip(
|
|
813
|
+
message: tooltip,
|
|
814
|
+
child: Material(
|
|
815
|
+
color: Colors.transparent,
|
|
816
|
+
borderRadius: BorderRadius.circular(11),
|
|
817
|
+
child: InkWell(
|
|
818
|
+
borderRadius: BorderRadius.circular(11),
|
|
819
|
+
onTap: onPressed,
|
|
820
|
+
child: Container(
|
|
821
|
+
width: 40,
|
|
822
|
+
height: 40,
|
|
823
|
+
decoration: BoxDecoration(
|
|
824
|
+
color: onPressed == null
|
|
825
|
+
? _bgTertiary.withValues(alpha: 0.52)
|
|
826
|
+
: backgroundColor ?? Colors.transparent,
|
|
827
|
+
borderRadius: BorderRadius.circular(11),
|
|
828
|
+
),
|
|
829
|
+
child: Icon(
|
|
830
|
+
icon,
|
|
831
|
+
size: 19,
|
|
832
|
+
color: onPressed == null ? _textMuted : foreground,
|
|
833
|
+
),
|
|
834
|
+
),
|
|
427
835
|
),
|
|
428
|
-
|
|
836
|
+
),
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
class _ScrollToBottomButton extends StatelessWidget {
|
|
842
|
+
const _ScrollToBottomButton({required this.onTap});
|
|
843
|
+
|
|
844
|
+
final VoidCallback onTap;
|
|
845
|
+
|
|
846
|
+
@override
|
|
847
|
+
Widget build(BuildContext context) {
|
|
848
|
+
return Tooltip(
|
|
849
|
+
message: 'Scroll to bottom',
|
|
850
|
+
child: Material(
|
|
851
|
+
color: _bgCard,
|
|
852
|
+
borderRadius: BorderRadius.circular(20),
|
|
853
|
+
elevation: 4,
|
|
854
|
+
shadowColor: Colors.black.withValues(alpha: 0.22),
|
|
855
|
+
child: InkWell(
|
|
856
|
+
onTap: onTap,
|
|
857
|
+
borderRadius: BorderRadius.circular(20),
|
|
858
|
+
child: Container(
|
|
859
|
+
width: 38,
|
|
860
|
+
height: 38,
|
|
861
|
+
decoration: BoxDecoration(
|
|
862
|
+
borderRadius: BorderRadius.circular(20),
|
|
863
|
+
border: Border.all(color: _border),
|
|
864
|
+
),
|
|
865
|
+
child: Icon(
|
|
866
|
+
Icons.keyboard_arrow_down_rounded,
|
|
867
|
+
size: 22,
|
|
868
|
+
color: _textSecondary,
|
|
869
|
+
),
|
|
870
|
+
),
|
|
871
|
+
),
|
|
872
|
+
),
|
|
429
873
|
);
|
|
430
874
|
}
|
|
431
875
|
}
|
|
@@ -1700,9 +2144,17 @@ class _MessagingCard extends StatelessWidget {
|
|
|
1700
2144
|
Widget build(BuildContext context) {
|
|
1701
2145
|
final connected = status?.isConnected ?? false;
|
|
1702
2146
|
final configured = status != null && status!.status != 'not_configured';
|
|
2147
|
+
final disabled = status?.status == 'disabled';
|
|
2148
|
+
final canDisconnect = configured && !connected && !disabled;
|
|
2149
|
+
final isDisconnecting = controller.isMessagingPlatformBusy(
|
|
2150
|
+
platform.id,
|
|
2151
|
+
'disconnect',
|
|
2152
|
+
);
|
|
1703
2153
|
final accent = platform.accent;
|
|
1704
2154
|
final actionLabel = connected
|
|
1705
2155
|
? 'Connected'
|
|
2156
|
+
: disabled
|
|
2157
|
+
? 'Disabled'
|
|
1706
2158
|
: configured
|
|
1707
2159
|
? 'Reconnect'
|
|
1708
2160
|
: 'Connect';
|
|
@@ -1767,11 +2219,15 @@ class _MessagingCard extends StatelessWidget {
|
|
|
1767
2219
|
_StatusPill(
|
|
1768
2220
|
label: connected
|
|
1769
2221
|
? 'Live'
|
|
2222
|
+
: disabled
|
|
2223
|
+
? 'Disabled'
|
|
1770
2224
|
: configured
|
|
1771
2225
|
? 'Ready'
|
|
1772
2226
|
: 'Setup',
|
|
1773
2227
|
color: connected
|
|
1774
2228
|
? _success
|
|
2229
|
+
: disabled
|
|
2230
|
+
? _textMuted
|
|
1775
2231
|
: configured
|
|
1776
2232
|
? _warning
|
|
1777
2233
|
: _textMuted,
|
|
@@ -1812,23 +2268,48 @@ class _MessagingCard extends StatelessWidget {
|
|
|
1812
2268
|
Expanded(
|
|
1813
2269
|
child: connected
|
|
1814
2270
|
? OutlinedButton.icon(
|
|
1815
|
-
onPressed: onDisconnect,
|
|
1816
|
-
icon:
|
|
2271
|
+
onPressed: isDisconnecting ? null : onDisconnect,
|
|
2272
|
+
icon: isDisconnecting
|
|
2273
|
+
? SizedBox.square(
|
|
2274
|
+
dimension: 18,
|
|
2275
|
+
child: CircularProgressIndicator(
|
|
2276
|
+
strokeWidth: 2,
|
|
2277
|
+
),
|
|
2278
|
+
)
|
|
2279
|
+
: Icon(Icons.link_off_rounded, size: 18),
|
|
1817
2280
|
label: Text(
|
|
1818
2281
|
'Disconnect',
|
|
1819
2282
|
overflow: TextOverflow.ellipsis,
|
|
1820
2283
|
),
|
|
1821
2284
|
)
|
|
1822
2285
|
: FilledButton.icon(
|
|
1823
|
-
onPressed:
|
|
2286
|
+
onPressed: disabled || isDisconnecting
|
|
2287
|
+
? null
|
|
2288
|
+
: onConnect,
|
|
1824
2289
|
icon: Icon(Icons.power_settings_new_rounded, size: 18),
|
|
1825
2290
|
label: Text(
|
|
1826
2291
|
actionLabel,
|
|
1827
2292
|
overflow: TextOverflow.ellipsis,
|
|
1828
2293
|
),
|
|
1829
|
-
style: FilledButton.styleFrom(
|
|
2294
|
+
style: FilledButton.styleFrom(
|
|
2295
|
+
backgroundColor: accent,
|
|
2296
|
+
foregroundColor: Colors.white,
|
|
2297
|
+
),
|
|
1830
2298
|
),
|
|
1831
2299
|
),
|
|
2300
|
+
if (canDisconnect) ...[
|
|
2301
|
+
const SizedBox(width: 8),
|
|
2302
|
+
IconButton.outlined(
|
|
2303
|
+
tooltip: 'Disconnect platform',
|
|
2304
|
+
onPressed: isDisconnecting ? null : onDisconnect,
|
|
2305
|
+
icon: isDisconnecting
|
|
2306
|
+
? SizedBox.square(
|
|
2307
|
+
dimension: 18,
|
|
2308
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
2309
|
+
)
|
|
2310
|
+
: Icon(Icons.link_off_rounded),
|
|
2311
|
+
),
|
|
2312
|
+
],
|
|
1832
2313
|
const SizedBox(width: 8),
|
|
1833
2314
|
IconButton.outlined(
|
|
1834
2315
|
tooltip: 'Access policy',
|
|
@@ -3173,7 +3654,7 @@ class _RunResponseCard extends StatelessWidget {
|
|
|
3173
3654
|
height: 1.6,
|
|
3174
3655
|
),
|
|
3175
3656
|
code: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
3176
|
-
fontFamily: GoogleFonts.
|
|
3657
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
3177
3658
|
backgroundColor: _bgSecondary,
|
|
3178
3659
|
color: _textPrimary,
|
|
3179
3660
|
),
|
|
@@ -3256,8 +3737,7 @@ class _DeliverableSummaryCard extends StatelessWidget {
|
|
|
3256
3737
|
style: TextStyle(
|
|
3257
3738
|
color: _textSecondary,
|
|
3258
3739
|
fontSize: 12,
|
|
3259
|
-
fontFamily:
|
|
3260
|
-
GoogleFonts.jetBrainsMono().fontFamily,
|
|
3740
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
3261
3741
|
),
|
|
3262
3742
|
),
|
|
3263
3743
|
],
|
|
@@ -3568,7 +4048,7 @@ class _RunDetailBlock extends StatelessWidget {
|
|
|
3568
4048
|
fontSize: 12.5,
|
|
3569
4049
|
color: _textPrimary,
|
|
3570
4050
|
fontFamily: monospace
|
|
3571
|
-
? GoogleFonts.
|
|
4051
|
+
? GoogleFonts.geistMono().fontFamily
|
|
3572
4052
|
: null,
|
|
3573
4053
|
),
|
|
3574
4054
|
),
|