adk-ui 0.8.0

Dynamic UI generation for ADK-Rust agents - render forms, cards, tables, charts and more
Documentation
import { useState, useEffect } from 'react';
import { ComponentRenderer } from './adk-ui-renderer/Renderer';
import { convertA2UIComponent } from './adk-ui-renderer/a2ui-converter';
import type { Component } from './adk-ui-renderer/types';
import './App.css';

interface Surface {
  surfaceId: string;
  components: Component[];
  dataModel: Record<string, unknown>;
}

interface Example {
  id: string;
  name: string;
  description: string;
  port: number;
}

const EXAMPLES: Example[] = [
  { id: 'ui_demo', name: 'UI Demo', description: 'Basic A2UI demo', port: 8080 },
  { id: 'ui_working_support', name: 'Support Intake', description: 'Support ticket system', port: 8081 },
  { id: 'ui_working_appointment', name: 'Appointments', description: 'Appointment booking', port: 8082 },
  { id: 'ui_working_events', name: 'Events', description: 'Event RSVP system', port: 8083 },
  { id: 'ui_working_facilities', name: 'Facilities', description: 'Work order system', port: 8084 },
  { id: 'ui_working_inventory', name: 'Inventory', description: 'Restock requests', port: 8085 },
];

function App() {
  const [surface, setSurface] = useState<Surface | null>(null);
  const [selectedExample, setSelectedExample] = useState<Example>(EXAMPLES[0]);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  const startSession = async () => {
    try {
      console.log('Starting session for:', selectedExample);
      setIsConnected(false);
      setSurface(null);
      setSessionId(null);

      const baseUrl = `http://localhost:${selectedExample.port}`;
      console.log('Base URL:', baseUrl);
      
      // Create session
      const sessionRes = await fetch(`${baseUrl}/api/apps/${selectedExample.id}/users/user1/sessions`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ state: {} }),
      });

      console.log('Session response status:', sessionRes.status);
      if (!sessionRes.ok) {
        console.error('Failed to create session:', sessionRes.statusText);
        return;
      }

      const session = await sessionRes.json();
      console.log('Session created:', session);
      setSessionId(session.id);

      // Connect to SSE
      const sseUrl = `${baseUrl}/api/run/${selectedExample.id}/user1/${session.id}?message=start`;
      console.log('Connecting to SSE:', sseUrl);
      const eventSource = new EventSource(sseUrl);
        body: JSON.stringify({ state: {} }),
      });

      if (!sessionRes.ok) {
        console.error('Failed to create session');
        return;
      }

      const session = await sessionRes.json();
      setSessionId(session.id);

      // Connect to SSE
      const eventSource = new EventSource(
        `${baseUrl}/api/run/${selectedExample.id}/user1/${session.id}?message=start`
      );

      eventSource.onopen = () => {
        setIsConnected(true);
      };

      eventSource.onmessage = (event) => {
        if (!event.data || event.data === ':keep-alive') return;

        const eventData = event.data.startsWith('data: ') 
          ? event.data.slice(6) 
          : event.data;

        if (!eventData.trim()) return;

        try {
          const evt = JSON.parse(eventData);

          // Extract components from function response
          if (evt.content?.parts) {
            for (const part of evt.content.parts) {
              if (part.functionResponse?.name === 'render_screen') {
                const response = part.functionResponse.response;
                if (response.components) {
                  // Parse components if they're a JSON string
                  const componentsArray = typeof response.components === 'string' 
                    ? JSON.parse(response.components)
                    : response.components;
                  
                  // Build component map
                  const componentMap = new Map<string, any>();
                  componentsArray.forEach((comp: any) => {
                    const converted = convertA2UIComponent(comp);
                    if (converted) {
                      componentMap.set(converted.id, converted);
                    }
                  });
                  
                  // Resolve children IDs to actual components
                  const resolveChildren = (comp: any): any => {
                    if (comp.children && Array.isArray(comp.children)) {
                      return {
                        ...comp,
                        children: comp.children.map((childId: string) => {
                          const child = componentMap.get(childId);
                          return child ? resolveChildren(child) : null;
                        }).filter(Boolean)
                      };
                    }
                    return comp;
                  };
                  
                  // Find root component and resolve its tree
                  const root = componentMap.get('root');
                  if (root) {
                    const resolvedRoot = resolveChildren(root);
                    setSurface({
                      surfaceId: response.surface_id || 'main',
                      components: [resolvedRoot],
                      dataModel: response.data_model || {},
                    });
                  }
                }
              }
            }
          }
        } catch (e) {
          console.error('Failed to parse SSE event:', e);
        }
      };

      eventSource.onerror = () => {
        setIsConnected(false);
        eventSource.close();
      };

      return () => {
        eventSource.close();
      };
    } catch (error) {
      console.error('Failed to start session:', error);
    }
  };

  useEffect(() => {
    startSession();
  }, [selectedExample]);

  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
      {/* Header */}
      <div className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-6 py-4">
        <div className="max-w-7xl mx-auto flex items-center justify-between">
          <div>
            <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
              A2UI Examples
            </h1>
            <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
              {selectedExample.description}
            </p>
          </div>
          
          <div className="flex items-center gap-4">
            <select
              value={selectedExample.id}
              onChange={(e) => {
                const example = EXAMPLES.find(ex => ex.id === e.target.value);
                if (example) setSelectedExample(example);
              }}
              className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
            >
              {EXAMPLES.map(ex => (
                <option key={ex.id} value={ex.id}>{ex.name}</option>
              ))}
            </select>
            
            <div className="flex items-center gap-2">
              <div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
              <span className="text-sm text-gray-600 dark:text-gray-400">
                {isConnected ? 'Connected' : 'Disconnected'}
              </span>
            </div>
          </div>
        </div>
      </div>

      {/* Content */}
      <div className="max-w-7xl mx-auto px-6 py-8">
        {surface && surface.components.length > 0 ? (
          <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
            {surface.components.map((component, index) => (
              <ComponentRenderer key={index} component={component} />
            ))}
          </div>
        ) : (
          <div className="text-center py-12">
            <div className="text-gray-400 dark:text-gray-600 mb-4">
              {isConnected ? (
                <>
                  <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4" />
                  <p>Waiting for UI...</p>
                </>
              ) : (
                <p>Connecting to {selectedExample.name}...</p>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

export default App;