@juspay/neurolink 12.57.0 → 12.58.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/CHANGELOG.md +3 -3
- package/README.md +75 -51
- package/dist/browser/neurolink.min.js +419 -416
- package/dist/cli/commands/decide.d.ts +4 -1
- package/dist/cli/commands/decide.js +6 -3
- package/dist/cli/commands/setup.d.ts +1 -1
- package/dist/cli/commands/setup.js +1 -1
- package/dist/cli/factories/commandFactory.js +1 -1
- package/dist/constants/enums.d.ts +4 -0
- package/dist/constants/enums.js +5 -0
- package/dist/core/factory.d.ts +7 -0
- package/dist/core/factory.js +20 -0
- package/dist/factories/providerDescriptors.js +96 -7
- package/dist/processors/media/VideoProcessor.d.ts +2 -2
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +8 -1
- package/dist/providers/catalog/openai-decisions.json +75 -0
- package/dist/providers/catalog/provider-catalog.schema.json +56 -3
- package/dist/providers/catalog/schema.js +92 -11
- package/dist/providers/configuredDecision.js +9 -14
- package/dist/providers/decide/dialects/index.js +2 -0
- package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
- package/dist/providers/decide/dialects/systemOne.js +17 -0
- package/dist/providers/systemOneDecision.js +6 -1
- package/dist/telemetry/attributes.d.ts +10 -3
- package/dist/telemetry/attributes.js +142 -10
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/types/classifierRouter.d.ts +21 -9
- package/dist/types/cli.d.ts +4 -2
- package/dist/types/decision.d.ts +67 -3
- package/dist/types/processor.d.ts +10 -0
- package/dist/types/providerCatalog.d.ts +7 -11
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +12 -1
- package/dist/utils/pricing.js +9 -0
- package/docs-site/static/search-index.json +84 -59
- package/package.json +6 -2
- package/dist/client/reactHooks.tsx +0 -1473
|
@@ -1,1473 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React Hooks for NeuroLink Client SDK
|
|
3
|
-
*
|
|
4
|
-
* Provides React hooks for interacting with NeuroLink agents, chat, workflows,
|
|
5
|
-
* and voice features. Compatible with React 18+ and follows React hooks best practices.
|
|
6
|
-
*
|
|
7
|
-
* @remarks
|
|
8
|
-
* This module requires React 18+ as a peer dependency. Install it with:
|
|
9
|
-
* ```bash
|
|
10
|
-
* npm install react react-dom
|
|
11
|
-
* # or
|
|
12
|
-
* pnpm add react react-dom
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* @module @neurolink/react
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
useState,
|
|
20
|
-
useCallback,
|
|
21
|
-
useRef,
|
|
22
|
-
useEffect,
|
|
23
|
-
useContext,
|
|
24
|
-
createContext,
|
|
25
|
-
useMemo,
|
|
26
|
-
} from "react";
|
|
27
|
-
import type { ReactNode } from "react";
|
|
28
|
-
|
|
29
|
-
import type {
|
|
30
|
-
ClientApiError,
|
|
31
|
-
NeuroLinkProviderProps,
|
|
32
|
-
UseChatOptions,
|
|
33
|
-
UseChatReturn,
|
|
34
|
-
UseAgentOptions,
|
|
35
|
-
UseAgentReturn,
|
|
36
|
-
UseWorkflowOptions,
|
|
37
|
-
UseWorkflowReturn,
|
|
38
|
-
UseVoiceOptions,
|
|
39
|
-
UseVoiceReturn,
|
|
40
|
-
UseStreamOptions,
|
|
41
|
-
UseStreamReturn,
|
|
42
|
-
UseToolsOptions,
|
|
43
|
-
UseToolsReturn,
|
|
44
|
-
ClientChatMessage as ChatMessage,
|
|
45
|
-
ClientAgentExecuteOptions,
|
|
46
|
-
ClientAgentExecuteResult,
|
|
47
|
-
ClientWorkflowExecuteOptions,
|
|
48
|
-
ClientWorkflowExecuteResult,
|
|
49
|
-
ClientStreamCallbacks,
|
|
50
|
-
ClientStreamEvent as StreamEvent,
|
|
51
|
-
ClientToolInfo,
|
|
52
|
-
SpeechRecognitionInternal,
|
|
53
|
-
SpeechRecognitionEventInternal,
|
|
54
|
-
SpeechRecognitionErrorEventInternal,
|
|
55
|
-
UnknownRecord,
|
|
56
|
-
StreamToolCall,
|
|
57
|
-
} from "../types/index.js";
|
|
58
|
-
import { NeuroLinkClient, createClient } from "./httpClient.js";
|
|
59
|
-
// =============================================================================
|
|
60
|
-
// Context and Provider
|
|
61
|
-
// =============================================================================
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Context for NeuroLink client
|
|
65
|
-
*/
|
|
66
|
-
const NeuroLinkContext = createContext<NeuroLinkClient | null>(null);
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Provider component for NeuroLink client
|
|
70
|
-
*
|
|
71
|
-
* Wraps your application to provide the NeuroLink client to all hooks.
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```tsx
|
|
75
|
-
* import { NeuroLinkProvider } from '@neurolink/react';
|
|
76
|
-
*
|
|
77
|
-
* function App() {
|
|
78
|
-
* return (
|
|
79
|
-
* <NeuroLinkProvider
|
|
80
|
-
* config={{
|
|
81
|
-
* baseUrl: 'https://api.neurolink.example.com',
|
|
82
|
-
* apiKey: process.env.NEUROLINK_API_KEY,
|
|
83
|
-
* }}
|
|
84
|
-
* >
|
|
85
|
-
* <YourApp />
|
|
86
|
-
* </NeuroLinkProvider>
|
|
87
|
-
* );
|
|
88
|
-
* }
|
|
89
|
-
* ```
|
|
90
|
-
*/
|
|
91
|
-
export function NeuroLinkProvider({
|
|
92
|
-
config,
|
|
93
|
-
children,
|
|
94
|
-
}: NeuroLinkProviderProps & { children: ReactNode }): ReactNode {
|
|
95
|
-
const client = useMemo(() => createClient(config), [config]);
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<NeuroLinkContext.Provider value={client}>
|
|
99
|
-
{children}
|
|
100
|
-
</NeuroLinkContext.Provider>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Hook to access the NeuroLink client
|
|
106
|
-
*
|
|
107
|
-
* Must be used within a NeuroLinkProvider.
|
|
108
|
-
*
|
|
109
|
-
* @throws Error if used outside of NeuroLinkProvider
|
|
110
|
-
*/
|
|
111
|
-
export function useNeuroLinkClient(): NeuroLinkClient {
|
|
112
|
-
const client = useContext(NeuroLinkContext);
|
|
113
|
-
|
|
114
|
-
if (!client) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
"useNeuroLinkClient must be used within a NeuroLinkProvider. " +
|
|
117
|
-
"Wrap your component tree with <NeuroLinkProvider config={...}>.",
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return client;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// =============================================================================
|
|
125
|
-
// Utility Hooks
|
|
126
|
-
// =============================================================================
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Generate a unique message ID
|
|
130
|
-
*/
|
|
131
|
-
function generateMessageId(): string {
|
|
132
|
-
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// =============================================================================
|
|
136
|
-
// useChat Hook
|
|
137
|
-
// =============================================================================
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* React hook for chat interactions with NeuroLink agents
|
|
141
|
-
*
|
|
142
|
-
* Provides a chat interface with support for streaming responses,
|
|
143
|
-
* tool calls, and conversation history management.
|
|
144
|
-
*
|
|
145
|
-
* @example Basic usage
|
|
146
|
-
* ```tsx
|
|
147
|
-
* function ChatComponent() {
|
|
148
|
-
* const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
|
|
149
|
-
* api: '/api/chat',
|
|
150
|
-
* agentId: 'my-agent',
|
|
151
|
-
* });
|
|
152
|
-
*
|
|
153
|
-
* return (
|
|
154
|
-
* <div>
|
|
155
|
-
* {messages.map(m => (
|
|
156
|
-
* <div key={m.id} className={m.role}>
|
|
157
|
-
* {m.content}
|
|
158
|
-
* </div>
|
|
159
|
-
* ))}
|
|
160
|
-
* <form onSubmit={handleSubmit}>
|
|
161
|
-
* <input value={input} onChange={handleInputChange} />
|
|
162
|
-
* <button type="submit" disabled={isLoading}>Send</button>
|
|
163
|
-
* </form>
|
|
164
|
-
* </div>
|
|
165
|
-
* );
|
|
166
|
-
* }
|
|
167
|
-
* ```
|
|
168
|
-
*/
|
|
169
|
-
export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
|
170
|
-
const {
|
|
171
|
-
agentId,
|
|
172
|
-
initialMessages = [],
|
|
173
|
-
sessionId: initialSessionId,
|
|
174
|
-
systemPrompt,
|
|
175
|
-
onFinish,
|
|
176
|
-
onError,
|
|
177
|
-
onToolCall,
|
|
178
|
-
body,
|
|
179
|
-
generateId = generateMessageId,
|
|
180
|
-
} = options;
|
|
181
|
-
|
|
182
|
-
const client = useNeuroLinkClient();
|
|
183
|
-
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
|
|
184
|
-
const [input, setInput] = useState("");
|
|
185
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
186
|
-
const [error, setError] = useState<ClientApiError | null>(null);
|
|
187
|
-
const [toolCalls, setToolCalls] = useState<StreamToolCall[]>([]);
|
|
188
|
-
|
|
189
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
190
|
-
const sessionIdRef = useRef<string | undefined>(initialSessionId);
|
|
191
|
-
|
|
192
|
-
// Keep a ref to the latest messages so callbacks never see stale state
|
|
193
|
-
const messagesRef = useRef<ChatMessage[]>(initialMessages);
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
messagesRef.current = messages;
|
|
196
|
-
}, [messages]);
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Handle input change
|
|
200
|
-
*/
|
|
201
|
-
const handleInputChange = useCallback((e: { target: { value: string } }) => {
|
|
202
|
-
setInput(e.target.value);
|
|
203
|
-
}, []);
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Append a message and get response
|
|
207
|
-
*/
|
|
208
|
-
const append = useCallback(
|
|
209
|
-
async (
|
|
210
|
-
message: Omit<ChatMessage, "id" | "createdAt">,
|
|
211
|
-
): Promise<string | null | undefined> => {
|
|
212
|
-
const userMessage: ChatMessage = {
|
|
213
|
-
id: generateId(),
|
|
214
|
-
createdAt: new Date(),
|
|
215
|
-
...message,
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
setMessages((prev: ChatMessage[]) => [...prev, userMessage]);
|
|
219
|
-
setIsLoading(true);
|
|
220
|
-
setError(null);
|
|
221
|
-
setToolCalls([]);
|
|
222
|
-
|
|
223
|
-
// Create abort controller
|
|
224
|
-
abortControllerRef.current = new AbortController();
|
|
225
|
-
|
|
226
|
-
const assistantId = generateId();
|
|
227
|
-
const currentToolCalls: StreamToolCall[] = [];
|
|
228
|
-
let assistantContent = "";
|
|
229
|
-
|
|
230
|
-
// Add placeholder for assistant message
|
|
231
|
-
const assistantMessage: ChatMessage = {
|
|
232
|
-
id: assistantId,
|
|
233
|
-
role: "assistant",
|
|
234
|
-
content: "",
|
|
235
|
-
createdAt: new Date(),
|
|
236
|
-
};
|
|
237
|
-
setMessages((prev: ChatMessage[]) => [...prev, assistantMessage]);
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
// Read from the ref so we always have the latest messages
|
|
241
|
-
const currentMessages = messagesRef.current;
|
|
242
|
-
await client.stream(
|
|
243
|
-
{
|
|
244
|
-
input: {
|
|
245
|
-
text: userMessage.content,
|
|
246
|
-
},
|
|
247
|
-
...(agentId ? { context: { agentId } } : {}),
|
|
248
|
-
...(systemPrompt ? { systemPrompt } : {}),
|
|
249
|
-
...(body
|
|
250
|
-
? {
|
|
251
|
-
context: {
|
|
252
|
-
...(agentId ? { agentId } : {}),
|
|
253
|
-
messages: [...currentMessages, userMessage].map((m) => ({
|
|
254
|
-
role: m.role,
|
|
255
|
-
content: m.content,
|
|
256
|
-
})),
|
|
257
|
-
sessionId: sessionIdRef.current,
|
|
258
|
-
...body,
|
|
259
|
-
},
|
|
260
|
-
}
|
|
261
|
-
: {
|
|
262
|
-
context: {
|
|
263
|
-
...(agentId ? { agentId } : {}),
|
|
264
|
-
messages: [...currentMessages, userMessage].map((m) => ({
|
|
265
|
-
role: m.role,
|
|
266
|
-
content: m.content,
|
|
267
|
-
})),
|
|
268
|
-
sessionId: sessionIdRef.current,
|
|
269
|
-
},
|
|
270
|
-
}),
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
onText: (text) => {
|
|
274
|
-
assistantContent += text;
|
|
275
|
-
setMessages((prev: ChatMessage[]) =>
|
|
276
|
-
prev.map((m: ChatMessage) =>
|
|
277
|
-
m.id === assistantId
|
|
278
|
-
? { ...m, content: assistantContent }
|
|
279
|
-
: m,
|
|
280
|
-
),
|
|
281
|
-
);
|
|
282
|
-
},
|
|
283
|
-
onToolCall: (toolCall) => {
|
|
284
|
-
currentToolCalls.push(toolCall);
|
|
285
|
-
setToolCalls([...currentToolCalls]);
|
|
286
|
-
onToolCall?.(toolCall);
|
|
287
|
-
},
|
|
288
|
-
onToolResult: (toolResult) => {
|
|
289
|
-
setMessages((prev: ChatMessage[]) =>
|
|
290
|
-
prev.map((m: ChatMessage) =>
|
|
291
|
-
m.id === assistantId
|
|
292
|
-
? {
|
|
293
|
-
...m,
|
|
294
|
-
toolCalls: currentToolCalls,
|
|
295
|
-
toolResults: [...(m.toolResults ?? []), toolResult],
|
|
296
|
-
}
|
|
297
|
-
: m,
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
},
|
|
301
|
-
onMetadata: (metadata) => {
|
|
302
|
-
if (metadata?.sessionId) {
|
|
303
|
-
sessionIdRef.current = metadata.sessionId as string;
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
{ signal: abortControllerRef.current.signal },
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Final message update
|
|
311
|
-
const finalMessage: ChatMessage = {
|
|
312
|
-
id: assistantId,
|
|
313
|
-
role: "assistant",
|
|
314
|
-
content: assistantContent,
|
|
315
|
-
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
316
|
-
createdAt: new Date(),
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
setMessages((prev: ChatMessage[]) =>
|
|
320
|
-
prev.map((m: ChatMessage) =>
|
|
321
|
-
m.id === assistantId ? finalMessage : m,
|
|
322
|
-
),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
onFinish?.(finalMessage);
|
|
326
|
-
return assistantId;
|
|
327
|
-
} catch (err) {
|
|
328
|
-
if ((err as Error).name === "AbortError") {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
const apiError = err as ClientApiError;
|
|
332
|
-
setError(apiError);
|
|
333
|
-
onError?.(apiError);
|
|
334
|
-
return null;
|
|
335
|
-
} finally {
|
|
336
|
-
setIsLoading(false);
|
|
337
|
-
setToolCalls([]);
|
|
338
|
-
abortControllerRef.current = null;
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
[
|
|
342
|
-
client,
|
|
343
|
-
agentId,
|
|
344
|
-
systemPrompt,
|
|
345
|
-
body,
|
|
346
|
-
generateId,
|
|
347
|
-
onFinish,
|
|
348
|
-
onError,
|
|
349
|
-
onToolCall,
|
|
350
|
-
],
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Handle form submission
|
|
355
|
-
*/
|
|
356
|
-
const handleSubmit = useCallback(
|
|
357
|
-
(
|
|
358
|
-
e?: { preventDefault?: () => void },
|
|
359
|
-
submitOptions?: { data?: UnknownRecord },
|
|
360
|
-
) => {
|
|
361
|
-
e?.preventDefault?.();
|
|
362
|
-
|
|
363
|
-
if (!input.trim()) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const message: Omit<ChatMessage, "id" | "createdAt"> = {
|
|
368
|
-
role: "user",
|
|
369
|
-
content: input,
|
|
370
|
-
metadata: submitOptions?.data as ChatMessage["metadata"],
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
setInput("");
|
|
374
|
-
append(message);
|
|
375
|
-
},
|
|
376
|
-
[input, append],
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Reload the last assistant message
|
|
381
|
-
*/
|
|
382
|
-
const reload = useCallback(async (): Promise<string | null | undefined> => {
|
|
383
|
-
// Read from the ref so we always have the latest messages
|
|
384
|
-
const currentMessages = messagesRef.current;
|
|
385
|
-
const lastUserMessageIndex = currentMessages.findLastIndex(
|
|
386
|
-
(m: ChatMessage) => m.role === "user",
|
|
387
|
-
);
|
|
388
|
-
if (lastUserMessageIndex === -1) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const lastUserMessage = currentMessages[lastUserMessageIndex];
|
|
393
|
-
|
|
394
|
-
// Remove messages after the last user message
|
|
395
|
-
setMessages((prev: ChatMessage[]) => prev.slice(0, lastUserMessageIndex));
|
|
396
|
-
|
|
397
|
-
return append({
|
|
398
|
-
role: "user",
|
|
399
|
-
content: lastUserMessage.content,
|
|
400
|
-
});
|
|
401
|
-
}, [append]);
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Stop streaming
|
|
405
|
-
*/
|
|
406
|
-
const stop = useCallback(() => {
|
|
407
|
-
abortControllerRef.current?.abort();
|
|
408
|
-
}, []);
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Clear error
|
|
412
|
-
*/
|
|
413
|
-
const clearError = useCallback(() => {
|
|
414
|
-
setError(null);
|
|
415
|
-
}, []);
|
|
416
|
-
|
|
417
|
-
// Cleanup on unmount
|
|
418
|
-
useEffect(() => {
|
|
419
|
-
return () => {
|
|
420
|
-
abortControllerRef.current?.abort();
|
|
421
|
-
};
|
|
422
|
-
}, []);
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
messages,
|
|
426
|
-
input,
|
|
427
|
-
setInput,
|
|
428
|
-
handleInputChange,
|
|
429
|
-
handleSubmit,
|
|
430
|
-
append,
|
|
431
|
-
reload,
|
|
432
|
-
stop,
|
|
433
|
-
setMessages,
|
|
434
|
-
isLoading,
|
|
435
|
-
error,
|
|
436
|
-
clearError,
|
|
437
|
-
toolCalls,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// =============================================================================
|
|
442
|
-
// useAgent Hook
|
|
443
|
-
// =============================================================================
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* React hook for interacting with NeuroLink agents
|
|
447
|
-
*
|
|
448
|
-
* Provides methods for executing agents with both streaming
|
|
449
|
-
* and non-streaming responses, with session management.
|
|
450
|
-
*
|
|
451
|
-
* @example Basic usage
|
|
452
|
-
* ```tsx
|
|
453
|
-
* function AgentComponent() {
|
|
454
|
-
* const { execute, isLoading, result, error } = useAgent({
|
|
455
|
-
* agentId: 'my-agent',
|
|
456
|
-
* onResponse: (result) => console.log('Agent responded:', result),
|
|
457
|
-
* });
|
|
458
|
-
*
|
|
459
|
-
* return (
|
|
460
|
-
* <div>
|
|
461
|
-
* <button onClick={() => execute('Hello!')}>
|
|
462
|
-
* {isLoading ? 'Thinking...' : 'Ask Agent'}
|
|
463
|
-
* </button>
|
|
464
|
-
* {result && <p>{result.content}</p>}
|
|
465
|
-
* {error && <p className="error">{error.message}</p>}
|
|
466
|
-
* </div>
|
|
467
|
-
* );
|
|
468
|
-
* }
|
|
469
|
-
* ```
|
|
470
|
-
*/
|
|
471
|
-
export function useAgent(options: UseAgentOptions): UseAgentReturn {
|
|
472
|
-
const {
|
|
473
|
-
agentId,
|
|
474
|
-
sessionId: initialSessionId,
|
|
475
|
-
onResponse,
|
|
476
|
-
onError,
|
|
477
|
-
onToolCall,
|
|
478
|
-
initialInput,
|
|
479
|
-
} = options;
|
|
480
|
-
|
|
481
|
-
const client = useNeuroLinkClient();
|
|
482
|
-
const [sessionId, setSessionId] = useState<string | null>(
|
|
483
|
-
initialSessionId ?? null,
|
|
484
|
-
);
|
|
485
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
486
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
487
|
-
const [result, setResult] = useState<ClientAgentExecuteResult | null>(null);
|
|
488
|
-
const [error, setError] = useState<ClientApiError | null>(null);
|
|
489
|
-
|
|
490
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
491
|
-
const hasAutoExecuted = useRef(false);
|
|
492
|
-
|
|
493
|
-
// Keep a ref to the latest sessionId so callbacks never see stale state
|
|
494
|
-
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
|
495
|
-
useEffect(() => {
|
|
496
|
-
sessionIdRef.current = sessionId;
|
|
497
|
-
}, [sessionId]);
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Execute agent (non-streaming)
|
|
501
|
-
*/
|
|
502
|
-
const execute = useCallback(
|
|
503
|
-
async (
|
|
504
|
-
input: string,
|
|
505
|
-
executeOptions?: Partial<ClientAgentExecuteOptions>,
|
|
506
|
-
): Promise<ClientAgentExecuteResult> => {
|
|
507
|
-
setIsLoading(true);
|
|
508
|
-
setError(null);
|
|
509
|
-
|
|
510
|
-
abortControllerRef.current = new AbortController();
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
const response = await client.executeAgent(
|
|
514
|
-
{
|
|
515
|
-
agentId,
|
|
516
|
-
input,
|
|
517
|
-
sessionId: sessionIdRef.current ?? undefined,
|
|
518
|
-
...executeOptions,
|
|
519
|
-
},
|
|
520
|
-
{ signal: abortControllerRef.current.signal },
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
const agentResult = response.data;
|
|
524
|
-
setResult(agentResult);
|
|
525
|
-
setSessionId(agentResult.sessionId);
|
|
526
|
-
onResponse?.(agentResult);
|
|
527
|
-
|
|
528
|
-
return agentResult;
|
|
529
|
-
} catch (err) {
|
|
530
|
-
const apiError = err as ClientApiError;
|
|
531
|
-
setError(apiError);
|
|
532
|
-
onError?.(apiError);
|
|
533
|
-
throw err;
|
|
534
|
-
} finally {
|
|
535
|
-
setIsLoading(false);
|
|
536
|
-
abortControllerRef.current = null;
|
|
537
|
-
}
|
|
538
|
-
},
|
|
539
|
-
[client, agentId, onResponse, onError],
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Stream agent execution
|
|
544
|
-
*/
|
|
545
|
-
const stream = useCallback(
|
|
546
|
-
async (input: string, callbacks?: ClientStreamCallbacks): Promise<void> => {
|
|
547
|
-
setIsStreaming(true);
|
|
548
|
-
setIsLoading(true);
|
|
549
|
-
setError(null);
|
|
550
|
-
|
|
551
|
-
abortControllerRef.current = new AbortController();
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
await client.streamAgent(
|
|
555
|
-
{
|
|
556
|
-
agentId,
|
|
557
|
-
input,
|
|
558
|
-
sessionId: sessionIdRef.current ?? undefined,
|
|
559
|
-
stream: true,
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
...callbacks,
|
|
563
|
-
onToolCall: (toolCall) => {
|
|
564
|
-
callbacks?.onToolCall?.(toolCall);
|
|
565
|
-
onToolCall?.(toolCall);
|
|
566
|
-
},
|
|
567
|
-
onDone: (streamResult) => {
|
|
568
|
-
callbacks?.onDone?.(streamResult);
|
|
569
|
-
setIsStreaming(false);
|
|
570
|
-
},
|
|
571
|
-
onError: (apiError) => {
|
|
572
|
-
callbacks?.onError?.(apiError);
|
|
573
|
-
setError(apiError);
|
|
574
|
-
onError?.(apiError);
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
{ signal: abortControllerRef.current.signal },
|
|
578
|
-
);
|
|
579
|
-
} catch (err) {
|
|
580
|
-
const apiError = err as ClientApiError;
|
|
581
|
-
setError(apiError);
|
|
582
|
-
onError?.(apiError);
|
|
583
|
-
} finally {
|
|
584
|
-
setIsLoading(false);
|
|
585
|
-
setIsStreaming(false);
|
|
586
|
-
abortControllerRef.current = null;
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
[client, agentId, onToolCall, onError],
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Abort current execution
|
|
594
|
-
*/
|
|
595
|
-
const abort = useCallback(() => {
|
|
596
|
-
abortControllerRef.current?.abort();
|
|
597
|
-
setIsLoading(false);
|
|
598
|
-
setIsStreaming(false);
|
|
599
|
-
}, []);
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Clear error
|
|
603
|
-
*/
|
|
604
|
-
const clearError = useCallback(() => {
|
|
605
|
-
setError(null);
|
|
606
|
-
}, []);
|
|
607
|
-
|
|
608
|
-
// Auto-execute on mount if initialInput is provided
|
|
609
|
-
useEffect(() => {
|
|
610
|
-
if (initialInput && !hasAutoExecuted.current) {
|
|
611
|
-
hasAutoExecuted.current = true;
|
|
612
|
-
execute(initialInput);
|
|
613
|
-
}
|
|
614
|
-
}, [initialInput, execute]);
|
|
615
|
-
|
|
616
|
-
// Cleanup on unmount
|
|
617
|
-
useEffect(() => {
|
|
618
|
-
return () => {
|
|
619
|
-
abortControllerRef.current?.abort();
|
|
620
|
-
};
|
|
621
|
-
}, []);
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
execute,
|
|
625
|
-
stream,
|
|
626
|
-
sessionId,
|
|
627
|
-
setSessionId,
|
|
628
|
-
isLoading,
|
|
629
|
-
isStreaming,
|
|
630
|
-
result,
|
|
631
|
-
error,
|
|
632
|
-
clearError,
|
|
633
|
-
abort,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// =============================================================================
|
|
638
|
-
// useWorkflow Hook
|
|
639
|
-
// =============================================================================
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* React hook for executing NeuroLink workflows
|
|
643
|
-
*
|
|
644
|
-
* Provides methods for executing, resuming, and monitoring workflows
|
|
645
|
-
* with automatic status polling and suspension handling.
|
|
646
|
-
*
|
|
647
|
-
* @example Basic usage
|
|
648
|
-
* ```tsx
|
|
649
|
-
* function WorkflowComponent() {
|
|
650
|
-
* const { execute, status, result, isLoading, error } = useWorkflow({
|
|
651
|
-
* workflowId: 'data-processing-workflow',
|
|
652
|
-
* onComplete: (result) => console.log('Workflow completed:', result),
|
|
653
|
-
* onStepComplete: (step) => console.log('Step completed:', step.stepId),
|
|
654
|
-
* });
|
|
655
|
-
*
|
|
656
|
-
* return (
|
|
657
|
-
* <div>
|
|
658
|
-
* <button onClick={() => execute({ data: inputData })}>
|
|
659
|
-
* Run Workflow
|
|
660
|
-
* </button>
|
|
661
|
-
* {status && <p>Status: {status}</p>}
|
|
662
|
-
* {result?.output && <pre>{JSON.stringify(result.output, null, 2)}</pre>}
|
|
663
|
-
* </div>
|
|
664
|
-
* );
|
|
665
|
-
* }
|
|
666
|
-
* ```
|
|
667
|
-
*/
|
|
668
|
-
export function useWorkflow(options: UseWorkflowOptions): UseWorkflowReturn {
|
|
669
|
-
const {
|
|
670
|
-
workflowId,
|
|
671
|
-
onComplete,
|
|
672
|
-
onError,
|
|
673
|
-
onStepComplete,
|
|
674
|
-
pollInterval = 2000,
|
|
675
|
-
} = options;
|
|
676
|
-
|
|
677
|
-
const client = useNeuroLinkClient();
|
|
678
|
-
const [runId, setRunId] = useState<string | null>(null);
|
|
679
|
-
const [status, setStatus] = useState<
|
|
680
|
-
ClientWorkflowExecuteResult["status"] | null
|
|
681
|
-
>(null);
|
|
682
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
683
|
-
const [result, setResult] = useState<ClientWorkflowExecuteResult | null>(
|
|
684
|
-
null,
|
|
685
|
-
);
|
|
686
|
-
const [error, setError] = useState<ClientApiError | null>(null);
|
|
687
|
-
|
|
688
|
-
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
689
|
-
const previousStepsRef = useRef<Set<string>>(new Set());
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Stop polling
|
|
693
|
-
*/
|
|
694
|
-
const stopPolling = useCallback(() => {
|
|
695
|
-
if (pollIntervalRef.current) {
|
|
696
|
-
clearInterval(pollIntervalRef.current);
|
|
697
|
-
pollIntervalRef.current = null;
|
|
698
|
-
}
|
|
699
|
-
}, []);
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Poll for workflow status
|
|
703
|
-
*/
|
|
704
|
-
const pollStatus = useCallback(
|
|
705
|
-
async (currentRunId: string) => {
|
|
706
|
-
try {
|
|
707
|
-
const response = await client.getWorkflowStatus(
|
|
708
|
-
workflowId,
|
|
709
|
-
currentRunId,
|
|
710
|
-
);
|
|
711
|
-
const workflowResult = response.data;
|
|
712
|
-
|
|
713
|
-
setStatus(workflowResult.status);
|
|
714
|
-
setResult(workflowResult);
|
|
715
|
-
|
|
716
|
-
// Check for newly completed steps
|
|
717
|
-
if (workflowResult.steps && onStepComplete) {
|
|
718
|
-
for (const step of workflowResult.steps) {
|
|
719
|
-
if (
|
|
720
|
-
step.status === "completed" &&
|
|
721
|
-
!previousStepsRef.current.has(step.stepId)
|
|
722
|
-
) {
|
|
723
|
-
previousStepsRef.current.add(step.stepId);
|
|
724
|
-
onStepComplete(step);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Handle completion
|
|
730
|
-
if (workflowResult.status === "completed") {
|
|
731
|
-
stopPolling();
|
|
732
|
-
setIsLoading(false);
|
|
733
|
-
onComplete?.(workflowResult);
|
|
734
|
-
} else if (workflowResult.status === "failed") {
|
|
735
|
-
stopPolling();
|
|
736
|
-
setIsLoading(false);
|
|
737
|
-
if (workflowResult.error) {
|
|
738
|
-
setError(workflowResult.error);
|
|
739
|
-
onError?.(workflowResult.error);
|
|
740
|
-
}
|
|
741
|
-
} else if (workflowResult.status === "suspended") {
|
|
742
|
-
stopPolling();
|
|
743
|
-
setIsLoading(false);
|
|
744
|
-
}
|
|
745
|
-
} catch (err) {
|
|
746
|
-
stopPolling();
|
|
747
|
-
setIsLoading(false);
|
|
748
|
-
const apiError = err as ClientApiError;
|
|
749
|
-
setError(apiError);
|
|
750
|
-
onError?.(apiError);
|
|
751
|
-
}
|
|
752
|
-
},
|
|
753
|
-
[client, workflowId, onComplete, onError, onStepComplete, stopPolling],
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* Start polling for workflow status
|
|
758
|
-
*/
|
|
759
|
-
const startPolling = useCallback(
|
|
760
|
-
(currentRunId: string) => {
|
|
761
|
-
stopPolling();
|
|
762
|
-
pollIntervalRef.current = setInterval(
|
|
763
|
-
() => pollStatus(currentRunId),
|
|
764
|
-
pollInterval,
|
|
765
|
-
);
|
|
766
|
-
},
|
|
767
|
-
[pollInterval, pollStatus, stopPolling],
|
|
768
|
-
);
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Execute workflow
|
|
772
|
-
*/
|
|
773
|
-
const execute = useCallback(
|
|
774
|
-
async (
|
|
775
|
-
input: UnknownRecord,
|
|
776
|
-
executeOptions?: Partial<ClientWorkflowExecuteOptions>,
|
|
777
|
-
): Promise<ClientWorkflowExecuteResult> => {
|
|
778
|
-
setIsLoading(true);
|
|
779
|
-
setError(null);
|
|
780
|
-
setStatus(null);
|
|
781
|
-
setResult(null);
|
|
782
|
-
previousStepsRef.current.clear();
|
|
783
|
-
|
|
784
|
-
try {
|
|
785
|
-
const response = await client.executeWorkflow({
|
|
786
|
-
workflowId,
|
|
787
|
-
input,
|
|
788
|
-
...executeOptions,
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
const workflowResult = response.data;
|
|
792
|
-
setRunId(workflowResult.runId);
|
|
793
|
-
setStatus(workflowResult.status);
|
|
794
|
-
setResult(workflowResult);
|
|
795
|
-
|
|
796
|
-
// Start polling if workflow is running
|
|
797
|
-
if (workflowResult.status === "running") {
|
|
798
|
-
startPolling(workflowResult.runId);
|
|
799
|
-
} else if (workflowResult.status === "completed") {
|
|
800
|
-
setIsLoading(false);
|
|
801
|
-
onComplete?.(workflowResult);
|
|
802
|
-
} else if (workflowResult.status === "failed") {
|
|
803
|
-
setIsLoading(false);
|
|
804
|
-
if (workflowResult.error) {
|
|
805
|
-
setError(workflowResult.error);
|
|
806
|
-
onError?.(workflowResult.error);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
return workflowResult;
|
|
811
|
-
} catch (err) {
|
|
812
|
-
setIsLoading(false);
|
|
813
|
-
const apiError = err as ClientApiError;
|
|
814
|
-
setError(apiError);
|
|
815
|
-
onError?.(apiError);
|
|
816
|
-
throw err;
|
|
817
|
-
}
|
|
818
|
-
},
|
|
819
|
-
[client, workflowId, onComplete, onError, startPolling],
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Resume suspended workflow
|
|
824
|
-
*/
|
|
825
|
-
const resume = useCallback(
|
|
826
|
-
async (
|
|
827
|
-
resumeToken: string,
|
|
828
|
-
resumeData?: UnknownRecord,
|
|
829
|
-
): Promise<ClientWorkflowExecuteResult> => {
|
|
830
|
-
setIsLoading(true);
|
|
831
|
-
setError(null);
|
|
832
|
-
|
|
833
|
-
try {
|
|
834
|
-
const response = await client.resumeWorkflow(
|
|
835
|
-
workflowId,
|
|
836
|
-
resumeToken,
|
|
837
|
-
resumeData,
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
const workflowResult = response.data;
|
|
841
|
-
setRunId(workflowResult.runId);
|
|
842
|
-
setStatus(workflowResult.status);
|
|
843
|
-
setResult(workflowResult);
|
|
844
|
-
|
|
845
|
-
// Start polling if workflow is running
|
|
846
|
-
if (workflowResult.status === "running") {
|
|
847
|
-
startPolling(workflowResult.runId);
|
|
848
|
-
} else if (workflowResult.status === "completed") {
|
|
849
|
-
setIsLoading(false);
|
|
850
|
-
onComplete?.(workflowResult);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return workflowResult;
|
|
854
|
-
} catch (err) {
|
|
855
|
-
setIsLoading(false);
|
|
856
|
-
const apiError = err as ClientApiError;
|
|
857
|
-
setError(apiError);
|
|
858
|
-
onError?.(apiError);
|
|
859
|
-
throw err;
|
|
860
|
-
}
|
|
861
|
-
},
|
|
862
|
-
[client, workflowId, onComplete, onError, startPolling],
|
|
863
|
-
);
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Get workflow status
|
|
867
|
-
*/
|
|
868
|
-
const getStatus = useCallback(
|
|
869
|
-
async (statusRunId: string): Promise<ClientWorkflowExecuteResult> => {
|
|
870
|
-
const response = await client.getWorkflowStatus(workflowId, statusRunId);
|
|
871
|
-
return response.data;
|
|
872
|
-
},
|
|
873
|
-
[client, workflowId],
|
|
874
|
-
);
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Cancel workflow execution
|
|
878
|
-
*/
|
|
879
|
-
const cancel = useCallback(
|
|
880
|
-
async (cancelRunId: string): Promise<void> => {
|
|
881
|
-
stopPolling();
|
|
882
|
-
await client.cancelWorkflow(workflowId, cancelRunId);
|
|
883
|
-
setStatus("failed");
|
|
884
|
-
setIsLoading(false);
|
|
885
|
-
},
|
|
886
|
-
[client, workflowId, stopPolling],
|
|
887
|
-
);
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Clear error
|
|
891
|
-
*/
|
|
892
|
-
const clearError = useCallback(() => {
|
|
893
|
-
setError(null);
|
|
894
|
-
}, []);
|
|
895
|
-
|
|
896
|
-
// Cleanup on unmount
|
|
897
|
-
useEffect(() => {
|
|
898
|
-
return () => {
|
|
899
|
-
stopPolling();
|
|
900
|
-
};
|
|
901
|
-
}, [stopPolling]);
|
|
902
|
-
|
|
903
|
-
return {
|
|
904
|
-
execute,
|
|
905
|
-
resume,
|
|
906
|
-
getStatus,
|
|
907
|
-
cancel,
|
|
908
|
-
runId,
|
|
909
|
-
status,
|
|
910
|
-
isLoading,
|
|
911
|
-
result,
|
|
912
|
-
error,
|
|
913
|
-
clearError,
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// =============================================================================
|
|
918
|
-
// useVoice Hook
|
|
919
|
-
// =============================================================================
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* React hook for voice interactions with NeuroLink
|
|
923
|
-
*
|
|
924
|
-
* Provides voice input (speech recognition) and output (text-to-speech)
|
|
925
|
-
* capabilities with support for real-time conversation.
|
|
926
|
-
*
|
|
927
|
-
* @example Basic usage
|
|
928
|
-
* ```tsx
|
|
929
|
-
* function VoiceComponent() {
|
|
930
|
-
* const {
|
|
931
|
-
* startListening,
|
|
932
|
-
* stopListening,
|
|
933
|
-
* speak,
|
|
934
|
-
* isListening,
|
|
935
|
-
* isSpeaking,
|
|
936
|
-
* transcript,
|
|
937
|
-
* isSupported,
|
|
938
|
-
* } = useVoice({
|
|
939
|
-
* voice: 'en-US-Neural2-C',
|
|
940
|
-
* autoPlay: true,
|
|
941
|
-
* });
|
|
942
|
-
*
|
|
943
|
-
* if (!isSupported) {
|
|
944
|
-
* return <p>Voice not supported in this browser</p>;
|
|
945
|
-
* }
|
|
946
|
-
*
|
|
947
|
-
* return (
|
|
948
|
-
* <div>
|
|
949
|
-
* <button onClick={isListening ? stopListening : startListening}>
|
|
950
|
-
* {isListening ? 'Stop' : 'Start'} Listening
|
|
951
|
-
* </button>
|
|
952
|
-
* <p>Transcript: {transcript}</p>
|
|
953
|
-
* <button onClick={() => speak('Hello!')}>Speak</button>
|
|
954
|
-
* </div>
|
|
955
|
-
* );
|
|
956
|
-
* }
|
|
957
|
-
* ```
|
|
958
|
-
*/
|
|
959
|
-
export function useVoice(options: UseVoiceOptions = {}): UseVoiceReturn {
|
|
960
|
-
const {
|
|
961
|
-
voice,
|
|
962
|
-
language = "en-US",
|
|
963
|
-
autoPlay = true,
|
|
964
|
-
onSpeechStart,
|
|
965
|
-
onSpeechEnd,
|
|
966
|
-
onError,
|
|
967
|
-
api = "/api/tts",
|
|
968
|
-
enableSpeechRecognition = true,
|
|
969
|
-
} = options;
|
|
970
|
-
|
|
971
|
-
const [isListening, setIsListening] = useState(false);
|
|
972
|
-
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
973
|
-
const [isProcessing, setIsProcessing] = useState(false);
|
|
974
|
-
const [transcript, setTranscript] = useState("");
|
|
975
|
-
const [response, setResponse] = useState<string | null>(null);
|
|
976
|
-
const [error, setError] = useState<ClientApiError | null>(null);
|
|
977
|
-
|
|
978
|
-
const recognitionRef = useRef<SpeechRecognitionInternal | null>(null);
|
|
979
|
-
const synthesisRef = useRef<SpeechSynthesisUtterance | null>(null);
|
|
980
|
-
|
|
981
|
-
// Check browser support
|
|
982
|
-
const isSupported = useMemo(() => {
|
|
983
|
-
if (typeof window === "undefined") {
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
const hasSpeechRecognition =
|
|
987
|
-
"SpeechRecognition" in window || "webkitSpeechRecognition" in window;
|
|
988
|
-
const hasSpeechSynthesis = "speechSynthesis" in window;
|
|
989
|
-
return hasSpeechRecognition || hasSpeechSynthesis;
|
|
990
|
-
}, []);
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* Initialize speech recognition
|
|
994
|
-
*/
|
|
995
|
-
const initRecognition = useCallback(() => {
|
|
996
|
-
if (typeof window === "undefined") {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const speechWindow = window as typeof window & {
|
|
1001
|
-
SpeechRecognition?: { new (): SpeechRecognitionInternal };
|
|
1002
|
-
webkitSpeechRecognition?: { new (): SpeechRecognitionInternal };
|
|
1003
|
-
};
|
|
1004
|
-
const SpeechRecognitionCtor =
|
|
1005
|
-
speechWindow.SpeechRecognition || speechWindow.webkitSpeechRecognition;
|
|
1006
|
-
|
|
1007
|
-
if (!SpeechRecognitionCtor) {
|
|
1008
|
-
return null;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const recognition = new SpeechRecognitionCtor();
|
|
1012
|
-
recognition.continuous = true;
|
|
1013
|
-
recognition.interimResults = true;
|
|
1014
|
-
recognition.lang = language;
|
|
1015
|
-
|
|
1016
|
-
recognition.onresult = (event: SpeechRecognitionEventInternal) => {
|
|
1017
|
-
let finalTranscript = "";
|
|
1018
|
-
let interimTranscript = "";
|
|
1019
|
-
|
|
1020
|
-
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
1021
|
-
const result = event.results[i];
|
|
1022
|
-
if (result.isFinal) {
|
|
1023
|
-
finalTranscript += result[0].transcript;
|
|
1024
|
-
} else {
|
|
1025
|
-
interimTranscript += result[0].transcript;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
setTranscript(finalTranscript || interimTranscript);
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
recognition.onerror = (event: SpeechRecognitionErrorEventInternal) => {
|
|
1033
|
-
const apiError: ClientApiError = {
|
|
1034
|
-
code: "SPEECH_RECOGNITION_ERROR",
|
|
1035
|
-
message: event.error,
|
|
1036
|
-
status: 500,
|
|
1037
|
-
};
|
|
1038
|
-
setError(apiError);
|
|
1039
|
-
onError?.(apiError);
|
|
1040
|
-
setIsListening(false);
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
recognition.onend = () => {
|
|
1044
|
-
setIsListening(false);
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1047
|
-
return recognition;
|
|
1048
|
-
}, [language, onError]);
|
|
1049
|
-
|
|
1050
|
-
/**
|
|
1051
|
-
* Start listening for voice input
|
|
1052
|
-
*/
|
|
1053
|
-
const startListening = useCallback(() => {
|
|
1054
|
-
if (!enableSpeechRecognition) {
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (!recognitionRef.current) {
|
|
1059
|
-
recognitionRef.current = initRecognition();
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (recognitionRef.current) {
|
|
1063
|
-
setTranscript("");
|
|
1064
|
-
setError(null);
|
|
1065
|
-
recognitionRef.current.start();
|
|
1066
|
-
setIsListening(true);
|
|
1067
|
-
}
|
|
1068
|
-
}, [enableSpeechRecognition, initRecognition]);
|
|
1069
|
-
|
|
1070
|
-
/**
|
|
1071
|
-
* Stop listening
|
|
1072
|
-
*/
|
|
1073
|
-
const stopListening = useCallback(() => {
|
|
1074
|
-
if (recognitionRef.current) {
|
|
1075
|
-
recognitionRef.current.stop();
|
|
1076
|
-
setIsListening(false);
|
|
1077
|
-
}
|
|
1078
|
-
}, []);
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* Speak text using TTS
|
|
1082
|
-
*/
|
|
1083
|
-
const speak = useCallback(
|
|
1084
|
-
async (text: string): Promise<void> => {
|
|
1085
|
-
if (typeof window === "undefined") {
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
setIsSpeaking(true);
|
|
1090
|
-
onSpeechStart?.();
|
|
1091
|
-
|
|
1092
|
-
try {
|
|
1093
|
-
// Use Web Speech API for basic TTS
|
|
1094
|
-
if ("speechSynthesis" in window) {
|
|
1095
|
-
return new Promise((resolve, reject) => {
|
|
1096
|
-
const utterance = new SpeechSynthesisUtterance(text);
|
|
1097
|
-
utterance.lang = language;
|
|
1098
|
-
|
|
1099
|
-
if (voice) {
|
|
1100
|
-
const voices = window.speechSynthesis.getVoices();
|
|
1101
|
-
const selectedVoice = voices.find((v) => v.name.includes(voice));
|
|
1102
|
-
if (selectedVoice) {
|
|
1103
|
-
utterance.voice = selectedVoice;
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
utterance.onend = () => {
|
|
1108
|
-
setIsSpeaking(false);
|
|
1109
|
-
onSpeechEnd?.();
|
|
1110
|
-
resolve();
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
utterance.onerror = (event) => {
|
|
1114
|
-
setIsSpeaking(false);
|
|
1115
|
-
const apiError: ClientApiError = {
|
|
1116
|
-
code: "SPEECH_SYNTHESIS_ERROR",
|
|
1117
|
-
message: event.error,
|
|
1118
|
-
status: 500,
|
|
1119
|
-
};
|
|
1120
|
-
setError(apiError);
|
|
1121
|
-
onError?.(apiError);
|
|
1122
|
-
reject(event);
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
synthesisRef.current = utterance;
|
|
1126
|
-
window.speechSynthesis.speak(utterance);
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
} catch (err) {
|
|
1130
|
-
setIsSpeaking(false);
|
|
1131
|
-
const apiError: ClientApiError = {
|
|
1132
|
-
code: "TTS_ERROR",
|
|
1133
|
-
message: (err as Error).message,
|
|
1134
|
-
status: 500,
|
|
1135
|
-
};
|
|
1136
|
-
setError(apiError);
|
|
1137
|
-
onError?.(apiError);
|
|
1138
|
-
}
|
|
1139
|
-
},
|
|
1140
|
-
[voice, language, onSpeechStart, onSpeechEnd, onError],
|
|
1141
|
-
);
|
|
1142
|
-
|
|
1143
|
-
/**
|
|
1144
|
-
* Stop speaking
|
|
1145
|
-
*/
|
|
1146
|
-
const stopSpeaking = useCallback(() => {
|
|
1147
|
-
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
|
1148
|
-
window.speechSynthesis.cancel();
|
|
1149
|
-
setIsSpeaking(false);
|
|
1150
|
-
}
|
|
1151
|
-
}, []);
|
|
1152
|
-
|
|
1153
|
-
/**
|
|
1154
|
-
* Submit voice input and get response
|
|
1155
|
-
*/
|
|
1156
|
-
const submit = useCallback(
|
|
1157
|
-
async (text: string): Promise<string> => {
|
|
1158
|
-
setIsProcessing(true);
|
|
1159
|
-
setError(null);
|
|
1160
|
-
|
|
1161
|
-
try {
|
|
1162
|
-
const res = await fetch(api, {
|
|
1163
|
-
method: "POST",
|
|
1164
|
-
headers: {
|
|
1165
|
-
"Content-Type": "application/json",
|
|
1166
|
-
},
|
|
1167
|
-
body: JSON.stringify({ text, voice }),
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
if (!res.ok) {
|
|
1171
|
-
const errorData = await res.json();
|
|
1172
|
-
throw errorData;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
const data = await res.json();
|
|
1176
|
-
const responseText = data.response || data.content || text;
|
|
1177
|
-
setResponse(responseText);
|
|
1178
|
-
|
|
1179
|
-
if (autoPlay) {
|
|
1180
|
-
await speak(responseText);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
return responseText;
|
|
1184
|
-
} catch (err) {
|
|
1185
|
-
const apiError = err as ClientApiError;
|
|
1186
|
-
setError(apiError);
|
|
1187
|
-
onError?.(apiError);
|
|
1188
|
-
throw err;
|
|
1189
|
-
} finally {
|
|
1190
|
-
setIsProcessing(false);
|
|
1191
|
-
}
|
|
1192
|
-
},
|
|
1193
|
-
[api, voice, autoPlay, speak, onError],
|
|
1194
|
-
);
|
|
1195
|
-
|
|
1196
|
-
// Cleanup on unmount
|
|
1197
|
-
useEffect(() => {
|
|
1198
|
-
return () => {
|
|
1199
|
-
recognitionRef.current?.stop();
|
|
1200
|
-
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
|
1201
|
-
window.speechSynthesis.cancel();
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
1204
|
-
}, []);
|
|
1205
|
-
|
|
1206
|
-
return {
|
|
1207
|
-
startListening,
|
|
1208
|
-
stopListening,
|
|
1209
|
-
speak,
|
|
1210
|
-
stopSpeaking,
|
|
1211
|
-
submit,
|
|
1212
|
-
isListening,
|
|
1213
|
-
isSpeaking,
|
|
1214
|
-
isProcessing,
|
|
1215
|
-
transcript,
|
|
1216
|
-
response,
|
|
1217
|
-
error,
|
|
1218
|
-
isSupported,
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// =============================================================================
|
|
1223
|
-
// useStream Hook
|
|
1224
|
-
// =============================================================================
|
|
1225
|
-
|
|
1226
|
-
/**
|
|
1227
|
-
* React hook for streaming responses from NeuroLink
|
|
1228
|
-
*
|
|
1229
|
-
* @example
|
|
1230
|
-
* ```tsx
|
|
1231
|
-
* function StreamComponent() {
|
|
1232
|
-
* const { start, stop, text, isStreaming } = useStream({
|
|
1233
|
-
* api: '/api/stream',
|
|
1234
|
-
* });
|
|
1235
|
-
*
|
|
1236
|
-
* return (
|
|
1237
|
-
* <div>
|
|
1238
|
-
* <button onClick={() => start({ prompt: 'Tell me a story' })}>
|
|
1239
|
-
* Start
|
|
1240
|
-
* </button>
|
|
1241
|
-
* <button onClick={stop} disabled={!isStreaming}>Stop</button>
|
|
1242
|
-
* <p>{text}</p>
|
|
1243
|
-
* </div>
|
|
1244
|
-
* );
|
|
1245
|
-
* }
|
|
1246
|
-
* ```
|
|
1247
|
-
*/
|
|
1248
|
-
export function useStream(options: UseStreamOptions = {}): UseStreamReturn {
|
|
1249
|
-
const { api = "/api/stream", callbacks } = options;
|
|
1250
|
-
|
|
1251
|
-
const [text, setText] = useState("");
|
|
1252
|
-
const [events, setEvents] = useState<StreamEvent[]>([]);
|
|
1253
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
1254
|
-
const [error, setError] = useState<ClientApiError | null>(null);
|
|
1255
|
-
|
|
1256
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
1257
|
-
|
|
1258
|
-
/**
|
|
1259
|
-
* Start streaming
|
|
1260
|
-
*/
|
|
1261
|
-
const start = useCallback(
|
|
1262
|
-
async (streamOptions: { prompt: string } & UnknownRecord) => {
|
|
1263
|
-
setText("");
|
|
1264
|
-
setEvents([]);
|
|
1265
|
-
setError(null);
|
|
1266
|
-
setIsStreaming(true);
|
|
1267
|
-
|
|
1268
|
-
abortControllerRef.current = new AbortController();
|
|
1269
|
-
|
|
1270
|
-
try {
|
|
1271
|
-
const response = await fetch(api, {
|
|
1272
|
-
method: "POST",
|
|
1273
|
-
headers: {
|
|
1274
|
-
"Content-Type": "application/json",
|
|
1275
|
-
Accept: "text/event-stream",
|
|
1276
|
-
},
|
|
1277
|
-
body: JSON.stringify(streamOptions),
|
|
1278
|
-
signal: abortControllerRef.current.signal,
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
if (!response.ok) {
|
|
1282
|
-
const errorData = await response.json();
|
|
1283
|
-
throw errorData;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
const reader = response.body?.getReader();
|
|
1287
|
-
if (!reader) {
|
|
1288
|
-
throw new Error("Response body is not readable");
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
const decoder = new TextDecoder();
|
|
1292
|
-
let buffer = "";
|
|
1293
|
-
let fullText = "";
|
|
1294
|
-
|
|
1295
|
-
while (true) {
|
|
1296
|
-
const { done, value } = await reader.read();
|
|
1297
|
-
if (done) {
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1302
|
-
const lines = buffer.split("\n");
|
|
1303
|
-
buffer = lines.pop() ?? "";
|
|
1304
|
-
|
|
1305
|
-
for (const line of lines) {
|
|
1306
|
-
if (line.startsWith("data: ")) {
|
|
1307
|
-
const data = line.slice(6);
|
|
1308
|
-
if (data === "[DONE]") {
|
|
1309
|
-
break;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
try {
|
|
1313
|
-
const event = JSON.parse(data) as StreamEvent;
|
|
1314
|
-
setEvents((prev: StreamEvent[]) => [...prev, event]);
|
|
1315
|
-
|
|
1316
|
-
if (event.type === "text" && event.content) {
|
|
1317
|
-
fullText += event.content;
|
|
1318
|
-
setText(fullText);
|
|
1319
|
-
callbacks?.onText?.(event.content);
|
|
1320
|
-
} else if (event.type === "tool-call" && event.toolCall) {
|
|
1321
|
-
callbacks?.onToolCall?.(event.toolCall);
|
|
1322
|
-
} else if (event.type === "tool-result" && event.toolResult) {
|
|
1323
|
-
callbacks?.onToolResult?.(event.toolResult);
|
|
1324
|
-
} else if (event.type === "error" && event.error) {
|
|
1325
|
-
callbacks?.onError?.(event.error);
|
|
1326
|
-
} else if (event.type === "metadata" && event.metadata) {
|
|
1327
|
-
callbacks?.onMetadata?.(event.metadata);
|
|
1328
|
-
}
|
|
1329
|
-
} catch {
|
|
1330
|
-
// Ignore parse errors
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
reader.releaseLock();
|
|
1337
|
-
} catch (err) {
|
|
1338
|
-
if ((err as Error).name === "AbortError") {
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
const apiError = err as ClientApiError;
|
|
1342
|
-
setError(apiError);
|
|
1343
|
-
callbacks?.onError?.(apiError);
|
|
1344
|
-
} finally {
|
|
1345
|
-
setIsStreaming(false);
|
|
1346
|
-
abortControllerRef.current = null;
|
|
1347
|
-
}
|
|
1348
|
-
},
|
|
1349
|
-
[api, callbacks],
|
|
1350
|
-
);
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Stop streaming
|
|
1354
|
-
*/
|
|
1355
|
-
const stop = useCallback(() => {
|
|
1356
|
-
abortControllerRef.current?.abort();
|
|
1357
|
-
setIsStreaming(false);
|
|
1358
|
-
}, []);
|
|
1359
|
-
|
|
1360
|
-
// Cleanup on unmount
|
|
1361
|
-
useEffect(() => {
|
|
1362
|
-
return () => {
|
|
1363
|
-
abortControllerRef.current?.abort();
|
|
1364
|
-
};
|
|
1365
|
-
}, []);
|
|
1366
|
-
|
|
1367
|
-
return {
|
|
1368
|
-
start,
|
|
1369
|
-
stop,
|
|
1370
|
-
text,
|
|
1371
|
-
events,
|
|
1372
|
-
isStreaming,
|
|
1373
|
-
error,
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// =============================================================================
|
|
1378
|
-
// useTools Hook
|
|
1379
|
-
// =============================================================================
|
|
1380
|
-
|
|
1381
|
-
/**
|
|
1382
|
-
* React hook for accessing and executing NeuroLink tools
|
|
1383
|
-
*
|
|
1384
|
-
* @example
|
|
1385
|
-
* ```tsx
|
|
1386
|
-
* function ToolsComponent() {
|
|
1387
|
-
* const { tools, execute, isLoading, error } = useTools({
|
|
1388
|
-
* category: 'data',
|
|
1389
|
-
* });
|
|
1390
|
-
*
|
|
1391
|
-
* return (
|
|
1392
|
-
* <div>
|
|
1393
|
-
* {tools.map(tool => (
|
|
1394
|
-
* <div key={tool.name}>
|
|
1395
|
-
* <h3>{tool.name}</h3>
|
|
1396
|
-
* <p>{tool.description}</p>
|
|
1397
|
-
* <button onClick={() => execute(tool.name, { input: 'test' })}>
|
|
1398
|
-
* Execute
|
|
1399
|
-
* </button>
|
|
1400
|
-
* </div>
|
|
1401
|
-
* ))}
|
|
1402
|
-
* </div>
|
|
1403
|
-
* );
|
|
1404
|
-
* }
|
|
1405
|
-
* ```
|
|
1406
|
-
*/
|
|
1407
|
-
export function useTools(options: UseToolsOptions = {}): UseToolsReturn {
|
|
1408
|
-
const { category, serverId, refreshInterval } = options;
|
|
1409
|
-
|
|
1410
|
-
const client = useNeuroLinkClient();
|
|
1411
|
-
const [tools, setTools] = useState<ClientToolInfo[]>([]);
|
|
1412
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
1413
|
-
const [error, setError] = useState<ClientApiError | null>(null);
|
|
1414
|
-
|
|
1415
|
-
/**
|
|
1416
|
-
* Refresh tool list
|
|
1417
|
-
*/
|
|
1418
|
-
const refresh = useCallback(async () => {
|
|
1419
|
-
setIsLoading(true);
|
|
1420
|
-
setError(null);
|
|
1421
|
-
|
|
1422
|
-
try {
|
|
1423
|
-
const response = await client.listTools({ category, serverId });
|
|
1424
|
-
setTools(response.data);
|
|
1425
|
-
} catch (err) {
|
|
1426
|
-
const apiError = err as ClientApiError;
|
|
1427
|
-
setError(apiError);
|
|
1428
|
-
} finally {
|
|
1429
|
-
setIsLoading(false);
|
|
1430
|
-
}
|
|
1431
|
-
}, [client, category, serverId]);
|
|
1432
|
-
|
|
1433
|
-
/**
|
|
1434
|
-
* Execute a tool
|
|
1435
|
-
*/
|
|
1436
|
-
const execute = useCallback(
|
|
1437
|
-
async (toolName: string, params: UnknownRecord): Promise<unknown> => {
|
|
1438
|
-
const response = await client.executeTool(toolName, params);
|
|
1439
|
-
return response.data;
|
|
1440
|
-
},
|
|
1441
|
-
[client],
|
|
1442
|
-
);
|
|
1443
|
-
|
|
1444
|
-
// Initial load
|
|
1445
|
-
useEffect(() => {
|
|
1446
|
-
refresh();
|
|
1447
|
-
}, [refresh]);
|
|
1448
|
-
|
|
1449
|
-
// Auto-refresh interval
|
|
1450
|
-
useEffect(() => {
|
|
1451
|
-
if (refreshInterval && refreshInterval > 0) {
|
|
1452
|
-
const interval = setInterval(refresh, refreshInterval);
|
|
1453
|
-
return () => clearInterval(interval);
|
|
1454
|
-
}
|
|
1455
|
-
return undefined;
|
|
1456
|
-
}, [refreshInterval, refresh]);
|
|
1457
|
-
|
|
1458
|
-
return {
|
|
1459
|
-
tools,
|
|
1460
|
-
execute,
|
|
1461
|
-
refresh,
|
|
1462
|
-
isLoading,
|
|
1463
|
-
error,
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// Type augmentation for SpeechRecognition (browser APIs)
|
|
1468
|
-
declare global {
|
|
1469
|
-
interface Window {
|
|
1470
|
-
SpeechRecognition: { new (): SpeechRecognitionInternal };
|
|
1471
|
-
webkitSpeechRecognition: { new (): SpeechRecognitionInternal };
|
|
1472
|
-
}
|
|
1473
|
-
}
|