@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +75 -51
  3. package/dist/browser/neurolink.min.js +419 -416
  4. package/dist/cli/commands/decide.d.ts +4 -1
  5. package/dist/cli/commands/decide.js +6 -3
  6. package/dist/cli/commands/setup.d.ts +1 -1
  7. package/dist/cli/commands/setup.js +1 -1
  8. package/dist/cli/factories/commandFactory.js +1 -1
  9. package/dist/constants/enums.d.ts +4 -0
  10. package/dist/constants/enums.js +5 -0
  11. package/dist/core/factory.d.ts +7 -0
  12. package/dist/core/factory.js +20 -0
  13. package/dist/factories/providerDescriptors.js +96 -7
  14. package/dist/processors/media/VideoProcessor.d.ts +2 -2
  15. package/dist/providers/catalog/index.generated.d.ts +1 -1
  16. package/dist/providers/catalog/index.generated.js +8 -1
  17. package/dist/providers/catalog/openai-decisions.json +75 -0
  18. package/dist/providers/catalog/provider-catalog.schema.json +56 -3
  19. package/dist/providers/catalog/schema.js +92 -11
  20. package/dist/providers/configuredDecision.js +9 -14
  21. package/dist/providers/decide/dialects/index.js +2 -0
  22. package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
  23. package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
  24. package/dist/providers/decide/dialects/systemOne.js +17 -0
  25. package/dist/providers/systemOneDecision.js +6 -1
  26. package/dist/telemetry/attributes.d.ts +10 -3
  27. package/dist/telemetry/attributes.js +142 -10
  28. package/dist/telemetry/index.d.ts +1 -1
  29. package/dist/telemetry/index.js +1 -1
  30. package/dist/types/classifierRouter.d.ts +21 -9
  31. package/dist/types/cli.d.ts +4 -2
  32. package/dist/types/decision.d.ts +67 -3
  33. package/dist/types/processor.d.ts +10 -0
  34. package/dist/types/providerCatalog.d.ts +7 -11
  35. package/dist/types/providerCatalog.generated.d.ts +2 -2
  36. package/dist/types/providers.d.ts +12 -1
  37. package/dist/utils/pricing.js +9 -0
  38. package/docs-site/static/search-index.json +84 -59
  39. package/package.json +6 -2
  40. 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
- }