Skip to main content
Glama
evalstate

Hugging Face MCP Server

by evalstate
GradioWidgetDevShim.tsx13.1 kB
import { useState, useEffect, useRef } from 'react'; import type { DisplayMode, OpenAiGlobals } from '../hooks'; const DEFAULT_TOOL_OUTPUT = { url: 'https://example.com/audio.wav', }; const DEFAULT_WIDGET_STATE = { lastPlayed: null, }; type MockOpenAiApi = Partial<OpenAiGlobals> & { setWidgetState: (state: unknown) => Promise<void>; callTool: (name: string, args: Record<string, unknown>) => Promise<{ result: string }>; sendFollowUpMessage: (args: { prompt: string }) => Promise<void>; openExternal: (payload: { href: string }) => void; requestDisplayMode: (args: { mode: DisplayMode }) => Promise<{ mode: DisplayMode }>; }; type OpenAiHostWindow = Window & { openai?: MockOpenAiApi }; export function GradioWidgetDevShim() { const iframeRef = useRef<HTMLIFrameElement>(null); const [toolOutputJson, setToolOutputJson] = useState( JSON.stringify(DEFAULT_TOOL_OUTPUT, null, 2) ); const [widgetStateJson, setWidgetStateJson] = useState( JSON.stringify(DEFAULT_WIDGET_STATE, null, 2) ); const [displayMode, setDisplayMode] = useState<DisplayMode>('inline'); const [maxHeight, setMaxHeight] = useState(800); const [theme, setTheme] = useState<'light' | 'dark'>('light'); const [error, setError] = useState<string | null>(null); // Load persisted values from localStorage useEffect(() => { const saved = localStorage.getItem('gradio-widget-dev-state'); if (saved) { try { const state = JSON.parse(saved); if (state.toolOutputJson) setToolOutputJson(state.toolOutputJson); if (state.widgetStateJson) setWidgetStateJson(state.widgetStateJson); if (state.displayMode) setDisplayMode(state.displayMode); if (state.maxHeight) setMaxHeight(state.maxHeight); if (state.theme) setTheme(state.theme); } catch (e) { console.error('Failed to load saved state:', e); } } }, []); // Save to localStorage on change useEffect(() => { const state = { toolOutputJson, widgetStateJson, displayMode, maxHeight, theme, }; localStorage.setItem('gradio-widget-dev-state', JSON.stringify(state)); }, [toolOutputJson, widgetStateJson, displayMode, maxHeight, theme]); // Auto-update when theme, displayMode, or maxHeight changes useEffect(() => { const iframe = iframeRef.current; if (!iframe?.contentWindow) return; const iframeWindow = iframe.contentWindow as OpenAiHostWindow; const openaiApi = iframeWindow.openai; if (!openaiApi) return; // Update the globals openaiApi.displayMode = displayMode; openaiApi.maxHeight = maxHeight; openaiApi.theme = theme; // Dispatch event to trigger hooks const event = new CustomEvent('openai:set_globals', { detail: { globals: { displayMode, maxHeight, theme, }, }, }); iframeWindow.dispatchEvent(event); console.log('[Shim] Auto-updated displayMode, maxHeight, theme'); }, [displayMode, maxHeight, theme]); // Initialize window.openai in iframe when it loads useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; const handleLoad = () => { const iframeWindow = iframe.contentWindow; if (!iframeWindow) return; // Helper to dispatch custom events in iframe const dispatchGlobalsEvent = (globals: Partial<OpenAiGlobals>) => { const event = new CustomEvent('openai:set_globals', { detail: { globals }, }); iframeWindow.dispatchEvent(event); }; // Mock the window.openai API const mockOpenAi: MockOpenAiApi = { theme, locale: 'en-US', displayMode, maxHeight, toolInput: {}, toolOutput: null, toolResponseMetadata: null, widgetState: null, userAgent: { device: { type: 'desktop' }, capabilities: { hover: true, touch: false }, }, safeArea: { insets: { top: 0, bottom: 0, left: 0, right: 0 }, }, setWidgetState: async (state: unknown) => { console.log('[Shim] setWidgetState called:', state); setWidgetStateJson(JSON.stringify(state, null, 2)); mockOpenAi.widgetState = state; dispatchGlobalsEvent({ widgetState: state }); }, callTool: async (name: string, args: Record<string, unknown>) => { console.log('[Shim] callTool called:', name, args); return { result: 'Mock tool response' }; }, sendFollowUpMessage: async (args: { prompt: string }) => { console.log('[Shim] sendFollowUpMessage called:', args); }, openExternal: (payload: { href: string }) => { console.log('[Shim] openExternal called:', payload); window.open(payload.href, '_blank'); }, requestDisplayMode: async (args: { mode: DisplayMode }) => { console.log('[Shim] requestDisplayMode called:', args); setDisplayMode(args.mode); return { mode: args.mode }; }, }; // Inject into iframe's window BEFORE the widget loads const hostWindow = iframeWindow as OpenAiHostWindow; hostWindow.openai = mockOpenAi; console.log('[Shim] window.openai initialized in iframe'); // Auto-send initial data after a short delay to ensure React is ready setTimeout(() => { try { const toolOutput = JSON.parse(toolOutputJson); const widgetState = widgetStateJson.trim() ? JSON.parse(widgetStateJson) : null; mockOpenAi.toolOutput = toolOutput; mockOpenAi.widgetState = widgetState; dispatchGlobalsEvent({ toolOutput, widgetState, displayMode, maxHeight, theme, }); console.log('[Shim] Initial data sent to widget'); } catch (e) { console.error('[Shim] Failed to send initial data:', e); } }, 100); }; iframe.addEventListener('load', handleLoad); return () => iframe.removeEventListener('load', handleLoad); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const sendUpdate = () => { setError(null); const iframe = iframeRef.current; if (!iframe?.contentWindow) { setError('Iframe not loaded'); return; } try { const toolOutput = JSON.parse(toolOutputJson); const widgetState = widgetStateJson.trim() ? JSON.parse(widgetStateJson) : null; const iframeWindow = iframe.contentWindow as OpenAiHostWindow; const openaiApi = iframeWindow.openai; if (!openaiApi) { setError('window.openai not initialized'); return; } // Update the globals openaiApi.toolOutput = toolOutput; openaiApi.widgetState = widgetState; openaiApi.displayMode = displayMode; openaiApi.maxHeight = maxHeight; openaiApi.theme = theme; // Dispatch event to trigger hooks const event = new CustomEvent('openai:set_globals', { detail: { globals: { toolOutput, widgetState, displayMode, maxHeight, theme, }, }, }); iframeWindow.dispatchEvent(event); console.log('[Shim] Update sent to widget'); } catch (e) { setError(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`); } }; return ( <div className="flex h-screen bg-gray-100"> {/* Left: Widget iframe */} <div className="flex-1 p-4 overflow-hidden"> <div className="h-full bg-white rounded-lg shadow-lg overflow-hidden"> <iframe ref={iframeRef} src="/gradio-widget.html" className="w-full h-full border-0" title="Gradio Widget" /> </div> </div> {/* Right: Controls */} <div className="w-96 p-4 overflow-y-auto bg-white border-l border-gray-200"> <div className="flex items-center gap-2 mb-4"> <h1 className="text-2xl font-bold">Widget Dev</h1> <span className="px-2 py-1 text-xs font-semibold bg-blue-100 text-blue-800 rounded"> IFRAME </span> </div> <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg text-blue-700 text-sm"> <strong>Iframe Mode:</strong> Widget loads in isolated context. Check browser console for [Shim] logs. </div> {error && ( <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm"> {error} </div> )} {/* Tool Output */} <div className="mb-6"> <label className="block text-sm font-medium mb-2"> Tool Output (JSON) </label> <textarea value={toolOutputJson} onChange={(e) => setToolOutputJson(e.target.value)} className="w-full h-32 p-2 border border-gray-300 rounded font-mono text-sm" spellCheck={false} /> </div> {/* Widget State */} <div className="mb-6"> <label className="block text-sm font-medium mb-2"> Widget State (JSON) </label> <textarea value={widgetStateJson} onChange={(e) => setWidgetStateJson(e.target.value)} className="w-full h-32 p-2 border border-gray-300 rounded font-mono text-sm" spellCheck={false} /> </div> {/* Display Mode */} <div className="mb-6"> <label className="block text-sm font-medium mb-2">Display Mode</label> <div className="flex gap-2"> {(['inline', 'fullscreen', 'pip'] as DisplayMode[]).map((mode) => ( <button key={mode} onClick={() => setDisplayMode(mode)} className={`px-4 py-2 rounded font-medium ${ displayMode === mode ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300' }`} > {mode} </button> ))} </div> </div> {/* Max Height */} <div className="mb-6"> <label className="block text-sm font-medium mb-2"> Max Height: {maxHeight}px </label> <input type="range" min="400" max="1200" step="50" value={maxHeight} onChange={(e) => setMaxHeight(Number(e.target.value))} className="w-full" /> </div> {/* Theme */} <div className="mb-6"> <label className="block text-sm font-medium mb-2">Theme</label> <div className="flex gap-2"> {(['light', 'dark'] as const).map((t) => ( <button key={t} onClick={() => setTheme(t)} className={`px-4 py-2 rounded font-medium ${ theme === t ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300' }`} > {t} </button> ))} </div> </div> {/* Send Button */} <button onClick={sendUpdate} className="w-full py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors" > Send Update </button> {/* Quick presets */} <div className="mt-8 pt-6 border-t border-gray-200"> <h2 className="text-lg font-semibold mb-3">Quick Presets</h2> <div className="space-y-2"> <button onClick={() => { setToolOutputJson( JSON.stringify( { url: 'https://example.com/audio.wav' }, null, 2 ) ); sendUpdate(); }} className="w-full px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded text-left" > Audio WAV </button> <button onClick={() => { setToolOutputJson( JSON.stringify( { url: 'https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png' }, null, 2 ) ); sendUpdate(); }} className="w-full px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded text-left" > Image PNG </button> <button onClick={() => { setToolOutputJson( JSON.stringify( { url: 'https://example.com/video.mp4' }, null, 2 ) ); sendUpdate(); }} className="w-full px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded text-left" > Video MP4 </button> <button onClick={() => { setToolOutputJson(JSON.stringify({}, null, 2)); sendUpdate(); }} className="w-full px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded text-left" > Empty (no content) </button> <button onClick={() => { setToolOutputJson(''); const iframe = iframeRef.current; if (iframe?.contentWindow) { const iframeWindow = iframe.contentWindow as OpenAiHostWindow; const openaiApi = iframeWindow.openai; if (openaiApi) { openaiApi.toolOutput = null; const event = new CustomEvent('openai:set_globals', { detail: { globals: { toolOutput: null, widgetState: widgetStateJson.trim() ? JSON.parse(widgetStateJson) : null, displayMode, maxHeight, theme, }, }, }); iframeWindow.dispatchEvent(event); console.log('[Shim] Loading state activated'); } } }} className="w-full px-3 py-2 text-sm bg-purple-100 hover:bg-purple-200 rounded text-left" > Show Loading </button> </div> </div> </div> </div> ); }

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/evalstate/hf-mcp-server'

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