adk-gateway 1.0.0

Multi-channel AI gateway for adk-rust agents — Telegram, Slack, WhatsApp, Discord, Matrix + control panel
import { useApi } from '../hooks/useApi';
import { api } from '../api/client';
import StatusBadge from '../components/StatusBadge';
import ConfirmDialog from '../components/ConfirmDialog';
import AlertBanner from '../components/AlertBanner';
import type { AwpSummary, AwpCapability, AwpSubscription } from '../types';
import { useState } from 'react';

export default function AWP() {
  const { data: summary, loading } = useApi<AwpSummary>(() => api.awpSummary(), []);
  const { data: capabilities } = useApi<AwpCapability[]>(() => api.awpCapabilities(), []);
  const { data: subscriptions, refetch: refetchSubs } = useApi<AwpSubscription[]>(() => api.awpSubscriptions(), []);
  const { data: consent } = useApi<unknown[]>(() => api.awpConsent(), []);

  const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
  const [alert, setAlert] = useState<{ type: 'success' | 'error'; message: string } | null>(null);

  const isEnabled = !!summary;

  const handleDeleteSubscription = async (id: string) => {
    try {
      const res = await api.deleteAwpSubscription(id);
      if (res.ok) {
        setAlert({ type: 'success', message: 'Subscription deleted.' });
        refetchSubs();
      } else {
        setAlert({ type: 'error', message: res.message || 'Failed to delete.' });
      }
    } catch {
      setAlert({ type: 'error', message: 'Network error.' });
    }
    setDeleteTarget(null);
  };

  if (loading) return <div className="text-gray-400">Loading AWP...</div>;

  return (
    <div>
      <h2 className="text-2xl font-semibold mb-5">AWP (Agentic Web Protocol)</h2>

      {alert && (
        <AlertBanner type={alert.type} message={alert.message} onDismiss={() => setAlert(null)} />
      )}

      {/* Promotional banner when disabled */}
      {!isEnabled && (
        <div className="bg-gradient-to-r from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl p-6 mb-6">
          <div className="flex items-start gap-4">
            <div className="text-4xl">🌐</div>
            <div className="flex-1">
              <h3 className="text-lg font-semibold text-gray-900 mb-2">Make Your Gateway Discoverable</h3>
              <p className="text-sm text-gray-600 mb-3">
                The Agentic Web Protocol (AWP) lets other AI agents discover and interact with your gateway.
                Enable AWP to expose capabilities, accept agent-to-agent messages, and participate in the agentic web.
              </p>
              <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
                <div className="bg-white/70 rounded-lg p-3">
                  <div className="text-sm font-medium text-gray-800">🔍 Discovery</div>
                  <div className="text-xs text-gray-500 mt-1">Other agents find you via <code className="bg-gray-100 px-1 rounded">/.well-known/awp.json</code></div>
                </div>
                <div className="bg-white/70 rounded-lg p-3">
                  <div className="text-sm font-medium text-gray-800">🤝 Agent-to-Agent</div>
                  <div className="text-xs text-gray-500 mt-1">Receive messages from other AI agents via <code className="bg-gray-100 px-1 rounded">/awp/a2a</code></div>
                </div>
                <div className="bg-white/70 rounded-lg p-3">
                  <div className="text-sm font-medium text-gray-800">📋 Capabilities</div>
                  <div className="text-xs text-gray-500 mt-1">Declare what your gateway can do in a machine-readable manifest</div>
                </div>
              </div>
              <div className="flex items-center gap-3">
                <div className="text-xs text-gray-500 bg-white/80 rounded-lg px-3 py-2 font-mono">
                  Add to config: <span className="text-indigo-600">{`"awp": { "enabled": true }`}</span>
                </div>
                <a
                  href="https://agenticwebprotocol.com"
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-xs text-[var(--color-accent)] hover:underline font-medium"
                >
                  Learn more →
                </a>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* Health & Site Info (when enabled) */}
      {isEnabled && (
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
          <div className="bg-white rounded-xl shadow-sm p-5">
            <h3 className="text-sm font-semibold text-gray-500 mb-2">Health</h3>
            <div className="flex items-center gap-3">
              <StatusBadge status={summary.health.state} />
              <span className="text-sm text-gray-600">{summary.health.message}</span>
            </div>
            <div className="text-xs text-gray-400 mt-2">Last updated: {new Date(summary.health.timestamp).toLocaleString()}</div>
          </div>
          <div className="bg-white rounded-xl shadow-sm p-5">
            <h3 className="text-sm font-semibold text-gray-500 mb-2">Site Info</h3>
            <div className="text-sm space-y-1">
              <div><strong>Name:</strong> {summary.site.name}</div>
              <div><strong>Description:</strong> {summary.site.description}</div>
              <div><strong>Domain:</strong> <span className="font-mono">{summary.site.domain}</span></div>
            </div>
          </div>
        </div>
      )}

      {/* Endpoints (always shown — shows what AWP provides) */}
      <h3 className="text-lg font-semibold mb-3">AWP Endpoints</h3>
      <div className="bg-white rounded-xl shadow-sm overflow-hidden mb-6">
        <table className="w-full">
          <thead>
            <tr className="bg-gray-50">
              <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Endpoint</th>
              <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Method</th>
              <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Description</th>
              <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Status</th>
            </tr>
          </thead>
          <tbody>
            {[
              { endpoint: '/.well-known/awp.json', method: 'GET', desc: 'Discovery document', always: true },
              { endpoint: '/awp/manifest', method: 'GET', desc: 'JSON-LD capability manifest', always: true },
              { endpoint: '/awp/health', method: 'GET', desc: 'Health state machine', always: true },
              { endpoint: '/awp/a2a', method: 'POST', desc: 'Agent-to-agent messages', always: false },
              { endpoint: '/awp/events/subscribe', method: 'POST', desc: 'Event subscriptions (HMAC-SHA256)', always: false },
              { endpoint: '/awp/consent/capture', method: 'POST', desc: 'Consent capture', always: false },
              { endpoint: '/awp/consent/check', method: 'GET', desc: 'Consent verification', always: false },
            ].map((ep) => (
              <tr key={ep.endpoint} className="border-t border-gray-100">
                <td className="px-4 py-3 text-sm font-mono">{ep.endpoint}</td>
                <td className="px-4 py-3 text-sm">{ep.method}</td>
                <td className="px-4 py-3 text-sm text-gray-600">{ep.desc}</td>
                <td className="px-4 py-3">
                  <StatusBadge status={isEnabled ? 'active' : 'disabled'} />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Capabilities */}
      <h3 className="text-lg font-semibold mb-3">Capabilities</h3>
      {isEnabled && capabilities && capabilities.length > 0 ? (
        <div className="bg-white rounded-xl shadow-sm overflow-hidden mb-6">
          <table className="w-full">
            <thead>
              <tr className="bg-gray-50">
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Name</th>
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Description</th>
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Endpoint</th>
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Access</th>
              </tr>
            </thead>
            <tbody>
              {capabilities.map((cap) => (
                <tr key={cap.name} className="border-t border-gray-100 hover:bg-gray-50">
                  <td className="px-4 py-3 text-sm font-medium">{cap.name}</td>
                  <td className="px-4 py-3 text-sm text-gray-600">{cap.description}</td>
                  <td className="px-4 py-3 text-sm font-mono text-gray-500">{cap.endpoint}</td>
                  <td className="px-4 py-3"><StatusBadge status={cap.access_level} /></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : (
        <div className="text-center py-6 text-gray-400 bg-white rounded-xl shadow-sm mb-6">
          {isEnabled ? 'No capabilities registered — add them to business.toml' : 'Enable AWP to see registered capabilities'}
        </div>
      )}

      {/* Event Subscriptions */}
      <h3 className="text-lg font-semibold mb-3">Event Subscriptions</h3>
      {isEnabled && subscriptions && subscriptions.length > 0 ? (
        <div className="bg-white rounded-xl shadow-sm overflow-hidden mb-6">
          <table className="w-full">
            <thead>
              <tr className="bg-gray-50">
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Subscriber</th>
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Callback URL</th>
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Events</th>
                <th className="text-left px-4 py-3 text-xs uppercase tracking-wide text-gray-500">Actions</th>
              </tr>
            </thead>
            <tbody>
              {subscriptions.map((sub) => (
                <tr key={sub.id} className="border-t border-gray-100 hover:bg-gray-50">
                  <td className="px-4 py-3 text-sm">{sub.subscriber}</td>
                  <td className="px-4 py-3 text-sm font-mono text-gray-500 break-all">{sub.callback_url}</td>
                  <td className="px-4 py-3 text-sm">{sub.event_types.join(', ')}</td>
                  <td className="px-4 py-3">
                    <button
                      onClick={() => setDeleteTarget(sub.id)}
                      className="px-3 py-1 text-xs font-medium text-red-700 bg-red-50 rounded-lg hover:bg-red-100"
                    >
                      Delete
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : (
        <div className="text-center py-6 text-gray-400 bg-white rounded-xl shadow-sm mb-6">
          {isEnabled ? 'No event subscriptions yet' : 'Enable AWP to manage subscriptions'}
        </div>
      )}

      {/* Consent Records */}
      <h3 className="text-lg font-semibold mb-3">Consent Records</h3>
      {isEnabled && consent && consent.length > 0 ? (
        <div className="bg-white rounded-xl shadow-sm p-4 mb-6">
          <pre className="text-sm font-mono text-gray-600 overflow-auto max-h-[300px]">
            {JSON.stringify(consent, null, 2)}
          </pre>
        </div>
      ) : (
        <div className="text-center py-6 text-gray-400 bg-white rounded-xl shadow-sm">
          {isEnabled ? 'No consent records' : 'Enable AWP to track consent'}
        </div>
      )}

      {deleteTarget && (
        <ConfirmDialog
          title="Delete Subscription"
          message={`Are you sure you want to delete subscription ${deleteTarget}?`}
          confirmLabel="Delete"
          destructive
          onConfirm={() => handleDeleteSubscription(deleteTarget)}
          onCancel={() => setDeleteTarget(null)}
        />
      )}
    </div>
  );
}