neoagent 2.4.1-beta.13 → 2.4.1-beta.15
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/extensions/chrome-browser/background.mjs +26 -0
- package/extensions/chrome-browser/manifest.json +3 -3
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/lib/main_chat.dart +276 -83
- package/flutter_app/lib/main_controller.dart +12 -0
- package/flutter_app/lib/main_devices.dart +39 -0
- package/flutter_app/lib/main_integrations.dart +3 -1
- package/flutter_app/lib/main_unified.dart +29 -1
- package/flutter_app/lib/src/backend_client.dart +11 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +23 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +56334 -56015
- package/server/routes/voice_assistant.js +36 -1
- package/server/services/browser/extension/registry.js +46 -5
- package/server/services/desktop/registry.js +104 -1
- package/server/services/streaming/android-stream.js +39 -1
- package/server/utils/version.js +29 -19
- package/server/utils/version.test.js +39 -0
|
@@ -8,6 +8,8 @@ let reconnectTimer = null;
|
|
|
8
8
|
let suppressSocketClose = false;
|
|
9
9
|
const DEFAULT_FETCH_TIMEOUT_MS = 10000;
|
|
10
10
|
const DEFAULT_WS_CONNECT_TIMEOUT_MS = 10000;
|
|
11
|
+
const KEEPALIVE_ALARM_NAME = 'neoagent-extension-keepalive';
|
|
12
|
+
const KEEPALIVE_ALARM_MINUTES = 1;
|
|
11
13
|
const EXTENSION_PROTOCOL_VERSION = 1;
|
|
12
14
|
|
|
13
15
|
function getStorage(keys = STORAGE_KEYS) {
|
|
@@ -81,6 +83,12 @@ function clearReconnectTimer() {
|
|
|
81
83
|
reconnectTimer = null;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
function ensureKeepaliveAlarm() {
|
|
87
|
+
chrome.alarms?.create?.(KEEPALIVE_ALARM_NAME, {
|
|
88
|
+
periodInMinutes: KEEPALIVE_ALARM_MINUTES,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
async function handleSocketDisconnected(ws) {
|
|
85
93
|
if (socket !== ws) {
|
|
86
94
|
return;
|
|
@@ -308,4 +316,22 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
|
308
316
|
return true;
|
|
309
317
|
});
|
|
310
318
|
|
|
319
|
+
chrome.alarms?.onAlarm?.addListener((alarm) => {
|
|
320
|
+
if (alarm?.name !== KEEPALIVE_ALARM_NAME) return;
|
|
321
|
+
connect().catch((error) => {
|
|
322
|
+
console.error('NeoAgent keepalive reconnect failed', error);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
chrome.runtime.onStartup?.addListener(() => {
|
|
327
|
+
ensureKeepaliveAlarm();
|
|
328
|
+
connect().catch(() => {});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
chrome.runtime.onInstalled?.addListener(() => {
|
|
332
|
+
ensureKeepaliveAlarm();
|
|
333
|
+
connect().catch(() => {});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
ensureKeepaliveAlarm();
|
|
311
337
|
connect().catch(() => {});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "NeoAgent Browser",
|
|
4
|
-
"version": "2.4.1.
|
|
5
|
-
"version_name": "2.4.1-beta.
|
|
4
|
+
"version": "2.4.1.8",
|
|
5
|
+
"version_name": "2.4.1-beta.8",
|
|
6
6
|
"description": "Connect this Chrome browser to NeoAgent for browser automation.",
|
|
7
7
|
"minimum_chrome_version": "118",
|
|
8
|
-
"permissions": ["debugger", "storage", "tabs"],
|
|
8
|
+
"permissions": ["alarms", "debugger", "storage", "tabs"],
|
|
9
9
|
"host_permissions": ["http://*/*", "https://*/*"],
|
|
10
10
|
"icons": {
|
|
11
11
|
"16": "icons/icon16.png",
|
|
@@ -124,7 +124,8 @@
|
|
|
124
124
|
|
|
125
125
|
<service
|
|
126
126
|
android:name=".auto.NeoAgentCarAppService"
|
|
127
|
-
android:exported="true"
|
|
127
|
+
android:exported="true"
|
|
128
|
+
android:permission="android.car.permission.BIND_CAR_APP_SERVICE">
|
|
128
129
|
<intent-filter>
|
|
129
130
|
<action android:name="androidx.car.app.CarAppService" />
|
|
130
131
|
<category android:name="androidx.car.app.category.IOT" />
|
|
@@ -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,108 +255,178 @@ 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
|
+
_stickToBottom = nearBottom;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
void _handleComposerLayoutChanged() {
|
|
280
|
+
if (_stickToBottom) {
|
|
281
|
+
_scheduleScrollToBottom();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
String _scrollContentSignature(
|
|
286
|
+
List<ChatEntry> messages,
|
|
287
|
+
NeoAgentController controller,
|
|
288
|
+
) {
|
|
289
|
+
final last = messages.isEmpty ? null : messages.last;
|
|
290
|
+
final activeRun = controller.activeRun;
|
|
291
|
+
return <Object?>[
|
|
292
|
+
messages.length,
|
|
293
|
+
last?.id,
|
|
294
|
+
last?.role,
|
|
295
|
+
last?.content.length,
|
|
296
|
+
last?.typing,
|
|
297
|
+
controller.toolEvents.length,
|
|
298
|
+
activeRun?.runId,
|
|
299
|
+
activeRun?.phase,
|
|
300
|
+
activeRun?.iteration,
|
|
301
|
+
controller.streamingAssistant.length,
|
|
302
|
+
controller.isSendingMessage,
|
|
303
|
+
].join('|');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
void _scheduleScrollToBottom({bool force = false}) {
|
|
307
|
+
if (!force && !_stickToBottom) return;
|
|
308
|
+
final generation = ++_scrollGeneration;
|
|
309
|
+
|
|
310
|
+
void settle(int remainingPasses) {
|
|
311
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
312
|
+
if (!mounted ||
|
|
313
|
+
generation != _scrollGeneration ||
|
|
314
|
+
!_scrollController.hasClients) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
final pos = _scrollController.position;
|
|
318
|
+
if (pos.hasContentDimensions) {
|
|
319
|
+
final target = pos.maxScrollExtent;
|
|
320
|
+
if ((pos.pixels - target).abs() > 0.5) {
|
|
321
|
+
_ignoreScrollUpdates = true;
|
|
322
|
+
_scrollController.jumpTo(target);
|
|
323
|
+
_ignoreScrollUpdates = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
_stickToBottom = true;
|
|
327
|
+
if (remainingPasses > 0) {
|
|
328
|
+
Timer(const Duration(milliseconds: 24), () {
|
|
329
|
+
if (mounted && generation == _scrollGeneration) {
|
|
330
|
+
settle(remainingPasses - 1);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
settle(_autoScrollSettlePasses);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
void _maybeFollowChatContent(
|
|
341
|
+
List<ChatEntry> messages,
|
|
342
|
+
NeoAgentController controller,
|
|
343
|
+
) {
|
|
344
|
+
final signature = _scrollContentSignature(messages, controller);
|
|
345
|
+
if (_lastScrollContentSignature == signature) return;
|
|
346
|
+
final isInitialContent = _lastScrollContentSignature.isEmpty;
|
|
347
|
+
final shouldFollow = _stickToBottom || isInitialContent;
|
|
348
|
+
_lastScrollContentSignature = signature;
|
|
349
|
+
if (shouldFollow) {
|
|
350
|
+
_scheduleScrollToBottom(force: isInitialContent);
|
|
351
|
+
}
|
|
181
352
|
}
|
|
182
353
|
|
|
183
354
|
@override
|
|
184
355
|
Widget build(BuildContext context) {
|
|
185
356
|
final controller = widget.controller;
|
|
186
357
|
final messages = controller.visibleChatMessages;
|
|
187
|
-
|
|
188
|
-
_lastToolCount != controller.toolEvents.length ||
|
|
189
|
-
_lastStream != controller.streamingAssistant) {
|
|
190
|
-
_lastMessageCount = messages.length;
|
|
191
|
-
_lastToolCount = controller.toolEvents.length;
|
|
192
|
-
_lastStream = controller.streamingAssistant;
|
|
193
|
-
if (_isNearBottom) _scrollToBottom();
|
|
194
|
-
}
|
|
358
|
+
_maybeFollowChatContent(messages, controller);
|
|
195
359
|
|
|
196
360
|
return Column(
|
|
197
361
|
children: <Widget>[
|
|
198
362
|
Expanded(
|
|
199
363
|
child: SelectionArea(
|
|
200
364
|
child: ListView(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
365
|
+
controller: _scrollController,
|
|
366
|
+
padding: _pagePadding(context),
|
|
367
|
+
children: <Widget>[
|
|
368
|
+
_PageTitle(
|
|
369
|
+
title: 'Chat',
|
|
370
|
+
subtitle: 'Live agent chat with tool and stream status.',
|
|
371
|
+
trailing: Wrap(
|
|
372
|
+
spacing: 10,
|
|
373
|
+
runSpacing: 10,
|
|
374
|
+
crossAxisAlignment: WrapCrossAlignment.center,
|
|
375
|
+
children: <Widget>[
|
|
376
|
+
FilledButton.icon(
|
|
377
|
+
onPressed: () => controller.setSelectedSection(
|
|
378
|
+
AppSection.voiceAssistant,
|
|
379
|
+
),
|
|
380
|
+
icon: Icon(Icons.call),
|
|
381
|
+
label: Text('Call'),
|
|
215
382
|
),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
icon: Icons.smart_toy_outlined,
|
|
226
|
-
),
|
|
227
|
-
],
|
|
228
|
-
),
|
|
229
|
-
),
|
|
230
|
-
if (controller.errorMessage != null) ...<Widget>[
|
|
231
|
-
_InlineError(message: controller.errorMessage!),
|
|
232
|
-
const SizedBox(height: 16),
|
|
233
|
-
],
|
|
234
|
-
if (controller.activeRun != null ||
|
|
235
|
-
controller.toolEvents.isNotEmpty)
|
|
236
|
-
Padding(
|
|
237
|
-
padding: const EdgeInsets.only(bottom: 16),
|
|
238
|
-
child: _RunStatusPanel(
|
|
239
|
-
run: controller.activeRun,
|
|
240
|
-
tools: controller.toolEvents,
|
|
383
|
+
_MetaPill(
|
|
384
|
+
label: controller.modelIndicator,
|
|
385
|
+
icon: Icons.memory_outlined,
|
|
386
|
+
),
|
|
387
|
+
_MetaPill(
|
|
388
|
+
label: 'Agent: ${controller.activeAgentLabel}',
|
|
389
|
+
icon: Icons.smart_toy_outlined,
|
|
390
|
+
),
|
|
391
|
+
],
|
|
241
392
|
),
|
|
242
393
|
),
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
394
|
+
if (controller.errorMessage != null) ...<Widget>[
|
|
395
|
+
_InlineError(message: controller.errorMessage!),
|
|
396
|
+
const SizedBox(height: 16),
|
|
397
|
+
],
|
|
398
|
+
if (controller.activeRun != null ||
|
|
399
|
+
controller.toolEvents.isNotEmpty)
|
|
400
|
+
Padding(
|
|
401
|
+
padding: const EdgeInsets.only(bottom: 16),
|
|
402
|
+
child: _RunStatusPanel(
|
|
403
|
+
run: controller.activeRun,
|
|
404
|
+
tools: controller.toolEvents,
|
|
251
405
|
),
|
|
252
406
|
),
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
407
|
+
if (messages.isEmpty)
|
|
408
|
+
Padding(
|
|
409
|
+
padding: EdgeInsets.only(top: 64),
|
|
410
|
+
child: Center(
|
|
411
|
+
child: _EmptyState(
|
|
412
|
+
title: 'How can I help?',
|
|
413
|
+
subtitle:
|
|
414
|
+
'Runs, tools, memory, scheduling, skills, and MCP are all available here.',
|
|
415
|
+
),
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
else
|
|
419
|
+
...messages.map(
|
|
420
|
+
(entry) => Padding(
|
|
421
|
+
padding: const EdgeInsets.only(bottom: 18),
|
|
422
|
+
child: _ChatBubble(
|
|
423
|
+
entry: entry,
|
|
424
|
+
onLoadRunDetail: controller.fetchRunDetail,
|
|
425
|
+
),
|
|
261
426
|
),
|
|
262
427
|
),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
),
|
|
428
|
+
],
|
|
429
|
+
),
|
|
266
430
|
),
|
|
267
431
|
),
|
|
268
432
|
Container(
|
|
@@ -287,6 +451,9 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
287
451
|
.map((entry) => entry.value)
|
|
288
452
|
.toList(growable: false);
|
|
289
453
|
});
|
|
454
|
+
if (_stickToBottom) {
|
|
455
|
+
_scheduleScrollToBottom();
|
|
456
|
+
}
|
|
290
457
|
if (_pendingSharedAttachments.isEmpty) {
|
|
291
458
|
_clearSharedPayload();
|
|
292
459
|
}
|
|
@@ -331,6 +498,32 @@ class _ChatPanelState extends State<ChatPanel> {
|
|
|
331
498
|
color: _textSecondary,
|
|
332
499
|
),
|
|
333
500
|
const SizedBox(width: 2),
|
|
501
|
+
_isTranscribing
|
|
502
|
+
? const SizedBox(
|
|
503
|
+
width: 40,
|
|
504
|
+
height: 40,
|
|
505
|
+
child: Padding(
|
|
506
|
+
padding: EdgeInsets.all(10),
|
|
507
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
508
|
+
),
|
|
509
|
+
)
|
|
510
|
+
: IconButton(
|
|
511
|
+
tooltip: _isDictating
|
|
512
|
+
? 'Stop & transcribe'
|
|
513
|
+
: 'Dictate',
|
|
514
|
+
onPressed: _isDictating
|
|
515
|
+
? _stopAndTranscribe
|
|
516
|
+
: _startDictation,
|
|
517
|
+
icon: Icon(
|
|
518
|
+
_isDictating
|
|
519
|
+
? Icons.stop_circle_outlined
|
|
520
|
+
: Icons.mic_none_rounded,
|
|
521
|
+
),
|
|
522
|
+
color: _isDictating
|
|
523
|
+
? Theme.of(context).colorScheme.error
|
|
524
|
+
: _textSecondary,
|
|
525
|
+
),
|
|
526
|
+
const SizedBox(width: 2),
|
|
334
527
|
FilledButton(
|
|
335
528
|
onPressed: () => controller.setSelectedSection(
|
|
336
529
|
AppSection.voiceAssistant,
|
|
@@ -4602,6 +4602,18 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4602
4602
|
}
|
|
4603
4603
|
}
|
|
4604
4604
|
|
|
4605
|
+
Future<String> transcribeDictationAudio({
|
|
4606
|
+
required String audioBase64,
|
|
4607
|
+
String mimeType = 'audio/pcm;rate=16000;channels=1',
|
|
4608
|
+
}) async {
|
|
4609
|
+
final result = await _backendClient.transcribeAudio(
|
|
4610
|
+
backendUrl,
|
|
4611
|
+
audioBase64: audioBase64,
|
|
4612
|
+
mimeType: mimeType,
|
|
4613
|
+
);
|
|
4614
|
+
return result['transcript']?.toString() ?? '';
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4605
4617
|
Future<void> sendMessage(
|
|
4606
4618
|
String task, {
|
|
4607
4619
|
List<SharedChatAttachment> sharedAttachments =
|
|
@@ -1537,6 +1537,40 @@ class _InteractiveSurfacePreviewState
|
|
|
1537
1537
|
} catch (_) {}
|
|
1538
1538
|
}
|
|
1539
1539
|
|
|
1540
|
+
void _handleStreamFirstFrame(String streamKey) {
|
|
1541
|
+
if (!mounted || _requestedStreamKey != streamKey) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (_streamFailedKey == streamKey) {
|
|
1545
|
+
setState(() => _streamFailedKey = null);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
void _handleStreamFrameTimeout(String streamKey) {
|
|
1550
|
+
if (!mounted || _requestedStreamKey != streamKey) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
setState(() => _streamFailedKey = streamKey);
|
|
1554
|
+
unawaited(_stopActiveStream());
|
|
1555
|
+
if ((widget.screenshotPath ?? '').isEmpty) {
|
|
1556
|
+
unawaited(_refreshStaticFrame());
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
Future<void> _refreshStaticFrame() async {
|
|
1561
|
+
switch (widget.surface) {
|
|
1562
|
+
case _DeviceSurface.browser:
|
|
1563
|
+
await widget.controller.screenshotBrowserRuntime();
|
|
1564
|
+
break;
|
|
1565
|
+
case _DeviceSurface.android:
|
|
1566
|
+
await widget.controller.screenshotAndroidRuntime();
|
|
1567
|
+
break;
|
|
1568
|
+
case _DeviceSurface.desktop:
|
|
1569
|
+
await widget.controller.screenshotDesktopRuntime();
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1540
1574
|
void _detachImageListener() {
|
|
1541
1575
|
if (_imageStream != null && _imageListener != null) {
|
|
1542
1576
|
_imageStream!.removeListener(_imageListener!);
|
|
@@ -1681,6 +1715,7 @@ class _InteractiveSurfacePreviewState
|
|
|
1681
1715
|
streamDeviceId != null &&
|
|
1682
1716
|
streamDeviceId.isNotEmpty &&
|
|
1683
1717
|
streamKey != _streamFailedKey) {
|
|
1718
|
+
final activeStreamKey = streamKey!;
|
|
1684
1719
|
return Stack(
|
|
1685
1720
|
fit: StackFit.expand,
|
|
1686
1721
|
children: <Widget>[
|
|
@@ -1690,6 +1725,10 @@ class _InteractiveSurfacePreviewState
|
|
|
1690
1725
|
deviceId: streamDeviceId,
|
|
1691
1726
|
platform: streamPlatform,
|
|
1692
1727
|
remoteResolution: _pixelSize,
|
|
1728
|
+
onFirstFrame: () =>
|
|
1729
|
+
_handleStreamFirstFrame(activeStreamKey),
|
|
1730
|
+
onFrameTimeout: () =>
|
|
1731
|
+
_handleStreamFrameTimeout(activeStreamKey),
|
|
1693
1732
|
onTap: widget.busy
|
|
1694
1733
|
? null
|
|
1695
1734
|
: (x, y) => unawaited(
|
|
@@ -188,7 +188,9 @@ class OfficialIntegrationsTab extends StatelessWidget {
|
|
|
188
188
|
!item.env.configured
|
|
189
189
|
? item.env.summary
|
|
190
190
|
: item.hasExpiredAccounts
|
|
191
|
-
?
|
|
191
|
+
? item.id == 'google_workspace'
|
|
192
|
+
? 'One or more accounts expired. Reconnect to restore access. If this keeps happening, your Google Cloud OAuth app may be in Testing mode — publish it to Production in Google Cloud Console to get long-lived tokens.'
|
|
193
|
+
: 'One or more accounts expired. Reconnect the affected account to restore tool access.'
|
|
192
194
|
: !item.supportsMultipleAccounts && item.isConnected
|
|
193
195
|
? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
|
|
194
196
|
: item.isConnected
|