Skip to main content
Glama
useWebSocket.ts7.29 kB
import { ref, onMounted, onUnmounted } from 'vue'; export interface AvailableAction { name: string; description: string; inputSchema: Record<string, unknown>; reason?: string; } // Single-field input request export interface InputRequest { prompt: string; inputType: 'text' | 'textarea' | 'number'; placeholder?: string; defaultValue?: string; requestId: string; content?: string; // Optional markdown content to display above the input form } // Multi-field form types export interface FormField { key: string; label: string; type: 'text' | 'textarea' | 'number' | 'checkbox' | 'select'; placeholder?: string; defaultValue?: string | number | boolean; required?: boolean; options?: string[]; // For select fields } export interface MultiFieldRequest { fields: FormField[]; content?: string; // Optional markdown content to display above the form requestId: string; } // Union type for any input request export type AnyInputRequest = InputRequest | MultiFieldRequest; // Type guard to check if request is multi-field export function isMultiFieldRequest(request: AnyInputRequest | null): request is MultiFieldRequest { return request !== null && 'fields' in request; } export type InputStatus = 'idle' | 'pending' | 'submitted' | 'cancelled'; export interface StateData { currentState: string; text: string; contentType: 'text' | 'markdown'; historyCount: number; lastAction: string | null; lastError: string | null; availableActions: AvailableAction[]; inputRequest: AnyInputRequest | null; inputStatus: InputStatus; userInput: string | null; multiFieldInput: Record<string, unknown> | null; userContext: Record<string, unknown>; sidebarVisible: boolean; } export interface ActionLogEntry { action: string; timestamp: string; detail: string; } // Compute a meaningful detail string based on action type function getActionDetail(data: StateData): string { const action = data.lastAction; switch (action) { case 'set_text': case 'set_markdown': // Show the text that was set (truncated) return truncate(data.text, 100); case 'append_text': return `Appended to text (now ${data.text.length} chars)`; case 'show_input': // Show the prompt that was displayed if (data.inputRequest && !isMultiFieldRequest(data.inputRequest)) { return data.inputRequest.prompt || 'Input requested'; } return 'Input requested'; case 'show_multi_form': // Show the number of fields if (data.inputRequest && isMultiFieldRequest(data.inputRequest)) { return `${data.inputRequest.fields.length} field form`; } return 'Multi-field form'; case 'input_submitted': // Show the value that was submitted return data.userInput || '(empty)'; case 'multi_form_submitted': // Show number of values submitted if (data.multiFieldInput) { const keys = Object.keys(data.multiFieldInput); return `${keys.length} values submitted`; } return 'Form submitted'; case 'input_cancelled': return 'User cancelled input'; case 'clear_text': return 'Text cleared'; case 'undo': return 'Restored previous state'; case 'reset': return 'Reset to initial state'; default: return data.text || '(empty)'; } } // Truncate string with ellipsis function truncate(str: string, maxLength: number): string { if (str.length <= maxLength) return str || '(empty)'; return str.slice(0, maxLength) + '...'; } export function useWebSocket(url: string = 'ws://localhost:8765') { const connected = ref(false); const state = ref<StateData>({ currentState: 'idle', text: '', contentType: 'text', historyCount: 0, lastAction: null, lastError: null, availableActions: [], inputRequest: null, inputStatus: 'idle', userInput: null, multiFieldInput: null, userContext: {}, sidebarVisible: true, }); const actionLog = ref<ActionLogEntry[]>([]); const error = ref<string | null>(null); let ws: WebSocket | null = null; let reconnectTimer: number | null = null; function connect() { try { ws = new WebSocket(url); ws.onopen = () => { connected.value = true; error.value = null; console.log('WebSocket connected'); }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'state_update') { const oldAction = state.value.lastAction; state.value = message.data; // Log the action if it changed if (message.data.lastAction && message.data.lastAction !== oldAction) { // Compute detail based on action type const detail = getActionDetail(message.data); actionLog.value.unshift({ action: message.data.lastAction, timestamp: message.timestamp || new Date().toISOString(), detail, }); // Keep only last 20 entries if (actionLog.value.length > 20) { actionLog.value.pop(); } } } } catch (e) { console.error('Failed to parse WebSocket message:', e); } }; ws.onclose = () => { connected.value = false; console.log('WebSocket disconnected, reconnecting...'); scheduleReconnect(); }; ws.onerror = (e) => { error.value = 'WebSocket error'; console.error('WebSocket error:', e); }; } catch (e) { error.value = 'Failed to connect'; scheduleReconnect(); } } function scheduleReconnect() { if (reconnectTimer) return; reconnectTimer = window.setTimeout(() => { reconnectTimer = null; connect(); }, 2000); } onMounted(() => { connect(); }); onUnmounted(() => { if (reconnectTimer) { clearTimeout(reconnectTimer); } if (ws) { ws.close(); } }); // Send message to server function sendMessage(message: { type: string; payload: Record<string, unknown> }) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } else { console.error('WebSocket not connected'); } } // Submit user input function submitInput(value: string, requestId: string) { sendMessage({ type: 'submit_input', payload: { value, requestId }, }); } // Cancel input request function cancelInput(requestId: string) { sendMessage({ type: 'cancel_input', payload: { requestId }, }); } // Submit multi-field form function submitMultiForm(values: Record<string, unknown>, requestId: string) { sendMessage({ type: 'submit_multi_form', payload: { values, requestId }, }); } // Toggle sidebar visibility function toggleSidebar() { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'toggle_sidebar' })); } else { console.error('WebSocket not connected'); } } return { connected, state, actionLog, error, submitInput, cancelInput, submitMultiForm, toggleSidebar, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/uptownhr/pane'

If you have feedback or need assistance with the MCP directory API, please join our Discord server