@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
1,160 lines (1,159 loc) • 38.1 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
/**
* React Hooks for NeuroLink Client SDK
*
* Provides React hooks for interacting with NeuroLink agents, chat, workflows,
* and voice features. Compatible with React 18+ and follows React hooks best practices.
*
* @remarks
* This module requires React 18+ as a peer dependency. Install it with:
* ```bash
* npm install react react-dom
* # or
* pnpm add react react-dom
* ```
*
* @module @neurolink/react
*/
import { useState, useCallback, useRef, useEffect, useContext, createContext, useMemo, } from "react";
import { createClient } from "./httpClient.js";
// =============================================================================
// Context and Provider
// =============================================================================
/**
* Context for NeuroLink client
*/
const NeuroLinkContext = createContext(null);
/**
* Provider component for NeuroLink client
*
* Wraps your application to provide the NeuroLink client to all hooks.
*
* @example
* ```tsx
* import { NeuroLinkProvider } from '@neurolink/react';
*
* function App() {
* return (
* <NeuroLinkProvider
* config={{
* baseUrl: 'https://api.neurolink.example.com',
* apiKey: process.env.NEUROLINK_API_KEY,
* }}
* >
* <YourApp />
* </NeuroLinkProvider>
* );
* }
* ```
*/
export function NeuroLinkProvider({ config, children, }) {
const client = useMemo(() => createClient(config), [config]);
return (_jsx(NeuroLinkContext.Provider, { value: client, children: children }));
}
/**
* Hook to access the NeuroLink client
*
* Must be used within a NeuroLinkProvider.
*
* @throws Error if used outside of NeuroLinkProvider
*/
export function useNeuroLinkClient() {
const client = useContext(NeuroLinkContext);
if (!client) {
throw new Error("useNeuroLinkClient must be used within a NeuroLinkProvider. " +
"Wrap your component tree with <NeuroLinkProvider config={...}>.");
}
return client;
}
// =============================================================================
// Utility Hooks
// =============================================================================
/**
* Generate a unique message ID
*/
function generateMessageId() {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// =============================================================================
// useChat Hook
// =============================================================================
/**
* React hook for chat interactions with NeuroLink agents
*
* Provides a chat interface with support for streaming responses,
* tool calls, and conversation history management.
*
* @example Basic usage
* ```tsx
* function ChatComponent() {
* const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
* api: '/api/chat',
* agentId: 'my-agent',
* });
*
* return (
* <div>
* {messages.map(m => (
* <div key={m.id} className={m.role}>
* {m.content}
* </div>
* ))}
* <form onSubmit={handleSubmit}>
* <input value={input} onChange={handleInputChange} />
* <button type="submit" disabled={isLoading}>Send</button>
* </form>
* </div>
* );
* }
* ```
*/
export function useChat(options = {}) {
const { agentId, initialMessages = [], sessionId: initialSessionId, systemPrompt, onFinish, onError, onToolCall, body, generateId = generateMessageId, } = options;
const client = useNeuroLinkClient();
const [messages, setMessages] = useState(initialMessages);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [toolCalls, setToolCalls] = useState([]);
const abortControllerRef = useRef(null);
const sessionIdRef = useRef(initialSessionId);
// Keep a ref to the latest messages so callbacks never see stale state
const messagesRef = useRef(initialMessages);
useEffect(() => {
messagesRef.current = messages;
}, [messages]);
/**
* Handle input change
*/
const handleInputChange = useCallback((e) => {
setInput(e.target.value);
}, []);
/**
* Append a message and get response
*/
const append = useCallback(async (message) => {
const userMessage = {
id: generateId(),
createdAt: new Date(),
...message,
};
setMessages((prev) => [...prev, userMessage]);
setIsLoading(true);
setError(null);
setToolCalls([]);
// Create abort controller
abortControllerRef.current = new AbortController();
const assistantId = generateId();
const currentToolCalls = [];
let assistantContent = "";
// Add placeholder for assistant message
const assistantMessage = {
id: assistantId,
role: "assistant",
content: "",
createdAt: new Date(),
};
setMessages((prev) => [...prev, assistantMessage]);
try {
// Read from the ref so we always have the latest messages
const currentMessages = messagesRef.current;
await client.stream({
input: {
text: userMessage.content,
},
...(agentId ? { context: { agentId } } : {}),
...(systemPrompt ? { systemPrompt } : {}),
...(body
? {
context: {
...(agentId ? { agentId } : {}),
messages: [...currentMessages, userMessage].map((m) => ({
role: m.role,
content: m.content,
})),
sessionId: sessionIdRef.current,
...body,
},
}
: {
context: {
...(agentId ? { agentId } : {}),
messages: [...currentMessages, userMessage].map((m) => ({
role: m.role,
content: m.content,
})),
sessionId: sessionIdRef.current,
},
}),
}, {
onText: (text) => {
assistantContent += text;
setMessages((prev) => prev.map((m) => m.id === assistantId
? { ...m, content: assistantContent }
: m));
},
onToolCall: (toolCall) => {
currentToolCalls.push(toolCall);
setToolCalls([...currentToolCalls]);
onToolCall?.(toolCall);
},
onToolResult: (toolResult) => {
setMessages((prev) => prev.map((m) => m.id === assistantId
? {
...m,
toolCalls: currentToolCalls,
toolResults: [...(m.toolResults ?? []), toolResult],
}
: m));
},
onMetadata: (metadata) => {
if (metadata?.sessionId) {
sessionIdRef.current = metadata.sessionId;
}
},
}, { signal: abortControllerRef.current.signal });
// Final message update
const finalMessage = {
id: assistantId,
role: "assistant",
content: assistantContent,
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
createdAt: new Date(),
};
setMessages((prev) => prev.map((m) => m.id === assistantId ? finalMessage : m));
onFinish?.(finalMessage);
return assistantId;
}
catch (err) {
if (err.name === "AbortError") {
return null;
}
const apiError = err;
setError(apiError);
onError?.(apiError);
return null;
}
finally {
setIsLoading(false);
setToolCalls([]);
abortControllerRef.current = null;
}
}, [
client,
agentId,
systemPrompt,
body,
generateId,
onFinish,
onError,
onToolCall,
]);
/**
* Handle form submission
*/
const handleSubmit = useCallback((e, submitOptions) => {
e?.preventDefault?.();
if (!input.trim())
return;
const message = {
role: "user",
content: input,
metadata: submitOptions?.data,
};
setInput("");
append(message);
}, [input, append]);
/**
* Reload the last assistant message
*/
const reload = useCallback(async () => {
// Read from the ref so we always have the latest messages
const currentMessages = messagesRef.current;
const lastUserMessageIndex = currentMessages.findLastIndex((m) => m.role === "user");
if (lastUserMessageIndex === -1)
return null;
const lastUserMessage = currentMessages[lastUserMessageIndex];
// Remove messages after the last user message
setMessages((prev) => prev.slice(0, lastUserMessageIndex));
return append({
role: "user",
content: lastUserMessage.content,
});
}, [append]);
/**
* Stop streaming
*/
const stop = useCallback(() => {
abortControllerRef.current?.abort();
}, []);
/**
* Clear error
*/
const clearError = useCallback(() => {
setError(null);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
return {
messages,
input,
setInput,
handleInputChange,
handleSubmit,
append,
reload,
stop,
setMessages,
isLoading,
error,
clearError,
toolCalls,
};
}
// =============================================================================
// useAgent Hook
// =============================================================================
/**
* React hook for interacting with NeuroLink agents
*
* Provides methods for executing agents with both streaming
* and non-streaming responses, with session management.
*
* @example Basic usage
* ```tsx
* function AgentComponent() {
* const { execute, isLoading, result, error } = useAgent({
* agentId: 'my-agent',
* onResponse: (result) => console.log('Agent responded:', result),
* });
*
* return (
* <div>
* <button onClick={() => execute('Hello!')}>
* {isLoading ? 'Thinking...' : 'Ask Agent'}
* </button>
* {result && <p>{result.content}</p>}
* {error && <p className="error">{error.message}</p>}
* </div>
* );
* }
* ```
*/
export function useAgent(options) {
const { agentId, sessionId: initialSessionId, onResponse, onError, onToolCall, initialInput, } = options;
const client = useNeuroLinkClient();
const [sessionId, setSessionId] = useState(initialSessionId ?? null);
const [isLoading, setIsLoading] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const hasAutoExecuted = useRef(false);
// Keep a ref to the latest sessionId so callbacks never see stale state
const sessionIdRef = useRef(initialSessionId ?? null);
useEffect(() => {
sessionIdRef.current = sessionId;
}, [sessionId]);
/**
* Execute agent (non-streaming)
*/
const execute = useCallback(async (input, executeOptions) => {
setIsLoading(true);
setError(null);
abortControllerRef.current = new AbortController();
try {
const response = await client.executeAgent({
agentId,
input,
sessionId: sessionIdRef.current ?? undefined,
...executeOptions,
}, { signal: abortControllerRef.current.signal });
const agentResult = response.data;
setResult(agentResult);
setSessionId(agentResult.sessionId);
onResponse?.(agentResult);
return agentResult;
}
catch (err) {
const apiError = err;
setError(apiError);
onError?.(apiError);
throw err;
}
finally {
setIsLoading(false);
abortControllerRef.current = null;
}
}, [client, agentId, onResponse, onError]);
/**
* Stream agent execution
*/
const stream = useCallback(async (input, callbacks) => {
setIsStreaming(true);
setIsLoading(true);
setError(null);
abortControllerRef.current = new AbortController();
try {
await client.streamAgent({
agentId,
input,
sessionId: sessionIdRef.current ?? undefined,
stream: true,
}, {
...callbacks,
onToolCall: (toolCall) => {
callbacks?.onToolCall?.(toolCall);
onToolCall?.(toolCall);
},
onDone: (streamResult) => {
callbacks?.onDone?.(streamResult);
setIsStreaming(false);
},
onError: (apiError) => {
callbacks?.onError?.(apiError);
setError(apiError);
onError?.(apiError);
},
}, { signal: abortControllerRef.current.signal });
}
catch (err) {
const apiError = err;
setError(apiError);
onError?.(apiError);
}
finally {
setIsLoading(false);
setIsStreaming(false);
abortControllerRef.current = null;
}
}, [client, agentId, onToolCall, onError]);
/**
* Abort current execution
*/
const abort = useCallback(() => {
abortControllerRef.current?.abort();
setIsLoading(false);
setIsStreaming(false);
}, []);
/**
* Clear error
*/
const clearError = useCallback(() => {
setError(null);
}, []);
// Auto-execute on mount if initialInput is provided
useEffect(() => {
if (initialInput && !hasAutoExecuted.current) {
hasAutoExecuted.current = true;
execute(initialInput);
}
}, [initialInput, execute]);
// Cleanup on unmount
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
return {
execute,
stream,
sessionId,
setSessionId,
isLoading,
isStreaming,
result,
error,
clearError,
abort,
};
}
// =============================================================================
// useWorkflow Hook
// =============================================================================
/**
* React hook for executing NeuroLink workflows
*
* Provides methods for executing, resuming, and monitoring workflows
* with automatic status polling and suspension handling.
*
* @example Basic usage
* ```tsx
* function WorkflowComponent() {
* const { execute, status, result, isLoading, error } = useWorkflow({
* workflowId: 'data-processing-workflow',
* onComplete: (result) => console.log('Workflow completed:', result),
* onStepComplete: (step) => console.log('Step completed:', step.stepId),
* });
*
* return (
* <div>
* <button onClick={() => execute({ data: inputData })}>
* Run Workflow
* </button>
* {status && <p>Status: {status}</p>}
* {result?.output && <pre>{JSON.stringify(result.output, null, 2)}</pre>}
* </div>
* );
* }
* ```
*/
export function useWorkflow(options) {
const { workflowId, onComplete, onError, onStepComplete, pollInterval = 2000, } = options;
const client = useNeuroLinkClient();
const [runId, setRunId] = useState(null);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const pollIntervalRef = useRef(null);
const previousStepsRef = useRef(new Set());
/**
* Stop polling
*/
const stopPolling = useCallback(() => {
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current);
pollIntervalRef.current = null;
}
}, []);
/**
* Poll for workflow status
*/
const pollStatus = useCallback(async (currentRunId) => {
try {
const response = await client.getWorkflowStatus(workflowId, currentRunId);
const workflowResult = response.data;
setStatus(workflowResult.status);
setResult(workflowResult);
// Check for newly completed steps
if (workflowResult.steps && onStepComplete) {
for (const step of workflowResult.steps) {
if (step.status === "completed" &&
!previousStepsRef.current.has(step.stepId)) {
previousStepsRef.current.add(step.stepId);
onStepComplete(step);
}
}
}
// Handle completion
if (workflowResult.status === "completed") {
stopPolling();
setIsLoading(false);
onComplete?.(workflowResult);
}
else if (workflowResult.status === "failed") {
stopPolling();
setIsLoading(false);
if (workflowResult.error) {
setError(workflowResult.error);
onError?.(workflowResult.error);
}
}
else if (workflowResult.status === "suspended") {
stopPolling();
setIsLoading(false);
}
}
catch (err) {
stopPolling();
setIsLoading(false);
const apiError = err;
setError(apiError);
onError?.(apiError);
}
}, [client, workflowId, onComplete, onError, onStepComplete, stopPolling]);
/**
* Start polling for workflow status
*/
const startPolling = useCallback((currentRunId) => {
stopPolling();
pollIntervalRef.current = setInterval(() => pollStatus(currentRunId), pollInterval);
}, [pollInterval, pollStatus, stopPolling]);
/**
* Execute workflow
*/
const execute = useCallback(async (input, executeOptions) => {
setIsLoading(true);
setError(null);
setStatus(null);
setResult(null);
previousStepsRef.current.clear();
try {
const response = await client.executeWorkflow({
workflowId,
input,
...executeOptions,
});
const workflowResult = response.data;
setRunId(workflowResult.runId);
setStatus(workflowResult.status);
setResult(workflowResult);
// Start polling if workflow is running
if (workflowResult.status === "running") {
startPolling(workflowResult.runId);
}
else if (workflowResult.status === "completed") {
setIsLoading(false);
onComplete?.(workflowResult);
}
else if (workflowResult.status === "failed") {
setIsLoading(false);
if (workflowResult.error) {
setError(workflowResult.error);
onError?.(workflowResult.error);
}
}
return workflowResult;
}
catch (err) {
setIsLoading(false);
const apiError = err;
setError(apiError);
onError?.(apiError);
throw err;
}
}, [client, workflowId, onComplete, onError, startPolling]);
/**
* Resume suspended workflow
*/
const resume = useCallback(async (resumeToken, resumeData) => {
setIsLoading(true);
setError(null);
try {
const response = await client.resumeWorkflow(workflowId, resumeToken, resumeData);
const workflowResult = response.data;
setRunId(workflowResult.runId);
setStatus(workflowResult.status);
setResult(workflowResult);
// Start polling if workflow is running
if (workflowResult.status === "running") {
startPolling(workflowResult.runId);
}
else if (workflowResult.status === "completed") {
setIsLoading(false);
onComplete?.(workflowResult);
}
return workflowResult;
}
catch (err) {
setIsLoading(false);
const apiError = err;
setError(apiError);
onError?.(apiError);
throw err;
}
}, [client, workflowId, onComplete, onError, startPolling]);
/**
* Get workflow status
*/
const getStatus = useCallback(async (statusRunId) => {
const response = await client.getWorkflowStatus(workflowId, statusRunId);
return response.data;
}, [client, workflowId]);
/**
* Cancel workflow execution
*/
const cancel = useCallback(async (cancelRunId) => {
stopPolling();
await client.cancelWorkflow(workflowId, cancelRunId);
setStatus("failed");
setIsLoading(false);
}, [client, workflowId, stopPolling]);
/**
* Clear error
*/
const clearError = useCallback(() => {
setError(null);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
stopPolling();
};
}, [stopPolling]);
return {
execute,
resume,
getStatus,
cancel,
runId,
status,
isLoading,
result,
error,
clearError,
};
}
// =============================================================================
// useVoice Hook
// =============================================================================
/**
* React hook for voice interactions with NeuroLink
*
* Provides voice input (speech recognition) and output (text-to-speech)
* capabilities with support for real-time conversation.
*
* @example Basic usage
* ```tsx
* function VoiceComponent() {
* const {
* startListening,
* stopListening,
* speak,
* isListening,
* isSpeaking,
* transcript,
* isSupported,
* } = useVoice({
* voice: 'en-US-Neural2-C',
* autoPlay: true,
* });
*
* if (!isSupported) {
* return <p>Voice not supported in this browser</p>;
* }
*
* return (
* <div>
* <button onClick={isListening ? stopListening : startListening}>
* {isListening ? 'Stop' : 'Start'} Listening
* </button>
* <p>Transcript: {transcript}</p>
* <button onClick={() => speak('Hello!')}>Speak</button>
* </div>
* );
* }
* ```
*/
export function useVoice(options = {}) {
const { voice, language = "en-US", autoPlay = true, onSpeechStart, onSpeechEnd, onError, api = "/api/tts", enableSpeechRecognition = true, } = options;
const [isListening, setIsListening] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [transcript, setTranscript] = useState("");
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const recognitionRef = useRef(null);
const synthesisRef = useRef(null);
// Check browser support
const isSupported = useMemo(() => {
if (typeof window === "undefined")
return false;
const hasSpeechRecognition = "SpeechRecognition" in window || "webkitSpeechRecognition" in window;
const hasSpeechSynthesis = "speechSynthesis" in window;
return hasSpeechRecognition || hasSpeechSynthesis;
}, []);
/**
* Initialize speech recognition
*/
const initRecognition = useCallback(() => {
if (typeof window === "undefined")
return null;
const SpeechRecognitionCtor = window.SpeechRecognition ||
window.webkitSpeechRecognition;
if (!SpeechRecognitionCtor)
return null;
const recognition = new SpeechRecognitionCtor();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = language;
recognition.onresult = (event) => {
let finalTranscript = "";
let interimTranscript = "";
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalTranscript += result[0].transcript;
}
else {
interimTranscript += result[0].transcript;
}
}
setTranscript(finalTranscript || interimTranscript);
};
recognition.onerror = (event) => {
const apiError = {
code: "SPEECH_RECOGNITION_ERROR",
message: event.error,
status: 500,
};
setError(apiError);
onError?.(apiError);
setIsListening(false);
};
recognition.onend = () => {
setIsListening(false);
};
return recognition;
}, [language, onError]);
/**
* Start listening for voice input
*/
const startListening = useCallback(() => {
if (!enableSpeechRecognition)
return;
if (!recognitionRef.current) {
recognitionRef.current = initRecognition();
}
if (recognitionRef.current) {
setTranscript("");
setError(null);
recognitionRef.current.start();
setIsListening(true);
}
}, [enableSpeechRecognition, initRecognition]);
/**
* Stop listening
*/
const stopListening = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
setIsListening(false);
}
}, []);
/**
* Speak text using TTS
*/
const speak = useCallback(async (text) => {
if (typeof window === "undefined")
return;
setIsSpeaking(true);
onSpeechStart?.();
try {
// Use Web Speech API for basic TTS
if ("speechSynthesis" in window) {
return new Promise((resolve, reject) => {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = language;
if (voice) {
const voices = window.speechSynthesis.getVoices();
const selectedVoice = voices.find((v) => v.name.includes(voice));
if (selectedVoice) {
utterance.voice = selectedVoice;
}
}
utterance.onend = () => {
setIsSpeaking(false);
onSpeechEnd?.();
resolve();
};
utterance.onerror = (event) => {
setIsSpeaking(false);
const apiError = {
code: "SPEECH_SYNTHESIS_ERROR",
message: event.error,
status: 500,
};
setError(apiError);
onError?.(apiError);
reject(event);
};
synthesisRef.current = utterance;
window.speechSynthesis.speak(utterance);
});
}
}
catch (err) {
setIsSpeaking(false);
const apiError = {
code: "TTS_ERROR",
message: err.message,
status: 500,
};
setError(apiError);
onError?.(apiError);
}
}, [voice, language, onSpeechStart, onSpeechEnd, onError]);
/**
* Stop speaking
*/
const stopSpeaking = useCallback(() => {
if (typeof window !== "undefined" && "speechSynthesis" in window) {
window.speechSynthesis.cancel();
setIsSpeaking(false);
}
}, []);
/**
* Submit voice input and get response
*/
const submit = useCallback(async (text) => {
setIsProcessing(true);
setError(null);
try {
const res = await fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text, voice }),
});
if (!res.ok) {
const errorData = await res.json();
throw errorData;
}
const data = await res.json();
const responseText = data.response || data.content || text;
setResponse(responseText);
if (autoPlay) {
await speak(responseText);
}
return responseText;
}
catch (err) {
const apiError = err;
setError(apiError);
onError?.(apiError);
throw err;
}
finally {
setIsProcessing(false);
}
}, [api, voice, autoPlay, speak, onError]);
// Cleanup on unmount
useEffect(() => {
return () => {
recognitionRef.current?.stop();
if (typeof window !== "undefined" && "speechSynthesis" in window) {
window.speechSynthesis.cancel();
}
};
}, []);
return {
startListening,
stopListening,
speak,
stopSpeaking,
submit,
isListening,
isSpeaking,
isProcessing,
transcript,
response,
error,
isSupported,
};
}
// =============================================================================
// useStream Hook
// =============================================================================
/**
* React hook for streaming responses from NeuroLink
*
* @example
* ```tsx
* function StreamComponent() {
* const { start, stop, text, isStreaming } = useStream({
* api: '/api/stream',
* });
*
* return (
* <div>
* <button onClick={() => start({ prompt: 'Tell me a story' })}>
* Start
* </button>
* <button onClick={stop} disabled={!isStreaming}>Stop</button>
* <p>{text}</p>
* </div>
* );
* }
* ```
*/
export function useStream(options = {}) {
const { api = "/api/stream", callbacks } = options;
const [text, setText] = useState("");
const [events, setEvents] = useState([]);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
/**
* Start streaming
*/
const start = useCallback(async (streamOptions) => {
setText("");
setEvents([]);
setError(null);
setIsStreaming(true);
abortControllerRef.current = new AbortController();
try {
const response = await fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify(streamOptions),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
const errorData = await response.json();
throw errorData;
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
const decoder = new TextDecoder();
let buffer = "";
let fullText = "";
while (true) {
const { done, value } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
break;
}
try {
const event = JSON.parse(data);
setEvents((prev) => [...prev, event]);
if (event.type === "text" && event.content) {
fullText += event.content;
setText(fullText);
callbacks?.onText?.(event.content);
}
else if (event.type === "tool-call" && event.toolCall) {
callbacks?.onToolCall?.(event.toolCall);
}
else if (event.type === "tool-result" && event.toolResult) {
callbacks?.onToolResult?.(event.toolResult);
}
else if (event.type === "error" && event.error) {
callbacks?.onError?.(event.error);
}
else if (event.type === "metadata" && event.metadata) {
callbacks?.onMetadata?.(event.metadata);
}
}
catch {
// Ignore parse errors
}
}
}
}
reader.releaseLock();
}
catch (err) {
if (err.name === "AbortError") {
return;
}
const apiError = err;
setError(apiError);
callbacks?.onError?.(apiError);
}
finally {
setIsStreaming(false);
abortControllerRef.current = null;
}
}, [api, callbacks]);
/**
* Stop streaming
*/
const stop = useCallback(() => {
abortControllerRef.current?.abort();
setIsStreaming(false);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
return {
start,
stop,
text,
events,
isStreaming,
error,
};
}
// =============================================================================
// useTools Hook
// =============================================================================
/**
* React hook for accessing and executing NeuroLink tools
*
* @example
* ```tsx
* function ToolsComponent() {
* const { tools, execute, isLoading, error } = useTools({
* category: 'data',
* });
*
* return (
* <div>
* {tools.map(tool => (
* <div key={tool.name}>
* <h3>{tool.name}</h3>
* <p>{tool.description}</p>
* <button onClick={() => execute(tool.name, { input: 'test' })}>
* Execute
* </button>
* </div>
* ))}
* </div>
* );
* }
* ```
*/
export function useTools(options = {}) {
const { category, serverId, refreshInterval } = options;
const client = useNeuroLinkClient();
const [tools, setTools] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
/**
* Refresh tool list
*/
const refresh = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await client.listTools({ category, serverId });
setTools(response.data);
}
catch (err) {
const apiError = err;
setError(apiError);
}
finally {
setIsLoading(false);
}
}, [client, category, serverId]);
/**
* Execute a tool
*/
const execute = useCallback(async (toolName, params) => {
const response = await client.executeTool(toolName, params);
return response.data;
}, [client]);
// Initial load
useEffect(() => {
refresh();
}, [refresh]);
// Auto-refresh interval
useEffect(() => {
if (refreshInterval && refreshInterval > 0) {
const interval = setInterval(refresh, refreshInterval);
return () => clearInterval(interval);
}
return undefined;
}, [refreshInterval, refresh]);
return {
tools,
execute,
refresh,
isLoading,
error,
};
}