UNPKG

@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 38.1 kB
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 { NeuroLinkClient, 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, }; } //# sourceMappingURL=reactHooks.js.map