Buckets:
evalstate/sandbox-testing-2 / hf-mcp-server /packages /app /src /web /components /GradioWidgetDevShim.tsx
| 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> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 13.1 kB
- Xet hash:
- db0de2f62e16c1d01530b4bb3c9e9a74458b7dafa23c69a12a8535d84b8dcdf8
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.